redux-sparse-kit 0.0.0-stage → 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksandr Khizhnyi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,332 @@
1
- # Temporary Holding Version
1
+ # redux-sparse-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/redux-sparse-kit.svg?style=flat-square)](https://www.npmjs.com/package/redux-sparse-kit)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md)
5
+
6
+ Dynamic reducers and epics for large Redux stores: only mounted features run on dispatch.
7
+
8
+ Every action a Redux store dispatches goes through every reducer, and with
9
+ redux-observable every epic sees it too. In a large application most of that
10
+ work is spent on features that are not on screen. `redux-sparse-kit` keeps the
11
+ store _sparse_: a feature's reducer and epics are injected when the feature
12
+ mounts and evicted some time after it is gone, so the cost of a dispatch
13
+ follows what the user is looking at, not the size of the codebase.
14
+
15
+ - **Segments** - a reducer, its epics and the segments it depends on, injected
16
+ and evicted as one unit. Each part is optional.
17
+ - **Reference counting** - a segment stays while anything holds it; shared
18
+ segments and dependencies are never evicted from under a feature.
19
+ - **Idle eviction** - a released segment waits before it is evicted, so moving
20
+ back and forth between pages does not tear features down and set them up
21
+ again. Idle segments can keep running, pause their epics or freeze.
22
+ - **Ordered, atomic changes** - dependencies are injected before their
23
+ dependents and evicted after them; every batch of changes reaches the store
24
+ as a single action.
25
+ - **Typed scopes** - an epic that reads a segment its segment does not depend
26
+ on is a type error.
27
+ - **Scope checks** - in development, reads of a segment's state outside the
28
+ part of the tree that holds it are reported.
29
+ - **Works with what you have** - a plain store enhancer, usable with
30
+ `createStore` or Redux Toolkit's `configureStore`, with any root reducer.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install redux-sparse-kit
36
+ # or
37
+ yarn add redux-sparse-kit
38
+ ```
39
+
40
+ `redux` 5 is required. React bindings need `react` 18 or later and
41
+ `react-redux`; epics need `rxjs` 7 and an epic middleware such as
42
+ `redux-observable`. Types need TypeScript 5.4 or later.
43
+
44
+ ## Quick start
45
+
46
+ ### 1. Create the store
47
+
48
+ ```ts
49
+ import {
50
+ applyMiddleware,
51
+ combineReducers,
52
+ compose,
53
+ legacy_createStore as createStore,
54
+ } from 'redux';
55
+ import type { StoreEnhancer } from 'redux';
56
+ import { createEpicMiddleware } from 'redux-observable';
57
+ import type { SparseStoreExt } from 'redux-sparse-kit';
58
+ import { sparseEnhancer } from 'redux-sparse-kit';
59
+ import { createEpicRunner } from 'redux-sparse-kit/observable';
60
+
61
+ const epicMiddleware = createEpicMiddleware();
62
+
63
+ export const store = createStore(
64
+ // The static state: always present, owned by the root reducer as usual.
65
+ combineReducers({ session: sessionReducer, router: routerReducer }),
66
+ // `compose` does not keep the types enhancers add to the store.
67
+ compose(
68
+ applyMiddleware(epicMiddleware),
69
+ sparseEnhancer({ epicRunner: createEpicRunner(epicMiddleware) }),
70
+ ) as StoreEnhancer<SparseStoreExt>,
71
+ );
72
+
73
+ epicMiddleware.run(rootEpic);
74
+ ```
75
+
76
+ With Redux Toolkit, which keeps the type of `store.sparse` on its own:
77
+
78
+ ```ts
79
+ const store = configureStore({
80
+ reducer: { session: sessionReducer, router: routerReducer },
81
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(epicMiddleware),
82
+ enhancers: (getDefaultEnhancers) =>
83
+ getDefaultEnhancers().concat(sparseEnhancer({ epicRunner: createEpicRunner(epicMiddleware) })),
84
+ });
85
+ ```
86
+
87
+ ### 2. Define segments
88
+
89
+ ```ts
90
+ import { defineSegment } from 'redux-sparse-kit';
91
+
92
+ export const accountSegment = defineSegment({
93
+ key: 'account',
94
+ reducer: accountReducer,
95
+ });
96
+
97
+ export const ordersSegment = defineSegment({
98
+ key: 'orders',
99
+ reducer: ordersReducer,
100
+ deps: [accountSegment],
101
+ epics: [loadOrdersEpic],
102
+ });
103
+ ```
104
+
105
+ A segment owns the top-level state key it names. Its dependencies are present
106
+ for as long as it is.
107
+
108
+ ### 3. Hold them where they are used
109
+
110
+ ```tsx
111
+ import { SparseScope } from 'redux-sparse-kit/react';
112
+
113
+ export function OrdersPage() {
114
+ return (
115
+ <SparseScope segments={[ordersSegment]} fallback={<Spinner />}>
116
+ <OrdersTable />
117
+ </SparseScope>
118
+ );
119
+ }
120
+ ```
121
+
122
+ `OrdersTable` renders once `orders` and `account` are in the store, and reads
123
+ them with `useSelector` as usual. When `OrdersPage` unmounts, the segments go
124
+ idle and are evicted a minute later unless something holds them again.
125
+
126
+ ## Segments
127
+
128
+ ```ts
129
+ defineSegment({
130
+ key: 'orders', // the top-level state key the reducer owns
131
+ reducer: ordersReducer,
132
+ deps: [accountSegment], // present for as long as this segment is
133
+ epics: [loadOrdersEpic], // started after injection, stopped before eviction
134
+ idleTimeout: 5 * 60_000, // overrides the store's idle timeout
135
+ whileIdle: 'pause-epics', // overrides the store's idle behaviour
136
+ });
137
+ ```
138
+
139
+ Every part but the key is optional. A segment without a reducer owns no slice
140
+ of the state: it can run epics only, or only bundle the segments it depends on,
141
+ so that one scope holds a whole page.
142
+
143
+ ```ts
144
+ const pricePolling = defineSegment({ key: 'pricePolling', epics: [pollPricesEpic] });
145
+
146
+ const ordersPage = defineSegment({
147
+ key: 'ordersPage',
148
+ deps: [ordersSegment, pricesSegment, pricePolling],
149
+ });
150
+ ```
151
+
152
+ A dependency has to be defined before the segments that list it, so the
153
+ dependency graph cannot have cycles. Two different segments cannot use the same
154
+ key in one store at the same time.
155
+
156
+ ### Typing epics
157
+
158
+ The state a segment's epics may read is the static state, the segment's own
159
+ slice and the slices of its direct dependencies. An epic that declares a state
160
+ outside of that is rejected:
161
+
162
+ ```ts
163
+ import type { Epic } from 'redux-observable';
164
+ import type { StateOfSegments } from 'redux-sparse-kit';
165
+
166
+ type OrdersState = StateOfSegments<typeof accountSegment> & { orders: Orders };
167
+
168
+ const loadOrdersEpic: Epic<Action, Action, OrdersState> = (action$, state$) => /* ... */;
169
+
170
+ defineSegment({ key: 'orders', reducer: ordersReducer, deps: [accountSegment], epics: [loadOrdersEpic] }); // ok
171
+ defineSegment({ key: 'orders', reducer: ordersReducer, epics: [loadOrdersEpic] }); // error: reads `account`
172
+ ```
173
+
174
+ Epics typed with a `void`, `unknown` or `any` state do not read it and are
175
+ always accepted. Register the static state once to let epics read it too:
176
+
177
+ ```ts
178
+ declare module 'redux-sparse-kit' {
179
+ interface StaticState {
180
+ session: SessionState;
181
+ router: RouterState;
182
+ }
183
+ }
184
+ ```
185
+
186
+ `ScopeStateOf<typeof segment>` is the full state a segment may read.
187
+
188
+ ## Lifecycle
189
+
190
+ ```
191
+ acquire release (last holder)
192
+ absent ─────────────▶ active ◀──────────────────────────▶ idle
193
+ ▲ ▲ acquire (revive) │
194
+ │ └──────────────────────────────────┤
195
+ │ │ idle timeout,
196
+ └───────────────────────── evict ───────────────────────┘ maxIdle
197
+ ```
198
+
199
+ - **Injecting** dispatches `@@sparse/INJECT` with the keys of the slices the
200
+ call brings in, dependencies first, then starts the epics of the injected
201
+ segments in that order.
202
+ - **Releasing** the last hold makes a segment idle. Its dependencies stay held
203
+ by it.
204
+ - **Reviving** an idle segment costs nothing: its slice and epics are still
205
+ there.
206
+ - **Evicting** stops the epics of the evicted segments, dependents first, then
207
+ dispatches `@@sparse/EVICT`, which removes their slices.
208
+ - Segments without a reducer add and remove no slice: when a change involves
209
+ only such segments, no action is dispatched. Dependencies that
210
+ only the evicted segments held are evicted in the same action. Segments that
211
+ expire together are swept together, when the browser is idle.
212
+
213
+ Reducers outside the segments can handle `SparseActionTypes.EVICT` to drop
214
+ data that refers to an evicted segment. A preloaded slice for a segment that is
215
+ not present yet is kept, and the segment starts from it when injected.
216
+
217
+ ## Store options
218
+
219
+ ```ts
220
+ sparseEnhancer({
221
+ epicRunner, // required once a held segment has epics
222
+ idleTimeout: 60_000, // ms a released segment waits; 0 = evict now, Infinity = never
223
+ whileIdle: 'keep-running', // what an idle segment does
224
+ maxIdle: Infinity, // idle segments kept at most; the least recently used go first
225
+ pauseWhenHidden: true, // stop the idle clock while the page is hidden
226
+ });
227
+ ```
228
+
229
+ | `whileIdle` | Reducer | Epics | Use for |
230
+ | -------------- | ------- | ------- | ----------------------------------------------------- |
231
+ | `keep-running` | runs | run | data kept current by a stream, e.g. live quotes |
232
+ | `pause-epics` | runs | stopped | epics that hold subscriptions or poll; restart on use |
233
+ | `freeze` | skipped | stopped | state that is cheap to keep but costly to update |
234
+
235
+ A frozen segment resumes from the state it had when it went idle, so actions
236
+ dispatched in the meantime are lost to it.
237
+
238
+ ## The store API
239
+
240
+ `sparseEnhancer` adds `store.sparse`:
241
+
242
+ ```ts
243
+ const release = store.sparse.acquire(ordersSegment, pricesSegment);
244
+ release(); // idempotent
245
+
246
+ store.sparse.has(ordersSegment); // or has('orders')
247
+ store.sparse.stats(); // { active: ['orders', 'account'], idle: ['prices'] }
248
+
249
+ store.sparse.batch(() => {
250
+ // injections and evictions caused in here reach the store as one action each
251
+ });
252
+
253
+ const unsubscribe = store.sparse.subscribe((event) => {
254
+ // { type: 'inject' | 'evict', keys } | { type: 'idle' | 'revive', key }
255
+ });
256
+ ```
257
+
258
+ `acquire` is all or nothing: if any of the segments, or their dependencies,
259
+ conflicts with a different segment already using its key, it throws and holds
260
+ nothing.
261
+
262
+ ## React
263
+
264
+ ```ts
265
+ import { SparseScope, useSparseSegments, createSparseReact } from 'redux-sparse-kit/react';
266
+ ```
267
+
268
+ - `<SparseScope segments fallback>` holds the segments while mounted and
269
+ renders its children once they are present. Segments still present from an
270
+ earlier visit render straight away. Holding happens in a layout effect, so
271
+ the fallback never reaches the screen when the segments can be injected
272
+ right away.
273
+ - `useSparseSegments(segments)` holds the segments while the calling component
274
+ is mounted and returns whether they are present yet.
275
+ - A new array of the same segments on every render does not acquire them
276
+ again.
277
+
278
+ The bindings find the store in react-redux's context, so they work with any
279
+ react-redux 8.1 or later. Pass `createSparseReact({ context })` when the
280
+ `<Provider>` was given a custom context.
281
+
282
+ ### Scope checks
283
+
284
+ In development, `SparseScope` reports reads of a segment's slice made outside
285
+ every scope that holds it: such a read works only while some other part of the
286
+ tree happens to hold the segment, and breaks once it is evicted.
287
+
288
+ ```tsx
289
+ <Provider store={store}>
290
+ {/* reports reads below that no scope holds */}
291
+ <SparseScope segments={[]}>
292
+ <App />
293
+ </SparseScope>
294
+ </Provider>
295
+ ```
296
+
297
+ A scope makes its segments and their dependencies available to everything
298
+ below it, including nested scopes. Reads of static state are never reported.
299
+
300
+ By default each key is logged once per selector with `console.error`. Pass a
301
+ handler to collect them instead, for example to fail end-to-end tests:
302
+
303
+ ```ts
304
+ const { SparseScope } = createSparseReact({
305
+ onViolation: ({ key, selector, scope }) => violations.push({ key, selector, scope }),
306
+ checks: true, // default: on outside production builds
307
+ });
308
+ ```
309
+
310
+ How reads are observed depends on react-redux. When its context value carries
311
+ `stateAccessTracking: true`, react-redux reports the first read of each
312
+ top-level key by each selector through an `onStateAccess(key, selector)`
313
+ callback on the context value; scopes replace that callback for their subtree
314
+ and report the selector that made the read. Otherwise scopes pass down a
315
+ wrapper of the store whose state is a proxy that sees each top-level read; the
316
+ selector is then not known. Checks are compiled out of production builds that
317
+ replace `process.env.NODE_ENV`.
318
+
319
+ ## Caveats
320
+
321
+ - **Server rendering** - segments are injected on the client. On the server
322
+ `SparseScope` renders its fallback.
323
+ - **`connect`** - with `onStateAccess`, reads made by `connect` are not
324
+ reported: react-redux reports only the reads of `useSelector`. With the store
325
+ wrapper they are.
326
+ - **Reading the whole state** - with the store wrapper, a selector that spreads
327
+ or iterates the whole state reads every key and is reported for each
328
+ segment slice.
329
+
330
+ ## License
331
+
332
+ [MIT](LICENSE.md)
@@ -0,0 +1,2 @@
1
+ import { _ as StaticState, a as SparseEvent, b as isSegmentKey, c as SparseStoreExt, d as ScopeStateOf, f as Segment, g as StateOfSegments, h as SegmentOptions, i as SparseController, l as sparseEnhancer, m as SegmentEpicStateError, n as SparseAction, o as SparseOptions, p as SegmentEpic, r as SparseActionTypes, s as SparseStats, t as EpicRunner, u as AnySegment, v as WhileIdle, y as defineSegment } from "./sparseEnhancer-CIcdhh-P.mjs";
2
+ export { type AnySegment, type EpicRunner, type ScopeStateOf, type Segment, type SegmentEpic, type SegmentEpicStateError, type SegmentOptions, type SparseAction, SparseActionTypes, type SparseController, type SparseEvent, type SparseOptions, type SparseStats, type SparseStoreExt, type StateOfSegments, type StaticState, type WhileIdle, defineSegment, isSegmentKey, sparseEnhancer };
@@ -0,0 +1,2 @@
1
+ import { _ as StaticState, a as SparseEvent, b as isSegmentKey, c as SparseStoreExt, d as ScopeStateOf, f as Segment, g as StateOfSegments, h as SegmentOptions, i as SparseController, l as sparseEnhancer, m as SegmentEpicStateError, n as SparseAction, o as SparseOptions, p as SegmentEpic, r as SparseActionTypes, s as SparseStats, t as EpicRunner, u as AnySegment, v as WhileIdle, y as defineSegment } from "./sparseEnhancer-CIcdhh-P.js";
2
+ export { type AnySegment, type EpicRunner, type ScopeStateOf, type Segment, type SegmentEpic, type SegmentEpicStateError, type SegmentOptions, type SparseAction, SparseActionTypes, type SparseController, type SparseEvent, type SparseOptions, type SparseStats, type SparseStoreExt, type StateOfSegments, type StaticState, type WhileIdle, defineSegment, isSegmentKey, sparseEnhancer };
package/dist/index.js ADDED
@@ -0,0 +1,336 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_segment = require("./segment-Dqd6-ABS.js");
3
+ //#region src/sparseEnhancer.ts
4
+ /**
5
+ * Actions the sparse store dispatches. Reducers outside the segments can
6
+ * handle {@linkcode SparseActionTypes.EVICT} to drop data that refers to an
7
+ * evicted segment.
8
+ */
9
+ const SparseActionTypes = {
10
+ /** Segment slices with the keys in `payload.keys` were added to the state. */
11
+ INJECT: "@@sparse/INJECT",
12
+ /** Segment slices with the keys in `payload.keys` are removed by this action. */
13
+ EVICT: "@@sparse/EVICT"
14
+ };
15
+ const emptyKeys = /* @__PURE__ */ new Set();
16
+ const defaultIdleTimeout = 6e4;
17
+ const whileIdleValues = [
18
+ "keep-running",
19
+ "pause-epics",
20
+ "freeze"
21
+ ];
22
+ const maxTimerDelay = 2 ** 31 - 1;
23
+ const scheduleIdle = (callback) => {
24
+ if (typeof requestIdleCallback === "function") requestIdleCallback(callback, { timeout: 1e3 });
25
+ else setTimeout(callback, 0);
26
+ };
27
+ /**
28
+ * A store enhancer that lets segments be injected into and evicted from a
29
+ * running store. The root reducer passed to `createStore` keeps owning the
30
+ * static state; segment slices live next to it under their keys.
31
+ */
32
+ function sparseEnhancer(options = {}) {
33
+ const { epicRunner, idleTimeout = defaultIdleTimeout, whileIdle = "keep-running", maxIdle = Infinity, pauseWhenHidden = true } = options;
34
+ const isHidden = () => pauseWhenHidden && typeof document !== "undefined" && document.visibilityState === "hidden";
35
+ if (!(idleTimeout >= 0)) throw new RangeError("The idle timeout must be a non-negative number");
36
+ if (!(maxIdle >= 0)) throw new RangeError("maxIdle must be a non-negative number");
37
+ if (!whileIdleValues.includes(whileIdle)) throw new RangeError(`whileIdle must be one of ${whileIdleValues.join(", ")}`);
38
+ const enhancer = (createStore) => (rootReducer, preloadedState) => {
39
+ const entries = /* @__PURE__ */ new Map();
40
+ const idleEntries = /* @__PURE__ */ new Set();
41
+ let evictingKeys = emptyKeys;
42
+ const slices = /* @__PURE__ */ new Map();
43
+ const isDynamicKey = (key) => slices.has(key) || evictingKeys.has(key) || require_segment.isSegmentKey(key);
44
+ const wrapReducer = (root) => (state, action) => {
45
+ var _staticState;
46
+ const current = state !== null && state !== void 0 ? state : {};
47
+ let staticState;
48
+ let hasDynamicKeys = false;
49
+ for (const key in current) if (isDynamicKey(key)) {
50
+ hasDynamicKeys = true;
51
+ break;
52
+ }
53
+ if (hasDynamicKeys) {
54
+ staticState = {};
55
+ for (const key in current) if (!isDynamicKey(key)) staticState[key] = current[key];
56
+ }
57
+ const input = state === void 0 ? void 0 : (_staticState = staticState) !== null && _staticState !== void 0 ? _staticState : current;
58
+ const nextStatic = root(input, action);
59
+ let changed = nextStatic !== input || evictingKeys.size > 0;
60
+ const values = [];
61
+ for (const [key, entry] of slices) {
62
+ const previous = current[key];
63
+ const value = entry.frozen && key in current ? previous : entry.segment.reducer(previous, action);
64
+ values.push(value);
65
+ if (value !== previous || !(key in current)) changed = true;
66
+ }
67
+ if (!changed) return current;
68
+ const next = { ...nextStatic };
69
+ for (const key in current) if (!slices.has(key) && !evictingKeys.has(key) && require_segment.isSegmentKey(key)) next[key] = current[key];
70
+ let index = 0;
71
+ for (const key of slices.keys()) next[key] = values[index++];
72
+ return next;
73
+ };
74
+ const store = createStore(wrapReducer(rootReducer), preloadedState);
75
+ let batchDepth = 0;
76
+ const pendingInject = [];
77
+ const pendingEvict = [];
78
+ const listeners = /* @__PURE__ */ new Set();
79
+ const emit = (event) => {
80
+ for (const listener of listeners) listener(event);
81
+ };
82
+ const dispatchSparse = (type, keys) => {
83
+ const action = {
84
+ type,
85
+ payload: { keys }
86
+ };
87
+ store.dispatch(action);
88
+ };
89
+ const flush = () => {
90
+ if (batchDepth > 0) return;
91
+ if (pendingInject.length > 0) {
92
+ const injected = pendingInject.splice(0);
93
+ const keys = injected.map((entry) => entry.segment.key);
94
+ const sliceKeys = [];
95
+ for (const entry of injected) if (entry.segment.reducer) {
96
+ slices.set(entry.segment.key, entry);
97
+ sliceKeys.push(entry.segment.key);
98
+ }
99
+ if (sliceKeys.length > 0) dispatchSparse(SparseActionTypes.INJECT, sliceKeys);
100
+ for (const entry of injected) if (!entry.paused) startEpics(entry);
101
+ emit({
102
+ type: "inject",
103
+ keys
104
+ });
105
+ }
106
+ if (pendingEvict.length > 0) {
107
+ const evicted = pendingEvict.splice(0);
108
+ const keys = evicted.map((entry) => entry.segment.key);
109
+ for (const entry of evicted) stopEpics(entry);
110
+ const sliceKeys = keys.filter((key) => slices.delete(key));
111
+ if (sliceKeys.length > 0) {
112
+ evictingKeys = new Set(sliceKeys);
113
+ try {
114
+ dispatchSparse(SparseActionTypes.EVICT, sliceKeys);
115
+ } finally {
116
+ evictingKeys = emptyKeys;
117
+ }
118
+ }
119
+ emit({
120
+ type: "evict",
121
+ keys
122
+ });
123
+ }
124
+ };
125
+ const startEpics = (entry) => {
126
+ for (const epic of entry.segment.epics) entry.stops.push(epicRunner.run(epic, entry.segment));
127
+ };
128
+ const stopEpics = (entry) => {
129
+ for (const stop of entry.stops.splice(0)) stop();
130
+ };
131
+ const batch = (fn) => {
132
+ batchDepth++;
133
+ try {
134
+ return fn();
135
+ } finally {
136
+ batchDepth--;
137
+ flush();
138
+ }
139
+ };
140
+ const assertCompatible = (segment, seen) => {
141
+ var _seen$get, _entries$get;
142
+ const other = (_seen$get = seen.get(segment.key)) !== null && _seen$get !== void 0 ? _seen$get : (_entries$get = entries.get(segment.key)) === null || _entries$get === void 0 ? void 0 : _entries$get.segment;
143
+ if (other === segment) return;
144
+ if (other !== void 0) throw new Error(`Two different segments use the key "${segment.key}"`);
145
+ if (segment.epics.length > 0 && epicRunner === void 0) throw new Error(`Segment "${segment.key}" has epics, but sparseEnhancer was created without an epicRunner`);
146
+ seen.set(segment.key, segment);
147
+ for (const dep of segment.deps) assertCompatible(dep, seen);
148
+ };
149
+ const hold = (segment) => {
150
+ const existing = entries.get(segment.key);
151
+ if (existing) {
152
+ if (existing.holders === 0) revive(existing);
153
+ existing.holders++;
154
+ return;
155
+ }
156
+ const evictIndex = pendingEvict.findIndex((entry) => entry.segment === segment);
157
+ if (evictIndex !== -1) {
158
+ const [entry] = pendingEvict.splice(evictIndex, 1);
159
+ entry.holders = 1;
160
+ entries.set(segment.key, entry);
161
+ for (const dep of segment.deps) hold(dep);
162
+ return;
163
+ }
164
+ const entry = {
165
+ segment,
166
+ holders: 1,
167
+ stops: [],
168
+ timer: void 0,
169
+ remaining: Infinity,
170
+ startedAt: 0,
171
+ expired: false,
172
+ paused: false,
173
+ frozen: false
174
+ };
175
+ entries.set(segment.key, entry);
176
+ for (const dep of segment.deps) hold(dep);
177
+ pendingInject.push(entry);
178
+ };
179
+ /**
180
+ * Drops a hold. A segment left without holders goes idle, unless it was
181
+ * only held by a segment being evicted: then it is evicted with it.
182
+ */
183
+ const unhold = (segment, cascade = false) => {
184
+ var _segment$idleTimeout;
185
+ const entry = entries.get(segment.key);
186
+ entry.holders--;
187
+ if (entry.holders > 0) return;
188
+ const timeout = (_segment$idleTimeout = segment.idleTimeout) !== null && _segment$idleTimeout !== void 0 ? _segment$idleTimeout : idleTimeout;
189
+ if (cascade || timeout === 0) evict(entry);
190
+ else becomeIdle(entry, timeout);
191
+ };
192
+ const becomeIdle = (entry, timeout) => {
193
+ var _entry$segment$whileI;
194
+ const policy = (_entry$segment$whileI = entry.segment.whileIdle) !== null && _entry$segment$whileI !== void 0 ? _entry$segment$whileI : whileIdle;
195
+ if (policy !== "keep-running") {
196
+ entry.paused = true;
197
+ entry.frozen = policy === "freeze";
198
+ stopEpics(entry);
199
+ }
200
+ entry.remaining = timeout > maxTimerDelay ? Infinity : timeout;
201
+ if (!isHidden()) startTimer(entry);
202
+ idleEntries.add(entry);
203
+ watchVisibility();
204
+ emit({
205
+ type: "idle",
206
+ key: entry.segment.key
207
+ });
208
+ while (idleEntries.size > maxIdle) evict(idleEntries.values().next().value);
209
+ };
210
+ const startTimer = (entry) => {
211
+ if (entry.remaining !== Infinity) {
212
+ entry.startedAt = Date.now();
213
+ entry.timer = setTimeout(() => {
214
+ entry.timer = void 0;
215
+ entry.expired = true;
216
+ scheduleSweep();
217
+ }, entry.remaining);
218
+ }
219
+ };
220
+ const pauseTimers = () => {
221
+ for (const entry of idleEntries) if (entry.timer !== void 0) {
222
+ clearTimeout(entry.timer);
223
+ entry.timer = void 0;
224
+ entry.remaining = Math.max(0, entry.remaining - (Date.now() - entry.startedAt));
225
+ }
226
+ };
227
+ const resumeTimers = () => {
228
+ for (const entry of idleEntries) if (entry.timer === void 0 && !entry.expired) startTimer(entry);
229
+ };
230
+ const onVisibilityChange = () => {
231
+ if (isHidden()) pauseTimers();
232
+ else resumeTimers();
233
+ };
234
+ let watchingVisibility = false;
235
+ /** Listens to the page visibility only while there are idle segments. */
236
+ const watchVisibility = () => {
237
+ const watch = idleEntries.size > 0;
238
+ if (watch !== watchingVisibility && pauseWhenHidden && typeof document !== "undefined") {
239
+ watchingVisibility = watch;
240
+ if (watch) document.addEventListener("visibilitychange", onVisibilityChange);
241
+ else document.removeEventListener("visibilitychange", onVisibilityChange);
242
+ }
243
+ };
244
+ const revive = (entry) => {
245
+ idleEntries.delete(entry);
246
+ watchVisibility();
247
+ clearTimeout(entry.timer);
248
+ entry.timer = void 0;
249
+ entry.expired = false;
250
+ entry.frozen = false;
251
+ if (entry.paused) {
252
+ entry.paused = false;
253
+ if (!pendingInject.includes(entry)) startEpics(entry);
254
+ }
255
+ emit({
256
+ type: "revive",
257
+ key: entry.segment.key
258
+ });
259
+ };
260
+ const evict = (entry) => {
261
+ idleEntries.delete(entry);
262
+ watchVisibility();
263
+ clearTimeout(entry.timer);
264
+ entries.delete(entry.segment.key);
265
+ const pendingIndex = pendingInject.indexOf(entry);
266
+ if (pendingIndex !== -1) pendingInject.splice(pendingIndex, 1);
267
+ else pendingEvict.push(entry);
268
+ for (const dep of entry.segment.deps) unhold(dep, true);
269
+ };
270
+ let sweepScheduled = false;
271
+ /** Evicts every expired segment with a single action. */
272
+ const sweep = () => {
273
+ sweepScheduled = false;
274
+ batch(() => {
275
+ for (const entry of entries.values()) if (entry.expired) evict(entry);
276
+ });
277
+ };
278
+ const scheduleSweep = () => {
279
+ if (!sweepScheduled) {
280
+ sweepScheduled = true;
281
+ scheduleIdle(sweep);
282
+ }
283
+ };
284
+ const sparse = {
285
+ acquire(...segments) {
286
+ const seen = /* @__PURE__ */ new Map();
287
+ for (const segment of segments) assertCompatible(segment, seen);
288
+ batch(() => {
289
+ for (const segment of segments) hold(segment);
290
+ });
291
+ let released = false;
292
+ return () => {
293
+ if (released) return;
294
+ released = true;
295
+ batch(() => {
296
+ for (const segment of segments) unhold(segment);
297
+ });
298
+ };
299
+ },
300
+ has(segmentOrKey) {
301
+ const key = typeof segmentOrKey === "string" ? segmentOrKey : segmentOrKey.key;
302
+ return entries.has(key) && !pendingInject.includes(entries.get(key));
303
+ },
304
+ stats() {
305
+ const stats = {
306
+ active: [],
307
+ idle: []
308
+ };
309
+ for (const [key, entry] of entries) if (!pendingInject.includes(entry)) (entry.holders > 0 ? stats.active : stats.idle).push(key);
310
+ return stats;
311
+ },
312
+ batch,
313
+ subscribe(listener) {
314
+ listeners.add(listener);
315
+ return () => {
316
+ listeners.delete(listener);
317
+ };
318
+ }
319
+ };
320
+ return {
321
+ ...store,
322
+ replaceReducer(nextReducer) {
323
+ store.replaceReducer(wrapReducer(nextReducer));
324
+ },
325
+ sparse
326
+ };
327
+ };
328
+ return enhancer;
329
+ }
330
+ //#endregion
331
+ exports.SparseActionTypes = SparseActionTypes;
332
+ exports.defineSegment = require_segment.defineSegment;
333
+ exports.isSegmentKey = require_segment.isSegmentKey;
334
+ exports.sparseEnhancer = sparseEnhancer;
335
+
336
+ //# sourceMappingURL=index.js.map