@aerogel/core 0.0.0-next.a68f133e2c9a1ae9ba84b4e2e42df909289e5fba → 0.0.0-next.aa6e27a9c197d1ee10c9fe018ee8c0fc6ff77767

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 (191) hide show
  1. package/dist/aerogel-core.d.ts +1948 -1460
  2. package/dist/aerogel-core.js +3223 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +27 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +26 -16
  7. package/src/bootstrap/options.ts +1 -1
  8. package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -10
  11. package/src/components/AppToasts.vue +16 -0
  12. package/src/components/composition.ts +23 -0
  13. package/src/components/contracts/AlertModal.ts +4 -0
  14. package/src/components/contracts/Button.ts +16 -0
  15. package/src/components/contracts/ConfirmModal.ts +41 -0
  16. package/src/components/contracts/DropdownMenu.ts +11 -0
  17. package/src/components/contracts/ErrorReportModal.ts +29 -0
  18. package/src/components/contracts/Input.ts +26 -0
  19. package/src/components/contracts/LoadingModal.ts +18 -0
  20. package/src/components/contracts/Modal.ts +13 -0
  21. package/src/components/contracts/PromptModal.ts +28 -0
  22. package/src/components/contracts/Select.ts +36 -0
  23. package/src/components/contracts/Toast.ts +13 -0
  24. package/src/components/contracts/index.ts +9 -0
  25. package/src/components/contracts/shared.ts +9 -0
  26. package/src/components/headless/HeadlessButton.vue +50 -0
  27. package/src/components/headless/HeadlessInput.vue +59 -0
  28. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  29. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  30. package/src/components/headless/HeadlessInputInput.vue +75 -0
  31. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  32. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  33. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +17 -17
  34. package/src/components/headless/HeadlessModalContent.vue +24 -0
  35. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  36. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  37. package/src/components/headless/HeadlessSelect.vue +105 -0
  38. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +30 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +15 -0
  44. package/src/components/headless/HeadlessToast.vue +18 -0
  45. package/src/components/headless/HeadlessToastAction.vue +13 -0
  46. package/src/components/headless/index.ts +18 -3
  47. package/src/components/index.ts +5 -9
  48. package/src/components/ui/AdvancedOptions.vue +18 -0
  49. package/src/components/ui/AlertModal.vue +13 -0
  50. package/src/components/ui/Button.vue +98 -0
  51. package/src/components/ui/Checkbox.vue +56 -0
  52. package/src/components/ui/ConfirmModal.vue +42 -0
  53. package/src/components/ui/DropdownMenu.vue +33 -0
  54. package/src/components/ui/EditableContent.vue +82 -0
  55. package/src/components/ui/ErrorMessage.vue +15 -0
  56. package/src/components/ui/ErrorReportModal.vue +62 -0
  57. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  58. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  59. package/src/components/ui/Form.vue +24 -0
  60. package/src/components/ui/Input.vue +56 -0
  61. package/src/components/ui/Link.vue +12 -0
  62. package/src/components/ui/LoadingModal.vue +32 -0
  63. package/src/components/ui/Markdown.vue +69 -0
  64. package/src/components/ui/Modal.vue +70 -0
  65. package/src/components/ui/ModalContext.vue +30 -0
  66. package/src/components/ui/ProgressBar.vue +50 -0
  67. package/src/components/ui/PromptModal.vue +35 -0
  68. package/src/components/ui/Select.vue +21 -0
  69. package/src/components/ui/SelectLabel.vue +10 -0
  70. package/src/components/ui/SelectOptions.vue +31 -0
  71. package/src/components/ui/SelectTrigger.vue +29 -0
  72. package/src/components/ui/SettingsModal.vue +51 -0
  73. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  74. package/src/components/ui/Toast.vue +42 -0
  75. package/src/components/ui/index.ts +27 -0
  76. package/src/components/utils.ts +106 -9
  77. package/src/directives/index.ts +11 -5
  78. package/src/directives/measure.ts +34 -6
  79. package/src/errors/Errors.state.ts +1 -1
  80. package/src/errors/Errors.ts +24 -32
  81. package/src/errors/JobCancelledError.ts +3 -0
  82. package/src/errors/index.ts +10 -6
  83. package/src/errors/utils.ts +35 -0
  84. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  85. package/src/forms/{Form.ts → FormController.ts} +85 -25
  86. package/src/forms/composition.ts +4 -4
  87. package/src/forms/index.ts +3 -1
  88. package/src/forms/utils.ts +36 -5
  89. package/src/forms/validation.ts +19 -0
  90. package/src/index.css +41 -0
  91. package/src/{main.ts → index.ts} +3 -0
  92. package/src/jobs/Job.ts +147 -0
  93. package/src/jobs/index.ts +10 -0
  94. package/src/jobs/listeners.ts +3 -0
  95. package/src/jobs/status.ts +4 -0
  96. package/src/lang/DefaultLangProvider.ts +46 -0
  97. package/src/lang/Lang.state.ts +11 -0
  98. package/src/lang/Lang.ts +43 -28
  99. package/src/lang/index.ts +8 -6
  100. package/src/plugins/Plugin.ts +1 -1
  101. package/src/plugins/index.ts +10 -7
  102. package/src/services/App.state.ts +26 -3
  103. package/src/services/App.ts +11 -3
  104. package/src/services/Cache.ts +43 -0
  105. package/src/services/Events.test.ts +39 -0
  106. package/src/services/Events.ts +111 -31
  107. package/src/services/Service.ts +129 -54
  108. package/src/services/Storage.ts +20 -0
  109. package/src/services/index.ts +13 -5
  110. package/src/services/utils.ts +18 -0
  111. package/src/testing/index.ts +26 -0
  112. package/src/testing/setup.ts +11 -0
  113. package/src/ui/UI.state.ts +19 -7
  114. package/src/ui/UI.ts +184 -76
  115. package/src/ui/index.ts +19 -18
  116. package/src/ui/utils.ts +16 -0
  117. package/src/utils/composition/events.ts +2 -2
  118. package/src/utils/composition/forms.ts +4 -3
  119. package/src/utils/composition/persistent.test.ts +33 -0
  120. package/src/utils/composition/persistent.ts +11 -0
  121. package/src/utils/composition/state.test.ts +47 -0
  122. package/src/utils/composition/state.ts +24 -0
  123. package/src/utils/index.ts +2 -0
  124. package/src/utils/markdown.test.ts +50 -0
  125. package/src/utils/markdown.ts +19 -6
  126. package/src/utils/vdom.ts +31 -0
  127. package/src/utils/vue.ts +22 -19
  128. package/dist/aerogel-core.cjs.js +0 -2
  129. package/dist/aerogel-core.cjs.js.map +0 -1
  130. package/dist/aerogel-core.esm.js +0 -2
  131. package/dist/aerogel-core.esm.js.map +0 -1
  132. package/histoire.config.ts +0 -7
  133. package/noeldemartin.config.js +0 -5
  134. package/postcss.config.js +0 -6
  135. package/src/assets/histoire.css +0 -3
  136. package/src/components/AGAppSnackbars.vue +0 -13
  137. package/src/components/constants.ts +0 -8
  138. package/src/components/forms/AGButton.vue +0 -44
  139. package/src/components/forms/AGCheckbox.vue +0 -41
  140. package/src/components/forms/AGForm.vue +0 -26
  141. package/src/components/forms/AGInput.vue +0 -38
  142. package/src/components/forms/AGSelect.story.vue +0 -46
  143. package/src/components/forms/AGSelect.vue +0 -60
  144. package/src/components/forms/index.ts +0 -5
  145. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  146. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  147. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  148. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  149. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  150. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  151. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  152. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  153. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  154. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  155. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  156. package/src/components/headless/forms/index.ts +0 -14
  157. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  158. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  159. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  160. package/src/components/headless/modals/index.ts +0 -4
  161. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  162. package/src/components/headless/snackbars/index.ts +0 -40
  163. package/src/components/lib/AGErrorMessage.vue +0 -16
  164. package/src/components/lib/AGLink.vue +0 -9
  165. package/src/components/lib/AGMarkdown.vue +0 -36
  166. package/src/components/lib/AGMeasured.vue +0 -15
  167. package/src/components/lib/index.ts +0 -5
  168. package/src/components/modals/AGAlertModal.ts +0 -15
  169. package/src/components/modals/AGAlertModal.vue +0 -14
  170. package/src/components/modals/AGConfirmModal.ts +0 -27
  171. package/src/components/modals/AGConfirmModal.vue +0 -26
  172. package/src/components/modals/AGErrorReportModal.ts +0 -46
  173. package/src/components/modals/AGErrorReportModal.vue +0 -54
  174. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  175. package/src/components/modals/AGLoadingModal.ts +0 -23
  176. package/src/components/modals/AGLoadingModal.vue +0 -15
  177. package/src/components/modals/AGModal.ts +0 -10
  178. package/src/components/modals/AGModal.vue +0 -39
  179. package/src/components/modals/AGModalContext.ts +0 -8
  180. package/src/components/modals/AGModalContext.vue +0 -22
  181. package/src/components/modals/AGModalTitle.vue +0 -9
  182. package/src/components/modals/AGPromptModal.ts +0 -30
  183. package/src/components/modals/AGPromptModal.vue +0 -34
  184. package/src/components/modals/index.ts +0 -17
  185. package/src/components/snackbars/AGSnackbar.vue +0 -36
  186. package/src/components/snackbars/index.ts +0 -3
  187. package/src/directives/initial-focus.ts +0 -11
  188. package/src/main.histoire.ts +0 -1
  189. package/tailwind.config.js +0 -4
  190. package/tsconfig.json +0 -11
  191. package/vite.config.ts +0 -14
@@ -0,0 +1,39 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import Events, { EventListenerPriorities } from './Events';
4
+
5
+ describe('Events', () => {
6
+
7
+ beforeEach(() => void Events.reset());
8
+
9
+ it('registers listeners', async () => {
10
+ // Arrange
11
+ let counter = 0;
12
+
13
+ Events.on('trigger', () => counter++);
14
+
15
+ // Act
16
+ await Events.emit('trigger');
17
+ await Events.emit('trigger');
18
+ await Events.emit('trigger');
19
+
20
+ // Assert
21
+ expect(counter).toEqual(3);
22
+ });
23
+
24
+ it('triggers listeners by priority', async () => {
25
+ // Arrange
26
+ const storage: string[] = [];
27
+
28
+ Events.on('trigger', () => storage.push('second'));
29
+ Events.on('trigger', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
30
+ Events.on('trigger', { priority: EventListenerPriorities.High }, () => storage.push('first'));
31
+
32
+ // Act
33
+ await Events.emit('trigger');
34
+
35
+ // Assert
36
+ expect(storage).toEqual(['first', 'second', 'third']);
37
+ });
38
+
39
+ });
@@ -1,9 +1,13 @@
1
- import { arr, facade, tap } from '@noeldemartin/utils';
2
- import type { FluentArray } from '@noeldemartin/utils';
1
+ import { arrayRemove, facade, fail, tap } from '@noeldemartin/utils';
3
2
 
4
- import Service from '@/services/Service';
3
+ import Service from '@aerogel/core/services/Service';
5
4
 
6
5
  export interface EventsPayload {}
6
+ export interface EventListenerOptions {
7
+ priority: EventListenerPriority;
8
+ }
9
+ export type AerogelGlobalEvents = Partial<{ [Event in EventWithoutPayload]: () => unknown }> &
10
+ Partial<{ [Event in EventWithPayload]: EventListener<EventsPayload[Event]> }>;
7
11
 
8
12
  export type EventListener<T = unknown> = (payload: T) => unknown;
9
13
  export type UnknownEvent<T> = T extends keyof EventsPayload ? never : T;
@@ -16,70 +20,146 @@ export type EventWithPayload = {
16
20
  [K in keyof EventsPayload]: EventsPayload[K] extends void ? never : K;
17
21
  }[keyof EventsPayload];
18
22
 
23
+ export const EventListenerPriorities = {
24
+ Low: -256,
25
+ Default: 0,
26
+ High: 256,
27
+ } as const;
28
+
29
+ export type EventListenerPriority = (typeof EventListenerPriorities)[keyof typeof EventListenerPriorities];
30
+
19
31
  export class EventsService extends Service {
20
32
 
21
- private listeners: Record<string, FluentArray<EventListener>> = {};
33
+ private listeners: Record<string, { priorities: number[]; handlers: Record<number, EventListener[]> }> = {};
34
+
35
+ protected override async boot(): Promise<void> {
36
+ Object.entries(globalThis.__aerogelEvents__ ?? {}).forEach(([event, listener]) =>
37
+ this.on(event as string, listener as EventListener));
38
+ }
22
39
 
23
40
  public emit<Event extends EventWithoutPayload>(event: Event): Promise<void>;
24
41
  public emit<Event extends EventWithPayload>(event: Event, payload: EventsPayload[Event]): Promise<void>;
25
42
  public emit<Event extends string>(event: UnknownEvent<Event>, payload?: unknown): Promise<void>;
26
43
  public async emit(event: string, payload?: unknown): Promise<void> {
27
- const listeners = [...(this.listeners[event] ?? [])];
44
+ const listeners = this.listeners[event] ?? { priorities: [], handlers: {} };
28
45
 
29
- await Promise.all(listeners.map((listener) => listener(payload)) ?? []);
46
+ for (const priority of listeners.priorities) {
47
+ await Promise.all(listeners.handlers[priority]?.map((listener) => listener(payload)) ?? []);
48
+ }
30
49
  }
31
50
 
51
+ /* eslint-disable max-len */
32
52
  public on<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): () => void;
33
- public on<Event extends EventWithPayload>(
34
- event: Event,
35
- listener: EventListener<EventsPayload[Event]>
36
- ): () => void | void;
37
-
53
+ public on<Event extends EventWithoutPayload>(event: Event, priority: EventListenerPriority, listener: () => unknown): () => void; // prettier-ignore
54
+ public on<Event extends EventWithoutPayload>(event: Event, options: Partial<EventListenerOptions>, listener: () => unknown): () => void; // prettier-ignore
55
+ public on<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
56
+ public on<Event extends EventWithPayload>(event: Event, priority: EventListenerPriority, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
57
+ public on<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
38
58
  public on<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
39
- public on(event: string, listener: EventListener): () => void {
40
- (this.listeners[event] ??= arr<EventListener>([])).push(listener);
41
-
42
- return () => this.off(event, listener);
59
+ public on<Event extends string>(event: UnknownEvent<Event>, priority: EventListenerPriority, listener: EventListener): () => void; // prettier-ignore
60
+ public on<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
61
+ /* eslint-enable max-len */
62
+
63
+ public on(
64
+ event: string,
65
+ optionsOrListener: Partial<EventListenerOptions> | EventListenerPriority | EventListener,
66
+ listener?: EventListener,
67
+ ): () => void {
68
+ const options =
69
+ typeof optionsOrListener === 'function'
70
+ ? {}
71
+ : typeof optionsOrListener === 'number'
72
+ ? { priority: optionsOrListener }
73
+ : optionsOrListener;
74
+ const handler = typeof optionsOrListener === 'function' ? optionsOrListener : (listener as EventListener);
75
+
76
+ this.registerListener(event, options, handler);
77
+
78
+ return () => this.off(event, handler);
43
79
  }
44
80
 
81
+ /* eslint-disable max-len */
45
82
  public once<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): () => void;
46
- public once<Event extends EventWithPayload>(
47
- event: Event,
48
- listener: EventListener<EventsPayload[Event]>
49
- ): () => void | void;
50
-
83
+ public once<Event extends EventWithoutPayload>(event: Event, options: Partial<EventListenerOptions>, listener: () => unknown): () => void; // prettier-ignore
84
+ public once<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
85
+ public once<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
51
86
  public once<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
52
- public once(event: string, listener: EventListener): () => void {
87
+ public once<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
88
+ /* eslint-enable max-len */
89
+
90
+ public once(
91
+ event: string,
92
+ optionsOrListener: Partial<EventListenerOptions> | EventListener,
93
+ listener?: EventListener,
94
+ ): () => void {
53
95
  let onceListener: EventListener | null = null;
96
+ const options = typeof optionsOrListener === 'function' ? {} : optionsOrListener;
97
+ const handler = typeof optionsOrListener === 'function' ? optionsOrListener : (listener as EventListener);
54
98
 
55
99
  return tap(
56
100
  () => onceListener && this.off(event, onceListener),
57
101
  (off) => {
58
- (this.listeners[event] ??= arr<EventListener>([])).push(
59
- (onceListener = (...args) => {
60
- off();
102
+ onceListener = (...args) => {
103
+ off();
61
104
 
62
- return listener(...args);
63
- }),
64
- );
105
+ return handler(...args);
106
+ };
107
+
108
+ this.registerListener(event, options, handler);
65
109
  },
66
110
  );
67
111
  }
68
112
 
69
113
  public off(event: string, listener: EventListener): void {
70
- const eventListeners = this.listeners[event];
114
+ const listeners = this.listeners[event];
71
115
 
72
- if (!eventListeners) {
116
+ if (!listeners) {
73
117
  return;
74
118
  }
75
119
 
76
- eventListeners.remove(listener);
120
+ const priorities = [...listeners.priorities];
121
+
122
+ for (const priority of priorities) {
123
+ arrayRemove(listeners.handlers[priority] ?? [], listener);
77
124
 
78
- if (eventListeners.isEmpty()) {
125
+ if (listeners.handlers[priority]?.length === 0) {
126
+ delete listeners.handlers[priority];
127
+ arrayRemove(listeners.priorities, priority);
128
+ }
129
+ }
130
+
131
+ if (listeners.priorities.length === 0) {
79
132
  delete this.listeners[event];
80
133
  }
81
134
  }
82
135
 
136
+ protected registerListener(event: string, options: Partial<EventListenerOptions>, handler: EventListener): void {
137
+ const priority = options.priority ?? 0;
138
+
139
+ if (!(event in this.listeners)) {
140
+ this.listeners[event] = { priorities: [], handlers: {} };
141
+ }
142
+
143
+ const priorities =
144
+ this.listeners[event]?.priorities ?? fail<number[]>(`priorities missing for event '${event}'`);
145
+ const handlers =
146
+ this.listeners[event]?.handlers ??
147
+ fail<Record<number, EventListener[]>>(`handlers missing for event '${event}'`);
148
+
149
+ if (!priorities.includes(priority)) {
150
+ priorities.push(priority);
151
+ priorities.sort((a, b) => b - a);
152
+ handlers[priority] = [];
153
+ }
154
+
155
+ handlers[priority]?.push(handler);
156
+ }
157
+
83
158
  }
84
159
 
85
160
  export default facade(EventsService);
161
+
162
+ declare global {
163
+ // eslint-disable-next-line no-var
164
+ var __aerogelEvents__: AerogelGlobalEvents | undefined;
165
+ }
@@ -1,49 +1,68 @@
1
- import { MagicObject, PromisedValue, Storage, isEmpty, objectDeepClone, objectOnly } from '@noeldemartin/utils';
2
- import type { Constructor } from '@noeldemartin/utils';
3
- import type { MaybeRef } from 'vue';
1
+ import {
2
+ MagicObject,
3
+ PromisedValue,
4
+ Storage,
5
+ arrayFrom,
6
+ fail,
7
+ isEmpty,
8
+ objectDeepClone,
9
+ objectOnly,
10
+ } from '@noeldemartin/utils';
11
+ import type { Constructor, Nullable } from '@noeldemartin/utils';
4
12
  import type { Store } from 'pinia';
5
13
 
6
- import ServiceBootError from '@/errors/ServiceBootError';
7
- import { defineServiceStore } from '@/services/store';
14
+ import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
15
+ import { defineServiceStore } from '@aerogel/core/services/store';
16
+ import type { Unref } from '@aerogel/core/utils/vue';
8
17
 
9
18
  export type ServiceState = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
10
19
  export type DefaultServiceState = any; // eslint-disable-line @typescript-eslint/no-explicit-any
11
20
  export type ServiceConstructor<T extends Service = Service> = Constructor<T> & typeof Service;
12
- export type UnrefServiceState<State extends ServiceState> = {
13
- [K in keyof State]: State[K] extends MaybeRef<infer T> ? T : State[K];
14
- };
15
21
 
16
22
  export type ComputedStateDefinition<TState extends ServiceState, TComputedState extends ServiceState> = {
17
- [K in keyof TComputedState]: (state: UnrefServiceState<TState>) => TComputedState[K];
23
+ [K in keyof TComputedState]: (state: Unref<TState>) => TComputedState[K];
18
24
  } & ThisType<{
19
25
  readonly [K in keyof TComputedState]: TComputedState[K];
20
26
  }>;
21
27
 
28
+ export type StateWatchers<TService extends Service, TState extends ServiceState> = {
29
+ [K in keyof TState]?: (this: TService, value: TState[K], oldValue: TState[K]) => unknown;
30
+ };
31
+
32
+ export type ServiceWithState<
33
+ State extends ServiceState = ServiceState,
34
+ ComputedState extends ServiceState = {},
35
+ ServiceStorage = Partial<State>,
36
+ > = Constructor<Unref<State>> &
37
+ Constructor<ComputedState> &
38
+ Constructor<Service<Unref<State>, ComputedState, Unref<ServiceStorage>>>;
39
+
22
40
  export function defineServiceState<
23
41
  State extends ServiceState = ServiceState,
24
- ComputedState extends ServiceState = {}
42
+ ComputedState extends ServiceState = {},
43
+ ServiceStorage = Partial<State>,
25
44
  >(options: {
26
45
  name: string;
27
46
  initialState: State | (() => State);
28
47
  persist?: (keyof State)[];
48
+ watch?: StateWatchers<Service, State>;
29
49
  computed?: ComputedStateDefinition<State, ComputedState>;
30
- serialize?: (state: Partial<State>) => Partial<State>;
31
- }): Constructor<UnrefServiceState<State>> &
32
- Constructor<ComputedState> &
33
- Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>> {
34
- return class extends Service<UnrefServiceState<State>, ComputedState> {
50
+ serialize?: (state: Partial<State>) => ServiceStorage;
51
+ restore?: (state: ServiceStorage) => Partial<State>;
52
+ }): ServiceWithState<State, ComputedState, ServiceStorage> {
53
+ return class extends Service<Unref<State>, ComputedState, ServiceStorage> {
35
54
 
36
- public static persist = (options.persist as string[]) ?? [];
55
+ public static override persist = (options.persist as string[]) ?? [];
37
56
 
38
- protected usesStore(): boolean {
57
+ protected override usesStore(): boolean {
39
58
  return true;
40
59
  }
41
60
 
42
- protected getName(): string | null {
61
+ protected override getName(): string | null {
43
62
  return options.name ?? null;
44
63
  }
45
64
 
46
- protected getInitialState(): UnrefServiceState<State> {
65
+ protected override getInitialState(): Unref<State> {
47
66
  if (typeof options.initialState === 'function') {
48
67
  return options.initialState();
49
68
  }
@@ -64,26 +83,32 @@ export function defineServiceState<
64
83
  state[key as keyof State] = value;
65
84
 
66
85
  return state;
67
- }, {} as UnrefServiceState<State>);
86
+ }, {} as Unref<State>);
87
+ }
88
+
89
+ protected override getComputedStateDefinition(): ComputedStateDefinition<Unref<State>, ComputedState> {
90
+ return (options.computed ?? {}) as ComputedStateDefinition<Unref<State>, ComputedState>;
68
91
  }
69
92
 
70
- protected getComputedStateDefinition(): ComputedStateDefinition<UnrefServiceState<State>, ComputedState> {
71
- return (options.computed ?? {}) as ComputedStateDefinition<UnrefServiceState<State>, ComputedState>;
93
+ protected override getStateWatchers(): StateWatchers<Service, Unref<State>> {
94
+ return (options.watch ?? {}) as StateWatchers<Service, Unref<State>>;
72
95
  }
73
96
 
74
- protected serializePersistedState(state: Partial<State>): Partial<State> {
75
- return options.serialize?.(state) ?? state;
97
+ protected override serializePersistedState(state: Partial<State>): ServiceStorage {
98
+ return options.serialize?.(state) ?? (state as ServiceStorage);
99
+ }
100
+
101
+ protected override deserializePersistedState(state: ServiceStorage): Partial<State> {
102
+ return options.restore?.(state) ?? (state as Partial<State>);
76
103
  }
77
104
 
78
- } as unknown as Constructor<UnrefServiceState<State>> &
79
- Constructor<ComputedState> &
80
- Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>>;
105
+ } as unknown as ServiceWithState<State, ComputedState, ServiceStorage>;
81
106
  }
82
107
 
83
108
  export default class Service<
84
109
  State extends ServiceState = DefaultServiceState,
85
110
  ComputedState extends ServiceState = {},
86
- ServiceStorage extends Partial<State> = Partial<State>
111
+ ServiceStorage = Partial<State>,
87
112
  > extends MagicObject {
88
113
 
89
114
  public static persist: string[] = [];
@@ -91,7 +116,8 @@ export default class Service<
91
116
  protected _name: string;
92
117
  private _booted: PromisedValue<void>;
93
118
  private _computedStateKeys: Set<keyof State>;
94
- private _store?: Store | false;
119
+ private _watchers: StateWatchers<Service, State>;
120
+ private _store: Store<string, State, ComputedState, {}> | false;
95
121
 
96
122
  constructor() {
97
123
  super();
@@ -101,6 +127,7 @@ export default class Service<
101
127
  this._name = this.getName() ?? new.target.name;
102
128
  this._booted = new PromisedValue();
103
129
  this._computedStateKeys = new Set(Object.keys(getters));
130
+ this._watchers = this.getStateWatchers();
104
131
  this._store =
105
132
  this.usesStore() &&
106
133
  defineServiceStore(this._name, {
@@ -115,6 +142,12 @@ export default class Service<
115
142
  return this._booted;
116
143
  }
117
144
 
145
+ public override static<T extends typeof Service>(): T;
146
+ public override static<T extends typeof Service, K extends keyof T>(property: K): T[K];
147
+ public override static<T extends typeof Service, K extends keyof T>(property?: K): T | T[K] {
148
+ return super.static<T, K>(property as K);
149
+ }
150
+
118
151
  public launch(): Promise<void> {
119
152
  const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
120
153
 
@@ -130,6 +163,10 @@ export default class Service<
130
163
  return this._booted;
131
164
  }
132
165
 
166
+ public hasPersistedState(): boolean {
167
+ return Storage.has(this._name);
168
+ }
169
+
133
170
  public hasState<P extends keyof State>(property: P): boolean {
134
171
  if (!this._store) {
135
172
  return false;
@@ -145,10 +182,10 @@ export default class Service<
145
182
  const store = this._store as any;
146
183
 
147
184
  if (property) {
148
- return store ? store[property] : undefined;
185
+ return store ? store[property] : (undefined as State[P]);
149
186
  }
150
187
 
151
- return store ? store : {};
188
+ return store ? store : ({} as State);
152
189
  }
153
190
 
154
191
  public setState<P extends keyof State>(property: P, value: State[P]): void;
@@ -158,16 +195,31 @@ export default class Service<
158
195
  return;
159
196
  }
160
197
 
161
- const state = (
162
- typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty
163
- ) as Partial<State>;
198
+ const update = typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty;
199
+ const old = objectOnly(this._store.$state as State, Object.keys(update));
164
200
 
165
- Object.assign(this._store.$state, state);
201
+ Object.assign(this._store.$state, update);
202
+ this.onStateUpdated(update as Partial<State>, old as Partial<State>);
203
+ }
204
+
205
+ public updatePersistedState<T extends keyof State>(key: T): void;
206
+ public updatePersistedState<T extends keyof State>(keys: T[]): void;
207
+ public updatePersistedState<T extends keyof State>(keyOrKeys: T | T[]): void {
208
+ if (!this._store) {
209
+ return;
210
+ }
166
211
 
167
- this.onStateUpdated(state);
212
+ const keys = arrayFrom(keyOrKeys) as Array<keyof State>;
213
+ const state = objectOnly(this._store.$state as State, keys);
214
+
215
+ if (isEmpty(state)) {
216
+ return;
217
+ }
218
+
219
+ this.onPersistentStateUpdated(state);
168
220
  }
169
221
 
170
- protected __get(property: string): unknown {
222
+ protected override __get(property: string): unknown {
171
223
  if (this.hasState(property)) {
172
224
  return this.getState(property);
173
225
  }
@@ -175,19 +227,29 @@ export default class Service<
175
227
  return super.__get(property);
176
228
  }
177
229
 
178
- protected __set(property: string, value: unknown): void {
230
+ protected override __set(property: string, value: unknown): void {
179
231
  this.setState({ [property]: value } as Partial<State>);
180
232
  }
181
233
 
182
- protected onStateUpdated(state: Partial<State>): void {
183
- // TODO fix this.static()
184
- const persist = (this.constructor as unknown as { persist: string[] }).persist;
185
- const persisted = objectOnly(state, persist);
234
+ protected onStateUpdated(update: Partial<State>, old: Partial<State>): void {
235
+ const persisted = objectOnly(update, this.static('persist'));
186
236
 
187
- if (isEmpty(persisted)) {
188
- return;
237
+ if (!isEmpty(persisted)) {
238
+ this.onPersistentStateUpdated(persisted as Partial<State>);
189
239
  }
190
240
 
241
+ for (const property in update) {
242
+ const watcher = this._watchers[property] as Nullable<(value: unknown, oldValue: unknown) => unknown>;
243
+
244
+ if (!watcher || update[property] === old[property]) {
245
+ continue;
246
+ }
247
+
248
+ watcher.call(this, update[property], old[property]);
249
+ }
250
+ }
251
+
252
+ protected onPersistentStateUpdated(persisted: Partial<State>): void {
191
253
  const storage = Storage.get<ServiceStorage>(this._name);
192
254
 
193
255
  if (!storage) {
@@ -216,34 +278,47 @@ export default class Service<
216
278
  return {} as ComputedStateDefinition<State, ComputedState>;
217
279
  }
218
280
 
219
- protected serializePersistedState(state: Partial<State>): Partial<State> {
220
- return state;
281
+ protected getStateWatchers(): StateWatchers<Service, State> {
282
+ return {};
283
+ }
284
+
285
+ protected serializePersistedState(state: Partial<State>): ServiceStorage {
286
+ return state as ServiceStorage;
287
+ }
288
+
289
+ protected deserializePersistedState(state: ServiceStorage): Partial<State> {
290
+ return state as Partial<State>;
221
291
  }
222
292
 
223
293
  protected async frameworkBoot(): Promise<void> {
224
- this.initializePersistedState();
294
+ this.restorePersistedState();
225
295
  }
226
296
 
227
297
  protected async boot(): Promise<void> {
228
298
  // Placeholder for overrides, don't place any functionality here.
229
299
  }
230
300
 
231
- protected initializePersistedState(): void {
232
- // TODO fix this.static()
233
- const persist = (this.constructor as unknown as { persist: string[] }).persist;
234
-
235
- if (!this.usesStore() || isEmpty(persist)) {
301
+ protected restorePersistedState(): void {
302
+ if (!this.usesStore() || isEmpty(this.static('persist'))) {
236
303
  return;
237
304
  }
238
305
 
239
306
  if (Storage.has(this._name)) {
240
307
  const persisted = Storage.require<ServiceStorage>(this._name);
241
- this.setState(persisted);
308
+ this.setState(this.deserializePersistedState(persisted));
242
309
 
243
310
  return;
244
311
  }
245
312
 
246
- Storage.set(this._name, objectOnly(this.getState(), persist));
313
+ Storage.set(this._name, objectOnly(this.getState(), this.static('persist')));
314
+ }
315
+
316
+ protected requireStore(): Store<string, State, ComputedState, {}> {
317
+ if (!this._store) {
318
+ return fail(`Failed getting '${this._name}' store`);
319
+ }
320
+
321
+ return this._store;
247
322
  }
248
323
 
249
324
  }
@@ -0,0 +1,20 @@
1
+ import { facade } from '@noeldemartin/utils';
2
+
3
+ import Events from '@aerogel/core/services/Events';
4
+ import Service from '@aerogel/core/services/Service';
5
+
6
+ export class StorageService extends Service {
7
+
8
+ public async purge(): Promise<void> {
9
+ await Events.emit('purge-storage');
10
+ }
11
+
12
+ }
13
+
14
+ export default facade(StorageService);
15
+
16
+ declare module '@aerogel/core/services/Events' {
17
+ export interface EventsPayload {
18
+ 'purge-storage': void;
19
+ }
20
+ }
@@ -1,22 +1,28 @@
1
1
  import type { App as VueApp } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
3
+ import { definePlugin } from '@aerogel/core/plugins';
4
+ import { isDevelopment, isTesting } from '@noeldemartin/utils';
4
5
 
5
6
  import App from './App';
7
+ import Cache from './Cache';
6
8
  import Events from './Events';
7
9
  import Service from './Service';
10
+ import Storage from './Storage';
8
11
  import { getPiniaStore } from './store';
9
12
 
10
13
  export * from './App';
14
+ export * from './Cache';
11
15
  export * from './Events';
12
16
  export * from './Service';
13
17
  export * from './store';
18
+ export * from './utils';
14
19
 
15
- export { App, Events, Service };
20
+ export { App, Cache, Events, Storage, Service };
16
21
 
17
22
  const defaultServices = {
18
23
  $app: App,
19
24
  $events: Events,
25
+ $storage: Storage,
20
26
  };
21
27
 
22
28
  export type DefaultServices = typeof defaultServices;
@@ -34,7 +40,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
34
40
 
35
41
  Object.assign(app.config.globalProperties, services);
36
42
 
37
- App.development && Object.assign(window, services);
43
+ if (isDevelopment() || isTesting()) {
44
+ Object.assign(globalThis, services);
45
+ }
38
46
  }
39
47
 
40
48
  export default definePlugin({
@@ -50,12 +58,12 @@ export default definePlugin({
50
58
  },
51
59
  });
52
60
 
53
- declare module '@/bootstrap/options' {
61
+ declare module '@aerogel/core/bootstrap/options' {
54
62
  export interface AerogelOptions {
55
63
  services?: Record<string, Service>;
56
64
  }
57
65
  }
58
66
 
59
- declare module '@vue/runtime-core' {
67
+ declare module 'vue' {
60
68
  interface ComponentCustomProperties extends Services {}
61
69
  }
@@ -0,0 +1,18 @@
1
+ import { objectOnly } from '@noeldemartin/utils';
2
+
3
+ export type Replace<
4
+ TOriginal extends Record<string, unknown>,
5
+ TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
6
+ > = {
7
+ [K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
8
+ };
9
+
10
+ export function replaceExisting<
11
+ TOriginal extends Record<string, unknown>,
12
+ TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
13
+ >(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
14
+ return {
15
+ ...original,
16
+ ...objectOnly(replacements, Object.keys(original)),
17
+ } as Replace<TOriginal, TReplacements>;
18
+ }
@@ -0,0 +1,26 @@
1
+ import { isTesting } from '@noeldemartin/utils';
2
+ import type { GetClosureArgs } from '@noeldemartin/utils';
3
+
4
+ import Events from '@aerogel/core/services/Events';
5
+ import { definePlugin } from '@aerogel/core/plugins';
6
+
7
+ export interface AerogelTestingRuntime {
8
+ on: (typeof Events)['on'];
9
+ }
10
+
11
+ export default definePlugin({
12
+ async install() {
13
+ if (!isTesting()) {
14
+ return;
15
+ }
16
+
17
+ globalThis.testingRuntime = {
18
+ on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
19
+ };
20
+ },
21
+ });
22
+
23
+ declare global {
24
+ // eslint-disable-next-line no-var
25
+ var testingRuntime: AerogelTestingRuntime | undefined;
26
+ }