@equinor/fusion-framework-react-app 12.0.4 → 13.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Change Log
2
2
 
3
+ ## 13.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b92698d: Add state management support to `@equinor/fusion-framework-react-app/state`:
8
+
9
+ - `enableAppState` - configures the app to use `@equinor/fusion-framework-module-state`, with
10
+ app-scoped storage key prefixing to prevent state collisions between apps.
11
+ - `useAppState` - a persistent, cross-component-synchronized alternative to `useState`, backed by
12
+ the state module's storage.
13
+ - `useStateSyncEvents` - subscribes to the app's `onStateSync.status`/`.change`/`.complete`/`.error`
14
+ events (dispatched when storage is configured for replication, e.g. `PouchDbSyncStorage`) and
15
+ returns the most recent events, oldest first, bounded to a given `limit`. Also re-exports
16
+ `StateSyncEvent` and `StateSyncEventType` from `@equinor/fusion-framework-module-state` for
17
+ convenience.
18
+
19
+ ```typescript
20
+ import { enableAppState } from "@equinor/fusion-framework-react-app/state";
21
+
22
+ export const configure = (configurator) => {
23
+ enableAppState(configurator);
24
+ };
25
+ ```
26
+
27
+ ```typescript
28
+ import {
29
+ useAppState,
30
+ useStateSyncEvents,
31
+ } from "@equinor/fusion-framework-react-app/state";
32
+
33
+ const [count, setCount] = useAppState("counter", { defaultValue: 0 });
34
+ const events = useStateSyncEvents(20);
35
+ ```
36
+
37
+ Requires the optional peer dependency `@equinor/fusion-framework-module-state`.
38
+
39
+ ### Patch Changes
40
+
41
+ - Updated dependencies [b92698d]
42
+ - Updated dependencies [1b9d026]
43
+ - Updated dependencies [1b9d026]
44
+ - Updated dependencies [0d6ef3a]
45
+ - Updated dependencies [020d9e5]
46
+ - Updated dependencies [0d9d876]
47
+ - Updated dependencies [05586e7]
48
+ - Updated dependencies [b92698d]
49
+ - Updated dependencies [b92698d]
50
+ - @equinor/fusion-framework-app@12.0.0
51
+ - @equinor/fusion-framework-module-navigation@7.0.7
52
+ - @equinor/fusion-framework-module-state@1.0.0
53
+ - @equinor/fusion-framework-module@6.1.2
54
+
55
+ ## 12.0.5
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies [de2b4fb]
60
+ - @equinor/fusion-framework-module-app@8.0.4
61
+ - @equinor/fusion-framework-module-http@8.0.5
62
+ - @equinor/fusion-framework-app@11.0.12
63
+
3
64
  ## 12.0.4
4
65
 
5
66
  ### Patch Changes
package/README.md CHANGED
@@ -106,6 +106,214 @@ const App = () => {
106
106
  };
107
107
  ```
108
108
 
109
+ ## State Management
110
+
111
+ [<img src="https://img.shields.io/github/package-json/v/equinor/fusion-framework?filename=packages%2Fmodules%2Fstate%2Fpackage.json&label=@equinor/fusion-framework-module-state&style=for-the-badge" />](https://github.com/equinor/fusion-framework/tree/main/packages/modules/state)
112
+
113
+ The Fusion Framework provides a powerful state management solution that enables persistent, cross-component state sharing with automatic synchronization. Unlike traditional React state that's lost on page refresh, this state persists across app sessions and stays synchronized between different components in real-time.
114
+
115
+ **Key Benefits:**
116
+ - 🔄 **Persistent State**: Survives page refreshes and app restarts
117
+ - 🔗 **Cross-Component Sync**: Share state between any components instantly
118
+ - ⚡ **Optimistic Updates**: Responsive UI with automatic rollback on errors
119
+ - 🛡️ **Type Safe**: Full TypeScript support with type inference
120
+ - 🎯 **Simple API**: Works like `useState` but with persistence
121
+
122
+ **Use Cases:**
123
+ - User preferences and settings
124
+ - Form data that should persist
125
+ - UI state like filters, sorting, or view modes
126
+ - Data that needs to be shared across multiple components
127
+ - Cache management for expensive operations
128
+
129
+ ### Installation
130
+
131
+ First, install the state module package:
132
+
133
+ ```sh
134
+ pnpm install @equinor/fusion-framework-module-state
135
+ ```
136
+
137
+ ### Setup
138
+
139
+ Enable the state module in your app configuration. This initializes the persistent storage and makes `useAppState` available throughout your application:
140
+
141
+ ```typescript
142
+ import { enableAppState } from '@equinor/fusion-framework-react-app/state';
143
+ export const configure: ModuleInitiator = (appConfigurator) => {
144
+ enableAppState(appConfigurator);
145
+ };
146
+ ```
147
+
148
+ > [!CAUTION]
149
+ > The state management module is a powerful tool, but it's important to know the potential pitfalls and limitations when using it in your application. The state management is global and can lead to unexpected behavior if not used carefully.
150
+ >
151
+ > __example 1:__ If you have multiple components that rely on the same state, updating the state in one component can cause re-renders in all components that use that state, potentially leading to performance issues.
152
+ >
153
+ > __example 2:__ The user has open multiple tabs of the application, and each tab is modifying the same state. This can lead to unexpected behavior, as changes made in one tab will be reflected to all tabs. _(like storing user preferences for selected columns)_
154
+
155
+ ### Basic Usage
156
+
157
+ Use `useAppState` just like React's `useState`, but with automatic persistence. The first parameter is a unique key, and the second is an options object with the default value:
158
+
159
+ ```typescript
160
+ import { useAppState } from '@equinor/fusion-framework-react-app/state';
161
+
162
+ const Counter = () => {
163
+ const [count, setCount] = useAppState('counter', { defaultValue: 0 });
164
+ return (
165
+ <div>
166
+ <span>Count: {count}</span>
167
+ <button onClick={() => setCount((prev) => (prev ?? 0) + 1)}>Increment</button>
168
+ </div>
169
+ );
170
+ };
171
+ ```
172
+
173
+ ### Cross-Component Synchronization
174
+
175
+ Multiple components can share the same state by using the same key. Changes in one component automatically update all others:
176
+
177
+ ```typescript
178
+ import { useAppState } from '@equinor/fusion-framework-react-app/state';
179
+
180
+ const Incrementer = () => {
181
+ const [count, setCount] = useAppState('counter', { defaultValue: 0 });
182
+ return (
183
+ <button onClick={() => setCount((prev) => (prev || 0) + 1)}>
184
+ Increment
185
+ </button>
186
+ );
187
+ };
188
+
189
+ const Display = () => {
190
+ const [count] = useAppState('counter', { defaultValue: 0 });
191
+ return <span>Current count: {count}</span>;
192
+ };
193
+
194
+ // Usage in your app
195
+ const App = () => (
196
+ <div>
197
+ <Incrementer />
198
+ <Display />
199
+ </div>
200
+ );
201
+ ```
202
+
203
+ ### Advanced Usage
204
+
205
+ **Complex Objects with TypeScript:**
206
+ ```typescript
207
+ interface UserPreferences {
208
+ theme: 'light' | 'dark';
209
+ language: string;
210
+ notifications: boolean;
211
+ }
212
+
213
+ const SettingsPanel = () => {
214
+ const [settings, setSettings] = useAppState<UserPreferences>('user-settings', {
215
+ defaultValue: { theme: 'light', language: 'en', notifications: true }
216
+ });
217
+
218
+ const toggleTheme = () => {
219
+ setSettings(prev => ({
220
+ ...prev!,
221
+ theme: prev!.theme === 'light' ? 'dark' : 'light'
222
+ }));
223
+ };
224
+
225
+ return <button onClick={toggleTheme}>Theme: {settings?.theme}</button>;
226
+ };
227
+ ```
228
+
229
+ **Clearing State:**
230
+ ```typescript
231
+ // Remove from storage completely
232
+ const clearSettings = () => setSettings(undefined);
233
+ ```
234
+
235
+ ### Best Practices
236
+
237
+ #### Avoid Stale Closures
238
+ > [!WARNING]
239
+ > When updating state based on the current value, always use the updater function to prevent stale closure issues in concurrent updates.
240
+
241
+ ```typescript
242
+ const [count, setCount] = useAppState('counter', { defaultValue: 0 });
243
+
244
+ // ❌ Bad: Can use stale value in rapid updates
245
+ const increment = () => setCount(count + 1);
246
+
247
+ // ✅ Good: Always gets the latest value
248
+ const increment = () => setCount(prev => (prev || 0) + 1);
249
+ ```
250
+
251
+ #### State Key Organization
252
+
253
+ Use hierarchical naming for better organization:
254
+
255
+ ```typescript
256
+ // ✅ Good - hierarchical, descriptive
257
+ 'user.profile.personal'
258
+ 'user.preferences.theme'
259
+ 'app.settings.notifications'
260
+ 'feature.dashboard.filters'
261
+
262
+ // ❌ Avoid - flat, unclear
263
+ 'userdata'
264
+ 'settings'
265
+ 'stuff'
266
+ ```
267
+
268
+ #### Use strong typing
269
+
270
+ ```typescript
271
+ // ✅ Good - strong typing
272
+ interface UserProfile {
273
+ id: string;
274
+ name: string;
275
+ email: string;
276
+ }
277
+
278
+ const [user, setUser] = useAppState<UserProfile>('user.profile');
279
+
280
+ // ❌ Avoid - weak typing
281
+ const [user, setUser] = useAppState('user.profile');
282
+ ```
283
+
284
+ > [!TIP] Validate Complex Schemas
285
+ > Use a library like `zod` or `yup` to validate complex state schemas before using them.
286
+ > ```typescript
287
+ > const userSchema = z.object({
288
+ > id: z.string().uuid(),
289
+ > name: z.string().min(2).max(100),
290
+ > email: z.string().email(),
291
+ > });
292
+ >
293
+ > type UserProfile = z.infer<typeof userSchema>;
294
+ >
295
+ > // ✅ Good - strong typing with validation
296
+ > const useMyUser = () => {
297
+ > const [value, setValue] = useAppState<UserProfile>('user.profile');
298
+ > const setUser = useCallback((user: UserProfile) => {
299
+ > if (userSchema.safeParse(user).success) {
300
+ > setValue(user);
301
+ > return true;
302
+ > } else {
303
+ > console.warn('Provided user is invalid');
304
+ > return false;
305
+ > }
306
+ > }, [setValue]);
307
+ > if(!userSchema.safeParse(value).success) {
308
+ > console.warn('Current user state is invalid');
309
+ > return null;
310
+ > }
311
+ > return value;
312
+ > };
313
+ > ```
314
+
315
+ ## Feature Flag
316
+
109
317
  ### Feature flags
110
318
 
111
319
  > **Note:** Requires `@equinor/fusion-framework-module-feature-flag`.
@@ -0,0 +1,5 @@
1
+ export { StateSyncEvent, } from '@equinor/fusion-framework-module-state';
2
+ export { enableState as enableAppState } from '@equinor/fusion-framework-app/enable-state';
3
+ export { useAppState } from './useAppState';
4
+ export { useStateSyncEvents } from './useStateSyncEvents';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,cAAc,GAEf,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EAAE,WAAW,IAAI,cAAc,EAAE,MAAM,4CAA4C,CAAC;AAE3F,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,247 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState, useSyncExternalStore, } from 'react';
2
+ import { BehaviorSubject, from } from 'rxjs';
3
+ import { map, skip } from 'rxjs/operators';
4
+ import useAppModule from '../useAppModule';
5
+ /**
6
+ * A React hook for managing persistent application state through the Fusion Framework.
7
+ *
8
+ * This hook provides a simple way to store and retrieve values that persist across
9
+ * app sessions and are shared between different parts of your application. It works
10
+ * similarly to `useState` but with automatic persistence and cross-component synchronization.
11
+ *
12
+ * **Key Features:**
13
+ * - Automatic persistence across app sessions
14
+ * - Real-time synchronization between components
15
+ * - Optimistic updates for responsive UX
16
+ * - Deep equality checking to prevent unnecessary re-renders
17
+ * - TypeScript support with type safety
18
+ * - Default value support
19
+ *
20
+ * **Important Notes:**
21
+ * - Setting a value to `undefined` will completely remove it from storage
22
+ * - Values must be serializable (JSON-compatible)
23
+ * - Use unique keys to avoid conflicts between different state items
24
+ * - **The key parameter must remain constant across re-renders** - changing the key after the hook
25
+ * is initialized will be ignored and a warning will be logged in development mode
26
+ *
27
+ * @template T The type of value to store. Must be serializable (string, number, boolean, object, array, etc.)
28
+ * @param key Unique identifier for this state item. Use descriptive names like 'user.preferences' or 'dashboard.filters'. **Must remain constant across re-renders.**
29
+ * @param options Configuration options including default value
30
+ * @returns A tuple containing [currentValue, setValue] similar to useState
31
+ *
32
+ * @example
33
+ * **Basic Usage:**
34
+ * ```tsx
35
+ * function UserProfile() {
36
+ * // Simple string state with default value
37
+ * const [userName, setUserName] = useAppState('user.name', {
38
+ * defaultValue: 'Anonymous'
39
+ * });
40
+ *
41
+ * return (
42
+ * <input
43
+ * value={userName || ''}
44
+ * onChange={(e) => setUserName(e.target.value)}
45
+ * />
46
+ * );
47
+ * }
48
+ * ```
49
+ *
50
+ * @example
51
+ * **Object State:**
52
+ * ```tsx
53
+ * interface UserSettings {
54
+ * theme: 'light' | 'dark';
55
+ * language: string;
56
+ * notifications: boolean;
57
+ * }
58
+ *
59
+ * function SettingsPanel() {
60
+ * const [settings, setSettings] = useAppState<UserSettings>('user.settings', {
61
+ * defaultValue: { theme: 'light', language: 'en', notifications: true }
62
+ * });
63
+ *
64
+ * const toggleTheme = () => {
65
+ * setSettings(prev => ({
66
+ * ...prev!,
67
+ * theme: prev!.theme === 'light' ? 'dark' : 'light'
68
+ * }));
69
+ * };
70
+ *
71
+ * return <button onClick={toggleTheme}>Theme: {settings?.theme}</button>;
72
+ * }
73
+ * ```
74
+ *
75
+ * @example
76
+ * **Array State:**
77
+ * ```tsx
78
+ * function TaskList() {
79
+ * const [tasks, setTasks] = useAppState<string[]>('tasks', { defaultValue: [] });
80
+ *
81
+ * const addTask = (text: string) => {
82
+ * setTasks(prev => [...(prev || []), text]);
83
+ * };
84
+ *
85
+ * const removeTask = (index: number) => {
86
+ * setTasks(prev => prev?.filter((_, i) => i !== index));
87
+ * };
88
+ *
89
+ * return (
90
+ * <ul>
91
+ * {tasks?.map((task, index) => (
92
+ * <li key={index} onClick={() => removeTask(index)}>
93
+ * {task}
94
+ * </li>
95
+ * ))}
96
+ * </ul>
97
+ * );
98
+ * }
99
+ * ```
100
+ *
101
+ * @example
102
+ * **Clearing State:**
103
+ * ```tsx
104
+ * function DataManager() {
105
+ * const [data, setData] = useAppState<unknown[]>('cache.data');
106
+ *
107
+ * const clearCache = () => {
108
+ * // Setting to undefined removes the item from storage completely
109
+ * setData(undefined);
110
+ * };
111
+ *
112
+ * return <button onClick={clearCache}>Clear Cache</button>;
113
+ * }
114
+ * ```
115
+ *
116
+ * @example
117
+ * **Cross-Component Synchronization:**
118
+ * ```tsx
119
+ * // Component A
120
+ * function ComponentA() {
121
+ * const [counter, setCounter] = useAppState('shared.counter', { defaultValue: 0 });
122
+ * return <button onClick={() => setCounter(c => (c || 0) + 1)}>Count: {counter}</button>;
123
+ * }
124
+ *
125
+ * // Component B (automatically stays in sync)
126
+ * function ComponentB() {
127
+ * const [counter] = useAppState('shared.counter', { defaultValue: 0 });
128
+ * return <div>Current count: {counter}</div>;
129
+ * }
130
+ * ```
131
+ *
132
+ * @example
133
+ * **Key Stability - DO and DON'T:**
134
+ * ```tsx
135
+ * function MyComponent({ userId }: { userId: string }) {
136
+ * // ❌ DON'T: Key changes with prop, will cause warnings and use initial key
137
+ * const [userPrefs] = useAppState(`user.${userId}.preferences`);
138
+ *
139
+ * // ✅ DO: Use a constant key
140
+ * const [globalSettings] = useAppState('app.global.settings');
141
+ *
142
+ * return <div>...</div>;
143
+ * }
144
+ * ```
145
+ *
146
+ * @since 6.3.0
147
+ */
148
+ export const useAppState = (key, options) => {
149
+ // Restrict development-only key validation to avoid production overhead.
150
+ if (process.env.NODE_ENV === 'development') {
151
+ // Warn early when callers provide a key that cannot identify persisted state.
152
+ if (!key || typeof key !== 'string') {
153
+ console.warn('useAppState: key must be a non-empty string');
154
+ }
155
+ }
156
+ // Capture the initial key value and ensure it never changes
157
+ const initialKey = useRef(key);
158
+ // Warn about key changes only in development because the hook intentionally keeps its initial key.
159
+ if (process.env.NODE_ENV === 'development') {
160
+ // Surface an unstable key while preserving the original storage identity.
161
+ if (initialKey.current !== key) {
162
+ console.warn(`useAppState: key changed from "${initialKey.current}" to "${key}". The key should remain constant across re-renders. Using initial key: "${initialKey.current}"`);
163
+ }
164
+ }
165
+ // Access the state module from the Fusion Framework's dependency injection system
166
+ const stateProvider = useAppModule('state');
167
+ // BehaviorSubject bridges async state provider with React's sync rendering.
168
+ // Provides immediate access via value$.value and replay semantics for new subscribers.
169
+ // Initialize with defaultValue for consistent SSR/client hydration.
170
+ const [value$] = useState(() => {
171
+ return new BehaviorSubject(options?.defaultValue);
172
+ });
173
+ // useLayoutEffect runs synchronously after DOM mutations but before paint,
174
+ // preventing visual inconsistencies during hydration and ensuring state sync before updates.
175
+ useLayoutEffect(() => {
176
+ const subscription = from(stateProvider.observeItem(initialKey.current, { initialValue: value$.value })).subscribe({
177
+ next: (item) => {
178
+ // Convert state provider's null to undefined for React conventions
179
+ value$.next(item === null ? undefined : item.value);
180
+ },
181
+ error: (err) => {
182
+ // Log errors for debugging but don't crash the component
183
+ console.error(`State observation error for key "${initialKey.current}":`, err);
184
+ },
185
+ complete: () => {
186
+ // Complete the local stream when the source completes
187
+ value$.complete();
188
+ },
189
+ });
190
+ // Critical: Always cleanup subscriptions to prevent memory leaks
191
+ return () => {
192
+ subscription.unsubscribe();
193
+ };
194
+ }, [stateProvider, value$]);
195
+ // Helper function to get the current value, falling back to the default if necessary
196
+ const getValue = useCallback((rawValue) => (rawValue === undefined ? options?.defaultValue : rawValue), [options?.defaultValue]);
197
+ // useSyncExternalStore integrates with React 18's concurrent features,
198
+ // ensuring consistent state during concurrent rendering and preventing tearing.
199
+ const value = useSyncExternalStore((callback) => {
200
+ const subscription = value$
201
+ // Transform state emissions into the snapshot updates expected by React.
202
+ .pipe(
203
+ // skip the initial value, since we don't want to emit anything before the app state provider has initialized
204
+ skip(1),
205
+ // Apply default value logic consistently with snapshot function
206
+ map((value) => (value === undefined ? options?.defaultValue : value)))
207
+ .subscribe(callback);
208
+ return () => {
209
+ subscription.unsubscribe();
210
+ };
211
+ },
212
+ // Snapshot function: returns current value synchronously for React rendering
213
+ () => getValue(value$.value),
214
+ // Server snapshot: ensures consistent hydration between server and client
215
+ () => options?.defaultValue);
216
+ // Implements optimistic updates: update local state immediately, then persist.
217
+ // If persistence fails, the state provider will emit the old value, reverting the update.
218
+ const setValue = useCallback((action) => {
219
+ // Handle both direct values and updater functions (like React's useState).
220
+ // Apply the same defaulting as the snapshot so updaters never see a bare `undefined`
221
+ // when a `defaultValue` was provided.
222
+ const value = typeof action === 'function' ? action(getValue(value$.value)) : action;
223
+ // Remove undefined values from storage while updating subscribers immediately.
224
+ if (value === undefined) {
225
+ // undefined means "delete from storage" - update local state first for immediate UI feedback
226
+ value$.next(undefined);
227
+ stateProvider.removeItem(initialKey.current).catch((error) => {
228
+ console.error(`Failed to remove item "${initialKey.current}":`, error);
229
+ });
230
+ }
231
+ else {
232
+ // Optimistic update: local state first, then persist to storage
233
+ value$.next(value);
234
+ stateProvider
235
+ .storeItem({
236
+ key: initialKey.current,
237
+ value,
238
+ })
239
+ .catch((error) => {
240
+ console.error(`Failed to store item "${initialKey.current}":`, error);
241
+ });
242
+ }
243
+ }, [stateProvider, value$, getValue]);
244
+ return [value, setValue];
245
+ };
246
+ export default useAppState;
247
+ //# sourceMappingURL=useAppState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAppState.js","sourceRoot":"","sources":["../../../src/state/useAppState.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
2
+ import { BehaviorSubject } from 'rxjs';
3
+ import { filter } from 'rxjs/operators';
4
+ import { StateSyncEvent } from '@equinor/fusion-framework-module-state';
5
+ import useAppModule from '../useAppModule';
6
+ /**
7
+ * Subscribes to the app's `state` module sync events (`onStateSync.status`,
8
+ * `onStateSync.change`, `onStateSync.complete`, `onStateSync.error`) and returns the most
9
+ * recent `limit` events, oldest first.
10
+ *
11
+ * Events are only dispatched while the state module's storage is configured for replication
12
+ * (see `PouchDbSyncStorage`) - with the state module's default, local-only storage, this hook
13
+ * returns an empty array.
14
+ *
15
+ * @param limit - Maximum number of most-recent sync events to retain.
16
+ * @returns The most recent sync events, oldest first.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * const events = useStateSyncEvents(20);
21
+ * const lastEvent = events.at(-1);
22
+ * ```
23
+ *
24
+ * @since 12.1.0
25
+ */
26
+ export const useStateSyncEvents = (limit) => {
27
+ const eventProvider = useAppModule('event');
28
+ const [event$] = useState(() => new BehaviorSubject([]));
29
+ // Read through a ref so changing `limit` trims the log without resubscribing.
30
+ const limitRef = useRef(limit);
31
+ limitRef.current = limit;
32
+ useLayoutEffect(() => {
33
+ // Narrow the shared event stream down to the sync-related events this hook exposes.
34
+ const subscription = eventProvider.event$.pipe(filter(StateSyncEvent.is)).subscribe((event) => {
35
+ const next = [...event$.getValue(), event];
36
+ // Clamp to 0 so `slice(-0)` (a no-op, unlike `slice(-1)`) can't retain the whole log.
37
+ const limit = Math.max(0, limitRef.current);
38
+ event$.next(limit === 0 ? [] : next.length > limit ? next.slice(-limit) : next);
39
+ });
40
+ return () => subscription.unsubscribe();
41
+ }, [eventProvider, event$]);
42
+ return useSyncExternalStore((onChange) => {
43
+ const subscription = event$.subscribe(onChange);
44
+ return () => subscription.unsubscribe();
45
+ }, () => event$.getValue(), () => event$.getValue());
46
+ };
47
+ export default useStateSyncEvents;
48
+ //# sourceMappingURL=useStateSyncEvents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStateSyncEvents.js","sourceRoot":"","sources":["../../../src/state/useStateSyncEvents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,OAAO,EAAE,cAAc,EAA2B,MAAM,wCAAwC,CAAC;AAEjG,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,KAAa,EAAwB,EAAE;IACxE,MAAM,aAAa,GAAG,YAAY,CAAc,OAAO,CAAC,CAAC;IACzD,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,eAAe,CAAuB,EAAE,CAAC,CAAC,CAAC;IAE/E,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/B,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC;IAEzB,eAAe,CAAC,GAAG,EAAE;QACnB,oFAAoF;QACpF,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC5F,MAAM,IAAI,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC;YAC3C,sFAAsF;YACtF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IAC1C,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IAE5B,OAAO,oBAAoB,CACzB,CAAC,QAAQ,EAAE,EAAE;QACX,MAAM,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IAC1C,CAAC,EACD,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EACvB,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,CACxB,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -1,3 +1,3 @@
1
1
  // Generated by genversion.
2
- export const version = '12.0.4';
2
+ export const version = '13.0.0';
3
3
  //# sourceMappingURL=version.js.map