redux-sparse-kit 0.1.0 → 0.2.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/README.md +32 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -2
- package/dist/index.mjs.map +1 -1
- package/dist/observable.d.mts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/react.d.mts +37 -3
- package/dist/react.d.ts +37 -3
- package/dist/react.js +57 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +57 -3
- package/dist/react.mjs.map +1 -1
- package/dist/{sparseEnhancer-CIcdhh-P.d.ts → sparseEnhancer-BNdAfdGG.d.mts} +6 -1
- package/dist/{sparseEnhancer-CIcdhh-P.d.mts → sparseEnhancer-BNdAfdGG.d.ts} +6 -1
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -151,7 +151,8 @@ const ordersPage = defineSegment({
|
|
|
151
151
|
|
|
152
152
|
A dependency has to be defined before the segments that list it, so the
|
|
153
153
|
dependency graph cannot have cycles. Two different segments cannot use the same
|
|
154
|
-
key in one store at the same time
|
|
154
|
+
key in one store at the same time, and a segment with a reducer cannot use a key
|
|
155
|
+
of the static state: acquiring it throws.
|
|
155
156
|
|
|
156
157
|
### Typing epics
|
|
157
158
|
|
|
@@ -244,6 +245,7 @@ const release = store.sparse.acquire(ordersSegment, pricesSegment);
|
|
|
244
245
|
release(); // idempotent
|
|
245
246
|
|
|
246
247
|
store.sparse.has(ordersSegment); // or has('orders')
|
|
248
|
+
store.sparse.isStatic('session'); // true: the root reducer owns it
|
|
247
249
|
store.sparse.stats(); // { active: ['orders', 'account'], idle: ['prices'] }
|
|
248
250
|
|
|
249
251
|
store.sparse.batch(() => {
|
|
@@ -262,7 +264,13 @@ nothing.
|
|
|
262
264
|
## React
|
|
263
265
|
|
|
264
266
|
```ts
|
|
265
|
-
import {
|
|
267
|
+
import {
|
|
268
|
+
SparseScope,
|
|
269
|
+
useSparseSegments,
|
|
270
|
+
useInjectReducer,
|
|
271
|
+
useInjectEpic,
|
|
272
|
+
createSparseReact,
|
|
273
|
+
} from 'redux-sparse-kit/react';
|
|
266
274
|
```
|
|
267
275
|
|
|
268
276
|
- `<SparseScope segments fallback>` holds the segments while mounted and
|
|
@@ -279,6 +287,28 @@ The bindings find the store in react-redux's context, so they work with any
|
|
|
279
287
|
react-redux 8.1 or later. Pass `createSparseReact({ context })` when the
|
|
280
288
|
`<Provider>` was given a custom context.
|
|
281
289
|
|
|
290
|
+
### Injecting by key
|
|
291
|
+
|
|
292
|
+
Code that injects reducers by key and epics one by one, rather than through
|
|
293
|
+
defined segments, can use two hooks that make the segments for it:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
function OrdersPanel() {
|
|
297
|
+
const ready = useInjectReducer({ key: 'orders', reducer: ordersReducer });
|
|
298
|
+
useInjectEpic({ epic: loadOrdersEpic });
|
|
299
|
+
// ...
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- The first injection under a key makes its segment, with its reducer and
|
|
304
|
+
options; later ones share it. A different reducer under the same key is
|
|
305
|
+
ignored, with a warning in development.
|
|
306
|
+
- An epic runs once however many components inject it, and stops when the
|
|
307
|
+
last of them unmounts. `epic: undefined` injects nothing.
|
|
308
|
+
- Both take `idleTimeout` and `whileIdle` for the segment they make, for
|
|
309
|
+
example `idleTimeout: Infinity` to keep it for good once injected.
|
|
310
|
+
- A key of the static state is left to the root reducer.
|
|
311
|
+
|
|
282
312
|
### Scope checks
|
|
283
313
|
|
|
284
314
|
In development, `SparseScope` reports reads of a segment's slice made outside
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +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-
|
|
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-BNdAfdGG.mjs";
|
|
2
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.d.ts
CHANGED
|
@@ -1,2 +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-
|
|
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-BNdAfdGG.js";
|
|
2
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
CHANGED
|
@@ -14,6 +14,7 @@ const SparseActionTypes = {
|
|
|
14
14
|
};
|
|
15
15
|
const emptyKeys = /* @__PURE__ */ new Set();
|
|
16
16
|
const defaultIdleTimeout = 6e4;
|
|
17
|
+
const probeActionType = "@@sparse/PROBE";
|
|
17
18
|
const whileIdleValues = [
|
|
18
19
|
"keep-running",
|
|
19
20
|
"pause-epics",
|
|
@@ -40,7 +41,23 @@ function sparseEnhancer(options = {}) {
|
|
|
40
41
|
const idleEntries = /* @__PURE__ */ new Set();
|
|
41
42
|
let evictingKeys = emptyKeys;
|
|
42
43
|
const slices = /* @__PURE__ */ new Map();
|
|
43
|
-
|
|
44
|
+
let staticKeys = emptyKeys;
|
|
45
|
+
const learnStaticKeys = (staticState) => {
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (const key in staticState) {
|
|
48
|
+
count++;
|
|
49
|
+
if (!staticKeys.has(key)) {
|
|
50
|
+
staticKeys = new Set(Object.keys(staticState));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (count !== staticKeys.size) staticKeys = new Set(Object.keys(staticState));
|
|
55
|
+
};
|
|
56
|
+
/** Asks a root reducer for its keys before it reduces a real state. */
|
|
57
|
+
const probeStaticKeys = (root) => {
|
|
58
|
+
learnStaticKeys(root(void 0, { type: probeActionType }));
|
|
59
|
+
};
|
|
60
|
+
const isDynamicKey = (key) => !staticKeys.has(key) && (slices.has(key) || evictingKeys.has(key) || require_segment.isSegmentKey(key));
|
|
44
61
|
const wrapReducer = (root) => (state, action) => {
|
|
45
62
|
var _staticState;
|
|
46
63
|
const current = state !== null && state !== void 0 ? state : {};
|
|
@@ -50,12 +67,15 @@ function sparseEnhancer(options = {}) {
|
|
|
50
67
|
hasDynamicKeys = true;
|
|
51
68
|
break;
|
|
52
69
|
}
|
|
70
|
+
const pendingSlices = [];
|
|
53
71
|
if (hasDynamicKeys) {
|
|
54
72
|
staticState = {};
|
|
55
73
|
for (const key in current) if (!isDynamicKey(key)) staticState[key] = current[key];
|
|
74
|
+
else if (!slices.has(key) && !evictingKeys.has(key)) pendingSlices.push(key);
|
|
56
75
|
}
|
|
57
76
|
const input = state === void 0 ? void 0 : (_staticState = staticState) !== null && _staticState !== void 0 ? _staticState : current;
|
|
58
77
|
const nextStatic = root(input, action);
|
|
78
|
+
if (nextStatic !== input) learnStaticKeys(nextStatic);
|
|
59
79
|
let changed = nextStatic !== input || evictingKeys.size > 0;
|
|
60
80
|
const values = [];
|
|
61
81
|
for (const [key, entry] of slices) {
|
|
@@ -66,11 +86,12 @@ function sparseEnhancer(options = {}) {
|
|
|
66
86
|
}
|
|
67
87
|
if (!changed) return current;
|
|
68
88
|
const next = { ...nextStatic };
|
|
69
|
-
for (const key
|
|
89
|
+
for (const key of pendingSlices) next[key] = current[key];
|
|
70
90
|
let index = 0;
|
|
71
91
|
for (const key of slices.keys()) next[key] = values[index++];
|
|
72
92
|
return next;
|
|
73
93
|
};
|
|
94
|
+
probeStaticKeys(rootReducer);
|
|
74
95
|
const store = createStore(wrapReducer(rootReducer), preloadedState);
|
|
75
96
|
let batchDepth = 0;
|
|
76
97
|
const pendingInject = [];
|
|
@@ -142,6 +163,7 @@ function sparseEnhancer(options = {}) {
|
|
|
142
163
|
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
164
|
if (other === segment) return;
|
|
144
165
|
if (other !== void 0) throw new Error(`Two different segments use the key "${segment.key}"`);
|
|
166
|
+
if (segment.reducer && staticKeys.has(segment.key)) throw new Error(`Segment "${segment.key}" uses the key of a slice of the static state`);
|
|
145
167
|
if (segment.epics.length > 0 && epicRunner === void 0) throw new Error(`Segment "${segment.key}" has epics, but sparseEnhancer was created without an epicRunner`);
|
|
146
168
|
seen.set(segment.key, segment);
|
|
147
169
|
for (const dep of segment.deps) assertCompatible(dep, seen);
|
|
@@ -301,6 +323,9 @@ function sparseEnhancer(options = {}) {
|
|
|
301
323
|
const key = typeof segmentOrKey === "string" ? segmentOrKey : segmentOrKey.key;
|
|
302
324
|
return entries.has(key) && !pendingInject.includes(entries.get(key));
|
|
303
325
|
},
|
|
326
|
+
isStatic(key) {
|
|
327
|
+
return staticKeys.has(key);
|
|
328
|
+
},
|
|
304
329
|
stats() {
|
|
305
330
|
const stats = {
|
|
306
331
|
active: [],
|
|
@@ -320,6 +345,7 @@ function sparseEnhancer(options = {}) {
|
|
|
320
345
|
return {
|
|
321
346
|
...store,
|
|
322
347
|
replaceReducer(nextReducer) {
|
|
348
|
+
probeStaticKeys(nextReducer);
|
|
323
349
|
store.replaceReducer(wrapReducer(nextReducer));
|
|
324
350
|
},
|
|
325
351
|
sparse
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["isSegmentKey"],"sources":["../src/sparseEnhancer.ts"],"sourcesContent":["import type { Action, Reducer, StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';\nimport type { AnySegment, SegmentEpic, WhileIdle } from './segment';\nimport { isSegmentKey } from './segment';\n\n/**\n * Actions the sparse store dispatches. Reducers outside the segments can\n * handle {@linkcode SparseActionTypes.EVICT} to drop data that refers to an\n * evicted segment.\n */\nexport const SparseActionTypes = {\n /** Segment slices with the keys in `payload.keys` were added to the state. */\n INJECT: '@@sparse/INJECT',\n /** Segment slices with the keys in `payload.keys` are removed by this action. */\n EVICT: '@@sparse/EVICT',\n} as const;\n\nexport interface SparseAction extends Action<\n (typeof SparseActionTypes)[keyof typeof SparseActionTypes]\n> {\n payload: { keys: readonly string[] };\n}\n\nexport type SparseEvent =\n | { type: 'inject'; keys: readonly string[] }\n | { type: 'evict'; keys: readonly string[] }\n /** A segment is no longer held and waits for its idle timeout. */\n | { type: 'idle'; key: string }\n /** An idle segment is held again before it was evicted. */\n | { type: 'revive'; key: string };\n\nexport interface SparseStats {\n /** Keys of present segments that something holds. */\n active: string[];\n /** Keys of present segments waiting for their idle timeout. */\n idle: string[];\n}\n\nexport interface SparseController {\n /**\n * Holds the given segments, injecting the ones that are not present yet\n * together with their dependencies. Returns a function that releases the\n * hold; calling it more than once has no further effect.\n */\n acquire(...segments: readonly AnySegment[]): () => void;\n /** Whether a segment with this key is present in the store. */\n has(segmentOrKey: AnySegment | string): boolean;\n /**\n * Runs `fn`, deferring injections and evictions it causes until it returns,\n * so that they reach the store as a single action of each kind.\n */\n batch<T>(fn: () => T): T;\n /** Listens to segments being injected, going idle, revived and evicted. */\n subscribe(listener: (event: SparseEvent) => void): () => void;\n /** The keys of the present segments, by state. */\n stats(): SparseStats;\n}\n\nexport interface SparseStoreExt {\n readonly sparse: SparseController;\n}\n\n/**\n * Starts segment epics. `redux-sparse-kit/observable` provides one for\n * redux-observable.\n */\nexport interface EpicRunner {\n /** Starts `epic` and returns a function that stops it. */\n run(epic: SegmentEpic, segment: AnySegment): () => void;\n}\n\nexport interface SparseOptions {\n /** Required as soon as a held segment has epics. */\n epicRunner?: EpicRunner;\n /**\n * How long a segment nothing holds stays present before it is evicted, in\n * milliseconds. `0` evicts it right away, `Infinity` never. Segments can\n * override it. Defaults to one minute.\n */\n idleTimeout?: number;\n /**\n * What an idle segment does until it is evicted. Segments can override it.\n * Defaults to `keep-running`.\n */\n whileIdle?: WhileIdle;\n /**\n * How many idle segments may wait for their idle timeout at once. Past it,\n * the ones idle the longest are evicted right away. Defaults to `Infinity`.\n */\n maxIdle?: number;\n /**\n * Stops the idle clock while the page is hidden, so that time spent in a\n * background tab does not count towards the idle timeout. Defaults to `true`.\n */\n pauseWhenHidden?: boolean;\n}\n\ninterface Entry {\n readonly segment: AnySegment;\n /** Holds by `acquire` callers and by present segments that depend on it. */\n holders: number;\n /** Stops the segment's running epics. */\n stops: (() => void)[];\n /** Fires once the segment has been idle for its idle timeout. */\n timer: ReturnType<typeof setTimeout> | undefined;\n /** Idle time left before it expires; `Infinity` when it never does. */\n remaining: number;\n /** When the running timer was started. */\n startedAt: number;\n /** Idle for its whole idle timeout, so evicted by the next sweep. */\n expired: boolean;\n /** Idle with its epics stopped. */\n paused: boolean;\n /** Idle with its reducer skipped. */\n frozen: boolean;\n}\n\ntype AnyReducer = Reducer<any, Action, any>;\n\nconst emptyKeys: ReadonlySet<string> = new Set();\n\nconst defaultIdleTimeout = 60_000;\n\nconst whileIdleValues: readonly WhileIdle[] = ['keep-running', 'pause-epics', 'freeze'];\n\n// Longer delays overflow `setTimeout` and would fire right away.\nconst maxTimerDelay = 2 ** 31 - 1;\n\nconst scheduleIdle = (callback: () => void) => {\n if (typeof requestIdleCallback === 'function') {\n requestIdleCallback(callback, { timeout: 1000 });\n } else {\n setTimeout(callback, 0);\n }\n};\n\n/**\n * A store enhancer that lets segments be injected into and evicted from a\n * running store. The root reducer passed to `createStore` keeps owning the\n * static state; segment slices live next to it under their keys.\n */\nexport function sparseEnhancer(options: SparseOptions = {}): StoreEnhancer<SparseStoreExt> {\n const {\n epicRunner,\n idleTimeout = defaultIdleTimeout,\n whileIdle = 'keep-running',\n maxIdle = Infinity,\n pauseWhenHidden = true,\n } = options;\n\n const isHidden = () =>\n pauseWhenHidden && typeof document !== 'undefined' && document.visibilityState === 'hidden';\n\n if (!(idleTimeout >= 0)) {\n throw new RangeError('The idle timeout must be a non-negative number');\n }\n\n if (!(maxIdle >= 0)) {\n throw new RangeError('maxIdle must be a non-negative number');\n }\n\n if (!whileIdleValues.includes(whileIdle)) {\n throw new RangeError(`whileIdle must be one of ${whileIdleValues.join(', ')}`);\n }\n\n const enhancer =\n (createStore: StoreEnhancerStoreCreator) =>\n (rootReducer: AnyReducer, preloadedState?: unknown) => {\n const entries = new Map<string, Entry>();\n // Idle entries, the one idle the longest first.\n const idleEntries = new Set<Entry>();\n // Keys being removed by the EVICT action currently being reduced.\n let evictingKeys: ReadonlySet<string> = emptyKeys;\n\n // Injected segments that own a slice of the state, by key: only those\n // with a reducer. They keep reducing until the action that evicts them.\n const slices = new Map<string, Entry>();\n\n const isDynamicKey = (key: string) =>\n slices.has(key) || evictingKeys.has(key) || isSegmentKey(key);\n\n const wrapReducer =\n (root: AnyReducer): AnyReducer =>\n (state, action) => {\n const current = (state ?? {}) as Record<string, unknown>;\n\n let staticState: Record<string, unknown> | undefined;\n let hasDynamicKeys = false;\n\n for (const key in current) {\n if (isDynamicKey(key)) {\n hasDynamicKeys = true;\n break;\n }\n }\n\n if (hasDynamicKeys) {\n staticState = {};\n for (const key in current) {\n if (!isDynamicKey(key)) {\n staticState[key] = current[key];\n }\n }\n }\n\n const input = state === undefined ? undefined : (staticState ?? current);\n const nextStatic = root(input, action) as Record<string, unknown>;\n\n let changed = nextStatic !== input || evictingKeys.size > 0;\n const values: unknown[] = [];\n\n for (const [key, entry] of slices) {\n const previous = current[key];\n const value =\n entry.frozen && key in current ? previous : entry.segment.reducer!(previous, action);\n values.push(value);\n if (value !== previous || !(key in current)) {\n changed = true;\n }\n }\n\n if (!changed) {\n return current;\n }\n\n const next: Record<string, unknown> = { ...nextStatic };\n\n for (const key in current) {\n if (!slices.has(key) && !evictingKeys.has(key) && isSegmentKey(key)) {\n // A slice of a segment that is not present yet, e.g. from a\n // preloaded state: kept so the segment starts from it.\n next[key] = current[key];\n }\n }\n\n let index = 0;\n for (const key of slices.keys()) {\n next[key] = values[index++];\n }\n\n return next;\n };\n\n const store = createStore(wrapReducer(rootReducer), preloadedState);\n\n let batchDepth = 0;\n const pendingInject: Entry[] = [];\n const pendingEvict: Entry[] = [];\n const listeners = new Set<(event: SparseEvent) => void>();\n\n const emit = (event: SparseEvent) => {\n for (const listener of listeners) {\n listener(event);\n }\n };\n\n const dispatchSparse = (type: SparseAction['type'], keys: readonly string[]) => {\n const action: SparseAction = { type, payload: { keys } };\n store.dispatch(action);\n };\n\n const flush = () => {\n if (batchDepth > 0) {\n return;\n }\n\n if (pendingInject.length > 0) {\n const injected = pendingInject.splice(0);\n const keys = injected.map((entry) => entry.segment.key);\n const sliceKeys: string[] = [];\n for (const entry of injected) {\n if (entry.segment.reducer) {\n slices.set(entry.segment.key, entry);\n sliceKeys.push(entry.segment.key);\n }\n }\n if (sliceKeys.length > 0) {\n dispatchSparse(SparseActionTypes.INJECT, sliceKeys);\n }\n // Started once their slices are in the state, deps first.\n for (const entry of injected) {\n if (!entry.paused) {\n startEpics(entry);\n }\n }\n emit({ type: 'inject', keys });\n }\n\n if (pendingEvict.length > 0) {\n const evicted = pendingEvict.splice(0);\n const keys = evicted.map((entry) => entry.segment.key);\n // Stopped while their slices are still in the state, dependents first.\n for (const entry of evicted) {\n stopEpics(entry);\n }\n const sliceKeys = keys.filter((key) => slices.delete(key));\n if (sliceKeys.length > 0) {\n evictingKeys = new Set(sliceKeys);\n try {\n dispatchSparse(SparseActionTypes.EVICT, sliceKeys);\n } finally {\n evictingKeys = emptyKeys;\n }\n }\n emit({ type: 'evict', keys });\n }\n };\n\n const startEpics = (entry: Entry) => {\n for (const epic of entry.segment.epics) {\n entry.stops.push(epicRunner!.run(epic, entry.segment));\n }\n };\n\n const stopEpics = (entry: Entry) => {\n for (const stop of entry.stops.splice(0)) {\n stop();\n }\n };\n\n const batch = <T>(fn: () => T): T => {\n batchDepth++;\n try {\n return fn();\n } finally {\n batchDepth--;\n flush();\n }\n };\n\n const assertCompatible = (segment: AnySegment, seen: Map<string, AnySegment>) => {\n const other = seen.get(segment.key) ?? entries.get(segment.key)?.segment;\n\n if (other === segment) {\n return;\n }\n\n if (other !== undefined) {\n throw new Error(`Two different segments use the key \"${segment.key}\"`);\n }\n\n if (segment.epics.length > 0 && epicRunner === undefined) {\n throw new Error(\n `Segment \"${segment.key}\" has epics, but sparseEnhancer was created without an epicRunner`,\n );\n }\n\n seen.set(segment.key, segment);\n\n for (const dep of segment.deps) {\n assertCompatible(dep, seen);\n }\n };\n\n const hold = (segment: AnySegment) => {\n const existing = entries.get(segment.key);\n\n if (existing) {\n if (existing.holders === 0) {\n revive(existing);\n }\n existing.holders++;\n return;\n }\n\n const evictIndex = pendingEvict.findIndex((entry) => entry.segment === segment);\n\n if (evictIndex !== -1) {\n // Released and held again before the eviction reached the store:\n // its slice is still there, so it only takes its holds back.\n const [entry] = pendingEvict.splice(evictIndex, 1);\n entry!.holders = 1;\n entries.set(segment.key, entry!);\n for (const dep of segment.deps) {\n hold(dep);\n }\n return;\n }\n\n const entry: Entry = {\n segment,\n holders: 1,\n stops: [],\n timer: undefined,\n remaining: Infinity,\n startedAt: 0,\n expired: false,\n paused: false,\n frozen: false,\n };\n entries.set(segment.key, entry);\n\n // Dependencies first, so they are injected before their dependents.\n for (const dep of segment.deps) {\n hold(dep);\n }\n\n pendingInject.push(entry);\n };\n\n /**\n * Drops a hold. A segment left without holders goes idle, unless it was\n * only held by a segment being evicted: then it is evicted with it.\n */\n const unhold = (segment: AnySegment, cascade = false) => {\n // Only reached for segments this store holds: a release undoes exactly\n // the holds its acquire took.\n const entry = entries.get(segment.key)!;\n\n entry.holders--;\n\n if (entry.holders > 0) {\n return;\n }\n\n const timeout = segment.idleTimeout ?? idleTimeout;\n\n if (cascade || timeout === 0) {\n evict(entry);\n } else {\n becomeIdle(entry, timeout);\n }\n };\n\n const becomeIdle = (entry: Entry, timeout: number) => {\n const policy = entry.segment.whileIdle ?? whileIdle;\n\n if (policy !== 'keep-running') {\n entry.paused = true;\n entry.frozen = policy === 'freeze';\n stopEpics(entry);\n }\n\n entry.remaining = timeout > maxTimerDelay ? Infinity : timeout;\n if (!isHidden()) {\n startTimer(entry);\n }\n idleEntries.add(entry);\n watchVisibility();\n emit({ type: 'idle', key: entry.segment.key });\n\n while (idleEntries.size > maxIdle) {\n evict(idleEntries.values().next().value!);\n }\n };\n\n const startTimer = (entry: Entry) => {\n if (entry.remaining !== Infinity) {\n entry.startedAt = Date.now();\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.expired = true;\n scheduleSweep();\n }, entry.remaining);\n }\n };\n\n const pauseTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer !== undefined) {\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.remaining = Math.max(0, entry.remaining - (Date.now() - entry.startedAt));\n }\n }\n };\n\n const resumeTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer === undefined && !entry.expired) {\n startTimer(entry);\n }\n }\n };\n\n const onVisibilityChange = () => {\n if (isHidden()) {\n pauseTimers();\n } else {\n resumeTimers();\n }\n };\n\n let watchingVisibility = false;\n\n /** Listens to the page visibility only while there are idle segments. */\n const watchVisibility = () => {\n const watch = idleEntries.size > 0;\n\n if (watch !== watchingVisibility && pauseWhenHidden && typeof document !== 'undefined') {\n watchingVisibility = watch;\n if (watch) {\n document.addEventListener('visibilitychange', onVisibilityChange);\n } else {\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n }\n };\n\n const revive = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.expired = false;\n entry.frozen = false;\n\n if (entry.paused) {\n entry.paused = false;\n // Not injected yet: its epics start once it is.\n if (!pendingInject.includes(entry)) {\n startEpics(entry);\n }\n }\n\n emit({ type: 'revive', key: entry.segment.key });\n };\n\n const evict = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entries.delete(entry.segment.key);\n\n const pendingIndex = pendingInject.indexOf(entry);\n if (pendingIndex !== -1) {\n // Released before it reached the store: nothing to evict.\n pendingInject.splice(pendingIndex, 1);\n } else {\n pendingEvict.push(entry);\n }\n\n // Dependents first, so they are evicted before their dependencies.\n for (const dep of entry.segment.deps) {\n unhold(dep, true);\n }\n };\n\n let sweepScheduled = false;\n\n /** Evicts every expired segment with a single action. */\n const sweep = () => {\n sweepScheduled = false;\n batch(() => {\n for (const entry of entries.values()) {\n if (entry.expired) {\n evict(entry);\n }\n }\n });\n };\n\n const scheduleSweep = () => {\n if (!sweepScheduled) {\n sweepScheduled = true;\n scheduleIdle(sweep);\n }\n };\n\n const sparse: SparseController = {\n acquire(...segments) {\n // Checked up front so that a refused acquire leaves no holds behind.\n const seen = new Map<string, AnySegment>();\n for (const segment of segments) {\n assertCompatible(segment, seen);\n }\n\n batch(() => {\n for (const segment of segments) {\n hold(segment);\n }\n });\n\n let released = false;\n\n return () => {\n if (released) {\n return;\n }\n released = true;\n\n batch(() => {\n for (const segment of segments) {\n unhold(segment);\n }\n });\n };\n },\n\n has(segmentOrKey) {\n const key = typeof segmentOrKey === 'string' ? segmentOrKey : segmentOrKey.key;\n return entries.has(key) && !pendingInject.includes(entries.get(key)!);\n },\n\n stats() {\n const stats: SparseStats = { active: [], idle: [] };\n for (const [key, entry] of entries) {\n if (!pendingInject.includes(entry)) {\n (entry.holders > 0 ? stats.active : stats.idle).push(key);\n }\n }\n return stats;\n },\n\n batch,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n\n return {\n ...store,\n replaceReducer(nextReducer: AnyReducer) {\n store.replaceReducer(wrapReducer(nextReducer));\n },\n sparse,\n };\n };\n\n return enhancer as StoreEnhancer<SparseStoreExt>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["isSegmentKey"],"sources":["../src/sparseEnhancer.ts"],"sourcesContent":["import type { Action, Reducer, StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';\nimport type { AnySegment, SegmentEpic, WhileIdle } from './segment';\nimport { isSegmentKey } from './segment';\n\n/**\n * Actions the sparse store dispatches. Reducers outside the segments can\n * handle {@linkcode SparseActionTypes.EVICT} to drop data that refers to an\n * evicted segment.\n */\nexport const SparseActionTypes = {\n /** Segment slices with the keys in `payload.keys` were added to the state. */\n INJECT: '@@sparse/INJECT',\n /** Segment slices with the keys in `payload.keys` are removed by this action. */\n EVICT: '@@sparse/EVICT',\n} as const;\n\nexport interface SparseAction extends Action<\n (typeof SparseActionTypes)[keyof typeof SparseActionTypes]\n> {\n payload: { keys: readonly string[] };\n}\n\nexport type SparseEvent =\n | { type: 'inject'; keys: readonly string[] }\n | { type: 'evict'; keys: readonly string[] }\n /** A segment is no longer held and waits for its idle timeout. */\n | { type: 'idle'; key: string }\n /** An idle segment is held again before it was evicted. */\n | { type: 'revive'; key: string };\n\nexport interface SparseStats {\n /** Keys of present segments that something holds. */\n active: string[];\n /** Keys of present segments waiting for their idle timeout. */\n idle: string[];\n}\n\nexport interface SparseController {\n /**\n * Holds the given segments, injecting the ones that are not present yet\n * together with their dependencies. Returns a function that releases the\n * hold; calling it more than once has no further effect.\n */\n acquire(...segments: readonly AnySegment[]): () => void;\n /** Whether a segment with this key is present in the store. */\n has(segmentOrKey: AnySegment | string): boolean;\n /**\n * Whether the root reducer owns this key: a segment with a reducer cannot\n * be acquired under it.\n */\n isStatic(key: string): boolean;\n /**\n * Runs `fn`, deferring injections and evictions it causes until it returns,\n * so that they reach the store as a single action of each kind.\n */\n batch<T>(fn: () => T): T;\n /** Listens to segments being injected, going idle, revived and evicted. */\n subscribe(listener: (event: SparseEvent) => void): () => void;\n /** The keys of the present segments, by state. */\n stats(): SparseStats;\n}\n\nexport interface SparseStoreExt {\n readonly sparse: SparseController;\n}\n\n/**\n * Starts segment epics. `redux-sparse-kit/observable` provides one for\n * redux-observable.\n */\nexport interface EpicRunner {\n /** Starts `epic` and returns a function that stops it. */\n run(epic: SegmentEpic, segment: AnySegment): () => void;\n}\n\nexport interface SparseOptions {\n /** Required as soon as a held segment has epics. */\n epicRunner?: EpicRunner;\n /**\n * How long a segment nothing holds stays present before it is evicted, in\n * milliseconds. `0` evicts it right away, `Infinity` never. Segments can\n * override it. Defaults to one minute.\n */\n idleTimeout?: number;\n /**\n * What an idle segment does until it is evicted. Segments can override it.\n * Defaults to `keep-running`.\n */\n whileIdle?: WhileIdle;\n /**\n * How many idle segments may wait for their idle timeout at once. Past it,\n * the ones idle the longest are evicted right away. Defaults to `Infinity`.\n */\n maxIdle?: number;\n /**\n * Stops the idle clock while the page is hidden, so that time spent in a\n * background tab does not count towards the idle timeout. Defaults to `true`.\n */\n pauseWhenHidden?: boolean;\n}\n\ninterface Entry {\n readonly segment: AnySegment;\n /** Holds by `acquire` callers and by present segments that depend on it. */\n holders: number;\n /** Stops the segment's running epics. */\n stops: (() => void)[];\n /** Fires once the segment has been idle for its idle timeout. */\n timer: ReturnType<typeof setTimeout> | undefined;\n /** Idle time left before it expires; `Infinity` when it never does. */\n remaining: number;\n /** When the running timer was started. */\n startedAt: number;\n /** Idle for its whole idle timeout, so evicted by the next sweep. */\n expired: boolean;\n /** Idle with its epics stopped. */\n paused: boolean;\n /** Idle with its reducer skipped. */\n frozen: boolean;\n}\n\ntype AnyReducer = Reducer<any, Action, any>;\n\nconst emptyKeys: ReadonlySet<string> = new Set();\n\nconst defaultIdleTimeout = 60_000;\n\n// Reduced only by the root reducer, to learn the keys of the static state.\nconst probeActionType = '@@sparse/PROBE';\n\nconst whileIdleValues: readonly WhileIdle[] = ['keep-running', 'pause-epics', 'freeze'];\n\n// Longer delays overflow `setTimeout` and would fire right away.\nconst maxTimerDelay = 2 ** 31 - 1;\n\nconst scheduleIdle = (callback: () => void) => {\n if (typeof requestIdleCallback === 'function') {\n requestIdleCallback(callback, { timeout: 1000 });\n } else {\n setTimeout(callback, 0);\n }\n};\n\n/**\n * A store enhancer that lets segments be injected into and evicted from a\n * running store. The root reducer passed to `createStore` keeps owning the\n * static state; segment slices live next to it under their keys.\n */\nexport function sparseEnhancer(options: SparseOptions = {}): StoreEnhancer<SparseStoreExt> {\n const {\n epicRunner,\n idleTimeout = defaultIdleTimeout,\n whileIdle = 'keep-running',\n maxIdle = Infinity,\n pauseWhenHidden = true,\n } = options;\n\n const isHidden = () =>\n pauseWhenHidden && typeof document !== 'undefined' && document.visibilityState === 'hidden';\n\n if (!(idleTimeout >= 0)) {\n throw new RangeError('The idle timeout must be a non-negative number');\n }\n\n if (!(maxIdle >= 0)) {\n throw new RangeError('maxIdle must be a non-negative number');\n }\n\n if (!whileIdleValues.includes(whileIdle)) {\n throw new RangeError(`whileIdle must be one of ${whileIdleValues.join(', ')}`);\n }\n\n const enhancer =\n (createStore: StoreEnhancerStoreCreator) =>\n (rootReducer: AnyReducer, preloadedState?: unknown) => {\n const entries = new Map<string, Entry>();\n // Idle entries, the one idle the longest first.\n const idleEntries = new Set<Entry>();\n // Keys being removed by the EVICT action currently being reduced.\n let evictingKeys: ReadonlySet<string> = emptyKeys;\n\n // Injected segments that own a slice of the state, by key: only those\n // with a reducer. They keep reducing until the action that evicts them.\n const slices = new Map<string, Entry>();\n\n // Keys of the static state: whatever the root reducer returns. Never\n // treated as segment slices, even when a segment uses the same key.\n let staticKeys: ReadonlySet<string> = emptyKeys;\n\n const learnStaticKeys = (staticState: object) => {\n let count = 0;\n for (const key in staticState) {\n count++;\n if (!staticKeys.has(key)) {\n staticKeys = new Set(Object.keys(staticState));\n return;\n }\n }\n if (count !== staticKeys.size) {\n staticKeys = new Set(Object.keys(staticState));\n }\n };\n\n /** Asks a root reducer for its keys before it reduces a real state. */\n const probeStaticKeys = (root: AnyReducer) => {\n learnStaticKeys(root(undefined, { type: probeActionType }) as object);\n };\n\n const isDynamicKey = (key: string) =>\n !staticKeys.has(key) && (slices.has(key) || evictingKeys.has(key) || isSegmentKey(key));\n\n const wrapReducer =\n (root: AnyReducer): AnyReducer =>\n (state, action) => {\n const current = (state ?? {}) as Record<string, unknown>;\n\n let staticState: Record<string, unknown> | undefined;\n let hasDynamicKeys = false;\n\n for (const key in current) {\n if (isDynamicKey(key)) {\n hasDynamicKeys = true;\n break;\n }\n }\n\n // Slices of segments that are not present yet, e.g. from a preloaded\n // state: kept so that the segments start from them.\n const pendingSlices: string[] = [];\n\n if (hasDynamicKeys) {\n staticState = {};\n for (const key in current) {\n if (!isDynamicKey(key)) {\n staticState[key] = current[key];\n } else if (!slices.has(key) && !evictingKeys.has(key)) {\n pendingSlices.push(key);\n }\n }\n }\n\n const input = state === undefined ? undefined : (staticState ?? current);\n const nextStatic = root(input, action) as Record<string, unknown>;\n\n if (nextStatic !== input) {\n learnStaticKeys(nextStatic);\n }\n\n let changed = nextStatic !== input || evictingKeys.size > 0;\n const values: unknown[] = [];\n\n for (const [key, entry] of slices) {\n const previous = current[key];\n const value =\n entry.frozen && key in current ? previous : entry.segment.reducer!(previous, action);\n values.push(value);\n if (value !== previous || !(key in current)) {\n changed = true;\n }\n }\n\n if (!changed) {\n return current;\n }\n\n const next: Record<string, unknown> = { ...nextStatic };\n\n for (const key of pendingSlices) {\n next[key] = current[key];\n }\n\n let index = 0;\n for (const key of slices.keys()) {\n next[key] = values[index++];\n }\n\n return next;\n };\n\n probeStaticKeys(rootReducer);\n const store = createStore(wrapReducer(rootReducer), preloadedState);\n\n let batchDepth = 0;\n const pendingInject: Entry[] = [];\n const pendingEvict: Entry[] = [];\n const listeners = new Set<(event: SparseEvent) => void>();\n\n const emit = (event: SparseEvent) => {\n for (const listener of listeners) {\n listener(event);\n }\n };\n\n const dispatchSparse = (type: SparseAction['type'], keys: readonly string[]) => {\n const action: SparseAction = { type, payload: { keys } };\n store.dispatch(action);\n };\n\n const flush = () => {\n if (batchDepth > 0) {\n return;\n }\n\n if (pendingInject.length > 0) {\n const injected = pendingInject.splice(0);\n const keys = injected.map((entry) => entry.segment.key);\n const sliceKeys: string[] = [];\n for (const entry of injected) {\n if (entry.segment.reducer) {\n slices.set(entry.segment.key, entry);\n sliceKeys.push(entry.segment.key);\n }\n }\n if (sliceKeys.length > 0) {\n dispatchSparse(SparseActionTypes.INJECT, sliceKeys);\n }\n // Started once their slices are in the state, deps first.\n for (const entry of injected) {\n if (!entry.paused) {\n startEpics(entry);\n }\n }\n emit({ type: 'inject', keys });\n }\n\n if (pendingEvict.length > 0) {\n const evicted = pendingEvict.splice(0);\n const keys = evicted.map((entry) => entry.segment.key);\n // Stopped while their slices are still in the state, dependents first.\n for (const entry of evicted) {\n stopEpics(entry);\n }\n const sliceKeys = keys.filter((key) => slices.delete(key));\n if (sliceKeys.length > 0) {\n evictingKeys = new Set(sliceKeys);\n try {\n dispatchSparse(SparseActionTypes.EVICT, sliceKeys);\n } finally {\n evictingKeys = emptyKeys;\n }\n }\n emit({ type: 'evict', keys });\n }\n };\n\n const startEpics = (entry: Entry) => {\n for (const epic of entry.segment.epics) {\n entry.stops.push(epicRunner!.run(epic, entry.segment));\n }\n };\n\n const stopEpics = (entry: Entry) => {\n for (const stop of entry.stops.splice(0)) {\n stop();\n }\n };\n\n const batch = <T>(fn: () => T): T => {\n batchDepth++;\n try {\n return fn();\n } finally {\n batchDepth--;\n flush();\n }\n };\n\n const assertCompatible = (segment: AnySegment, seen: Map<string, AnySegment>) => {\n const other = seen.get(segment.key) ?? entries.get(segment.key)?.segment;\n\n if (other === segment) {\n return;\n }\n\n if (other !== undefined) {\n throw new Error(`Two different segments use the key \"${segment.key}\"`);\n }\n\n if (segment.reducer && staticKeys.has(segment.key)) {\n throw new Error(`Segment \"${segment.key}\" uses the key of a slice of the static state`);\n }\n\n if (segment.epics.length > 0 && epicRunner === undefined) {\n throw new Error(\n `Segment \"${segment.key}\" has epics, but sparseEnhancer was created without an epicRunner`,\n );\n }\n\n seen.set(segment.key, segment);\n\n for (const dep of segment.deps) {\n assertCompatible(dep, seen);\n }\n };\n\n const hold = (segment: AnySegment) => {\n const existing = entries.get(segment.key);\n\n if (existing) {\n if (existing.holders === 0) {\n revive(existing);\n }\n existing.holders++;\n return;\n }\n\n const evictIndex = pendingEvict.findIndex((entry) => entry.segment === segment);\n\n if (evictIndex !== -1) {\n // Released and held again before the eviction reached the store:\n // its slice is still there, so it only takes its holds back.\n const [entry] = pendingEvict.splice(evictIndex, 1);\n entry!.holders = 1;\n entries.set(segment.key, entry!);\n for (const dep of segment.deps) {\n hold(dep);\n }\n return;\n }\n\n const entry: Entry = {\n segment,\n holders: 1,\n stops: [],\n timer: undefined,\n remaining: Infinity,\n startedAt: 0,\n expired: false,\n paused: false,\n frozen: false,\n };\n entries.set(segment.key, entry);\n\n // Dependencies first, so they are injected before their dependents.\n for (const dep of segment.deps) {\n hold(dep);\n }\n\n pendingInject.push(entry);\n };\n\n /**\n * Drops a hold. A segment left without holders goes idle, unless it was\n * only held by a segment being evicted: then it is evicted with it.\n */\n const unhold = (segment: AnySegment, cascade = false) => {\n // Only reached for segments this store holds: a release undoes exactly\n // the holds its acquire took.\n const entry = entries.get(segment.key)!;\n\n entry.holders--;\n\n if (entry.holders > 0) {\n return;\n }\n\n const timeout = segment.idleTimeout ?? idleTimeout;\n\n if (cascade || timeout === 0) {\n evict(entry);\n } else {\n becomeIdle(entry, timeout);\n }\n };\n\n const becomeIdle = (entry: Entry, timeout: number) => {\n const policy = entry.segment.whileIdle ?? whileIdle;\n\n if (policy !== 'keep-running') {\n entry.paused = true;\n entry.frozen = policy === 'freeze';\n stopEpics(entry);\n }\n\n entry.remaining = timeout > maxTimerDelay ? Infinity : timeout;\n if (!isHidden()) {\n startTimer(entry);\n }\n idleEntries.add(entry);\n watchVisibility();\n emit({ type: 'idle', key: entry.segment.key });\n\n while (idleEntries.size > maxIdle) {\n evict(idleEntries.values().next().value!);\n }\n };\n\n const startTimer = (entry: Entry) => {\n if (entry.remaining !== Infinity) {\n entry.startedAt = Date.now();\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.expired = true;\n scheduleSweep();\n }, entry.remaining);\n }\n };\n\n const pauseTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer !== undefined) {\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.remaining = Math.max(0, entry.remaining - (Date.now() - entry.startedAt));\n }\n }\n };\n\n const resumeTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer === undefined && !entry.expired) {\n startTimer(entry);\n }\n }\n };\n\n const onVisibilityChange = () => {\n if (isHidden()) {\n pauseTimers();\n } else {\n resumeTimers();\n }\n };\n\n let watchingVisibility = false;\n\n /** Listens to the page visibility only while there are idle segments. */\n const watchVisibility = () => {\n const watch = idleEntries.size > 0;\n\n if (watch !== watchingVisibility && pauseWhenHidden && typeof document !== 'undefined') {\n watchingVisibility = watch;\n if (watch) {\n document.addEventListener('visibilitychange', onVisibilityChange);\n } else {\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n }\n };\n\n const revive = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.expired = false;\n entry.frozen = false;\n\n if (entry.paused) {\n entry.paused = false;\n // Not injected yet: its epics start once it is.\n if (!pendingInject.includes(entry)) {\n startEpics(entry);\n }\n }\n\n emit({ type: 'revive', key: entry.segment.key });\n };\n\n const evict = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entries.delete(entry.segment.key);\n\n const pendingIndex = pendingInject.indexOf(entry);\n if (pendingIndex !== -1) {\n // Released before it reached the store: nothing to evict.\n pendingInject.splice(pendingIndex, 1);\n } else {\n pendingEvict.push(entry);\n }\n\n // Dependents first, so they are evicted before their dependencies.\n for (const dep of entry.segment.deps) {\n unhold(dep, true);\n }\n };\n\n let sweepScheduled = false;\n\n /** Evicts every expired segment with a single action. */\n const sweep = () => {\n sweepScheduled = false;\n batch(() => {\n for (const entry of entries.values()) {\n if (entry.expired) {\n evict(entry);\n }\n }\n });\n };\n\n const scheduleSweep = () => {\n if (!sweepScheduled) {\n sweepScheduled = true;\n scheduleIdle(sweep);\n }\n };\n\n const sparse: SparseController = {\n acquire(...segments) {\n // Checked up front so that a refused acquire leaves no holds behind.\n const seen = new Map<string, AnySegment>();\n for (const segment of segments) {\n assertCompatible(segment, seen);\n }\n\n batch(() => {\n for (const segment of segments) {\n hold(segment);\n }\n });\n\n let released = false;\n\n return () => {\n if (released) {\n return;\n }\n released = true;\n\n batch(() => {\n for (const segment of segments) {\n unhold(segment);\n }\n });\n };\n },\n\n has(segmentOrKey) {\n const key = typeof segmentOrKey === 'string' ? segmentOrKey : segmentOrKey.key;\n return entries.has(key) && !pendingInject.includes(entries.get(key)!);\n },\n\n isStatic(key) {\n return staticKeys.has(key);\n },\n\n stats() {\n const stats: SparseStats = { active: [], idle: [] };\n for (const [key, entry] of entries) {\n if (!pendingInject.includes(entry)) {\n (entry.holders > 0 ? stats.active : stats.idle).push(key);\n }\n }\n return stats;\n },\n\n batch,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n\n return {\n ...store,\n replaceReducer(nextReducer: AnyReducer) {\n probeStaticKeys(nextReducer);\n store.replaceReducer(wrapReducer(nextReducer));\n },\n sparse,\n };\n };\n\n return enhancer as StoreEnhancer<SparseStoreExt>;\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const SparseActionTypes = {
|
|
|
13
13
|
};
|
|
14
14
|
const emptyKeys = /* @__PURE__ */ new Set();
|
|
15
15
|
const defaultIdleTimeout = 6e4;
|
|
16
|
+
const probeActionType = "@@sparse/PROBE";
|
|
16
17
|
const whileIdleValues = [
|
|
17
18
|
"keep-running",
|
|
18
19
|
"pause-epics",
|
|
@@ -39,7 +40,23 @@ function sparseEnhancer(options = {}) {
|
|
|
39
40
|
const idleEntries = /* @__PURE__ */ new Set();
|
|
40
41
|
let evictingKeys = emptyKeys;
|
|
41
42
|
const slices = /* @__PURE__ */ new Map();
|
|
42
|
-
|
|
43
|
+
let staticKeys = emptyKeys;
|
|
44
|
+
const learnStaticKeys = (staticState) => {
|
|
45
|
+
let count = 0;
|
|
46
|
+
for (const key in staticState) {
|
|
47
|
+
count++;
|
|
48
|
+
if (!staticKeys.has(key)) {
|
|
49
|
+
staticKeys = new Set(Object.keys(staticState));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (count !== staticKeys.size) staticKeys = new Set(Object.keys(staticState));
|
|
54
|
+
};
|
|
55
|
+
/** Asks a root reducer for its keys before it reduces a real state. */
|
|
56
|
+
const probeStaticKeys = (root) => {
|
|
57
|
+
learnStaticKeys(root(void 0, { type: probeActionType }));
|
|
58
|
+
};
|
|
59
|
+
const isDynamicKey = (key) => !staticKeys.has(key) && (slices.has(key) || evictingKeys.has(key) || isSegmentKey(key));
|
|
43
60
|
const wrapReducer = (root) => (state, action) => {
|
|
44
61
|
var _staticState;
|
|
45
62
|
const current = state !== null && state !== void 0 ? state : {};
|
|
@@ -49,12 +66,15 @@ function sparseEnhancer(options = {}) {
|
|
|
49
66
|
hasDynamicKeys = true;
|
|
50
67
|
break;
|
|
51
68
|
}
|
|
69
|
+
const pendingSlices = [];
|
|
52
70
|
if (hasDynamicKeys) {
|
|
53
71
|
staticState = {};
|
|
54
72
|
for (const key in current) if (!isDynamicKey(key)) staticState[key] = current[key];
|
|
73
|
+
else if (!slices.has(key) && !evictingKeys.has(key)) pendingSlices.push(key);
|
|
55
74
|
}
|
|
56
75
|
const input = state === void 0 ? void 0 : (_staticState = staticState) !== null && _staticState !== void 0 ? _staticState : current;
|
|
57
76
|
const nextStatic = root(input, action);
|
|
77
|
+
if (nextStatic !== input) learnStaticKeys(nextStatic);
|
|
58
78
|
let changed = nextStatic !== input || evictingKeys.size > 0;
|
|
59
79
|
const values = [];
|
|
60
80
|
for (const [key, entry] of slices) {
|
|
@@ -65,11 +85,12 @@ function sparseEnhancer(options = {}) {
|
|
|
65
85
|
}
|
|
66
86
|
if (!changed) return current;
|
|
67
87
|
const next = { ...nextStatic };
|
|
68
|
-
for (const key
|
|
88
|
+
for (const key of pendingSlices) next[key] = current[key];
|
|
69
89
|
let index = 0;
|
|
70
90
|
for (const key of slices.keys()) next[key] = values[index++];
|
|
71
91
|
return next;
|
|
72
92
|
};
|
|
93
|
+
probeStaticKeys(rootReducer);
|
|
73
94
|
const store = createStore(wrapReducer(rootReducer), preloadedState);
|
|
74
95
|
let batchDepth = 0;
|
|
75
96
|
const pendingInject = [];
|
|
@@ -141,6 +162,7 @@ function sparseEnhancer(options = {}) {
|
|
|
141
162
|
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;
|
|
142
163
|
if (other === segment) return;
|
|
143
164
|
if (other !== void 0) throw new Error(`Two different segments use the key "${segment.key}"`);
|
|
165
|
+
if (segment.reducer && staticKeys.has(segment.key)) throw new Error(`Segment "${segment.key}" uses the key of a slice of the static state`);
|
|
144
166
|
if (segment.epics.length > 0 && epicRunner === void 0) throw new Error(`Segment "${segment.key}" has epics, but sparseEnhancer was created without an epicRunner`);
|
|
145
167
|
seen.set(segment.key, segment);
|
|
146
168
|
for (const dep of segment.deps) assertCompatible(dep, seen);
|
|
@@ -300,6 +322,9 @@ function sparseEnhancer(options = {}) {
|
|
|
300
322
|
const key = typeof segmentOrKey === "string" ? segmentOrKey : segmentOrKey.key;
|
|
301
323
|
return entries.has(key) && !pendingInject.includes(entries.get(key));
|
|
302
324
|
},
|
|
325
|
+
isStatic(key) {
|
|
326
|
+
return staticKeys.has(key);
|
|
327
|
+
},
|
|
303
328
|
stats() {
|
|
304
329
|
const stats = {
|
|
305
330
|
active: [],
|
|
@@ -319,6 +344,7 @@ function sparseEnhancer(options = {}) {
|
|
|
319
344
|
return {
|
|
320
345
|
...store,
|
|
321
346
|
replaceReducer(nextReducer) {
|
|
347
|
+
probeStaticKeys(nextReducer);
|
|
322
348
|
store.replaceReducer(wrapReducer(nextReducer));
|
|
323
349
|
},
|
|
324
350
|
sparse
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/sparseEnhancer.ts"],"sourcesContent":["import type { Action, Reducer, StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';\nimport type { AnySegment, SegmentEpic, WhileIdle } from './segment';\nimport { isSegmentKey } from './segment';\n\n/**\n * Actions the sparse store dispatches. Reducers outside the segments can\n * handle {@linkcode SparseActionTypes.EVICT} to drop data that refers to an\n * evicted segment.\n */\nexport const SparseActionTypes = {\n /** Segment slices with the keys in `payload.keys` were added to the state. */\n INJECT: '@@sparse/INJECT',\n /** Segment slices with the keys in `payload.keys` are removed by this action. */\n EVICT: '@@sparse/EVICT',\n} as const;\n\nexport interface SparseAction extends Action<\n (typeof SparseActionTypes)[keyof typeof SparseActionTypes]\n> {\n payload: { keys: readonly string[] };\n}\n\nexport type SparseEvent =\n | { type: 'inject'; keys: readonly string[] }\n | { type: 'evict'; keys: readonly string[] }\n /** A segment is no longer held and waits for its idle timeout. */\n | { type: 'idle'; key: string }\n /** An idle segment is held again before it was evicted. */\n | { type: 'revive'; key: string };\n\nexport interface SparseStats {\n /** Keys of present segments that something holds. */\n active: string[];\n /** Keys of present segments waiting for their idle timeout. */\n idle: string[];\n}\n\nexport interface SparseController {\n /**\n * Holds the given segments, injecting the ones that are not present yet\n * together with their dependencies. Returns a function that releases the\n * hold; calling it more than once has no further effect.\n */\n acquire(...segments: readonly AnySegment[]): () => void;\n /** Whether a segment with this key is present in the store. */\n has(segmentOrKey: AnySegment | string): boolean;\n /**\n * Runs `fn`, deferring injections and evictions it causes until it returns,\n * so that they reach the store as a single action of each kind.\n */\n batch<T>(fn: () => T): T;\n /** Listens to segments being injected, going idle, revived and evicted. */\n subscribe(listener: (event: SparseEvent) => void): () => void;\n /** The keys of the present segments, by state. */\n stats(): SparseStats;\n}\n\nexport interface SparseStoreExt {\n readonly sparse: SparseController;\n}\n\n/**\n * Starts segment epics. `redux-sparse-kit/observable` provides one for\n * redux-observable.\n */\nexport interface EpicRunner {\n /** Starts `epic` and returns a function that stops it. */\n run(epic: SegmentEpic, segment: AnySegment): () => void;\n}\n\nexport interface SparseOptions {\n /** Required as soon as a held segment has epics. */\n epicRunner?: EpicRunner;\n /**\n * How long a segment nothing holds stays present before it is evicted, in\n * milliseconds. `0` evicts it right away, `Infinity` never. Segments can\n * override it. Defaults to one minute.\n */\n idleTimeout?: number;\n /**\n * What an idle segment does until it is evicted. Segments can override it.\n * Defaults to `keep-running`.\n */\n whileIdle?: WhileIdle;\n /**\n * How many idle segments may wait for their idle timeout at once. Past it,\n * the ones idle the longest are evicted right away. Defaults to `Infinity`.\n */\n maxIdle?: number;\n /**\n * Stops the idle clock while the page is hidden, so that time spent in a\n * background tab does not count towards the idle timeout. Defaults to `true`.\n */\n pauseWhenHidden?: boolean;\n}\n\ninterface Entry {\n readonly segment: AnySegment;\n /** Holds by `acquire` callers and by present segments that depend on it. */\n holders: number;\n /** Stops the segment's running epics. */\n stops: (() => void)[];\n /** Fires once the segment has been idle for its idle timeout. */\n timer: ReturnType<typeof setTimeout> | undefined;\n /** Idle time left before it expires; `Infinity` when it never does. */\n remaining: number;\n /** When the running timer was started. */\n startedAt: number;\n /** Idle for its whole idle timeout, so evicted by the next sweep. */\n expired: boolean;\n /** Idle with its epics stopped. */\n paused: boolean;\n /** Idle with its reducer skipped. */\n frozen: boolean;\n}\n\ntype AnyReducer = Reducer<any, Action, any>;\n\nconst emptyKeys: ReadonlySet<string> = new Set();\n\nconst defaultIdleTimeout = 60_000;\n\nconst whileIdleValues: readonly WhileIdle[] = ['keep-running', 'pause-epics', 'freeze'];\n\n// Longer delays overflow `setTimeout` and would fire right away.\nconst maxTimerDelay = 2 ** 31 - 1;\n\nconst scheduleIdle = (callback: () => void) => {\n if (typeof requestIdleCallback === 'function') {\n requestIdleCallback(callback, { timeout: 1000 });\n } else {\n setTimeout(callback, 0);\n }\n};\n\n/**\n * A store enhancer that lets segments be injected into and evicted from a\n * running store. The root reducer passed to `createStore` keeps owning the\n * static state; segment slices live next to it under their keys.\n */\nexport function sparseEnhancer(options: SparseOptions = {}): StoreEnhancer<SparseStoreExt> {\n const {\n epicRunner,\n idleTimeout = defaultIdleTimeout,\n whileIdle = 'keep-running',\n maxIdle = Infinity,\n pauseWhenHidden = true,\n } = options;\n\n const isHidden = () =>\n pauseWhenHidden && typeof document !== 'undefined' && document.visibilityState === 'hidden';\n\n if (!(idleTimeout >= 0)) {\n throw new RangeError('The idle timeout must be a non-negative number');\n }\n\n if (!(maxIdle >= 0)) {\n throw new RangeError('maxIdle must be a non-negative number');\n }\n\n if (!whileIdleValues.includes(whileIdle)) {\n throw new RangeError(`whileIdle must be one of ${whileIdleValues.join(', ')}`);\n }\n\n const enhancer =\n (createStore: StoreEnhancerStoreCreator) =>\n (rootReducer: AnyReducer, preloadedState?: unknown) => {\n const entries = new Map<string, Entry>();\n // Idle entries, the one idle the longest first.\n const idleEntries = new Set<Entry>();\n // Keys being removed by the EVICT action currently being reduced.\n let evictingKeys: ReadonlySet<string> = emptyKeys;\n\n // Injected segments that own a slice of the state, by key: only those\n // with a reducer. They keep reducing until the action that evicts them.\n const slices = new Map<string, Entry>();\n\n const isDynamicKey = (key: string) =>\n slices.has(key) || evictingKeys.has(key) || isSegmentKey(key);\n\n const wrapReducer =\n (root: AnyReducer): AnyReducer =>\n (state, action) => {\n const current = (state ?? {}) as Record<string, unknown>;\n\n let staticState: Record<string, unknown> | undefined;\n let hasDynamicKeys = false;\n\n for (const key in current) {\n if (isDynamicKey(key)) {\n hasDynamicKeys = true;\n break;\n }\n }\n\n if (hasDynamicKeys) {\n staticState = {};\n for (const key in current) {\n if (!isDynamicKey(key)) {\n staticState[key] = current[key];\n }\n }\n }\n\n const input = state === undefined ? undefined : (staticState ?? current);\n const nextStatic = root(input, action) as Record<string, unknown>;\n\n let changed = nextStatic !== input || evictingKeys.size > 0;\n const values: unknown[] = [];\n\n for (const [key, entry] of slices) {\n const previous = current[key];\n const value =\n entry.frozen && key in current ? previous : entry.segment.reducer!(previous, action);\n values.push(value);\n if (value !== previous || !(key in current)) {\n changed = true;\n }\n }\n\n if (!changed) {\n return current;\n }\n\n const next: Record<string, unknown> = { ...nextStatic };\n\n for (const key in current) {\n if (!slices.has(key) && !evictingKeys.has(key) && isSegmentKey(key)) {\n // A slice of a segment that is not present yet, e.g. from a\n // preloaded state: kept so the segment starts from it.\n next[key] = current[key];\n }\n }\n\n let index = 0;\n for (const key of slices.keys()) {\n next[key] = values[index++];\n }\n\n return next;\n };\n\n const store = createStore(wrapReducer(rootReducer), preloadedState);\n\n let batchDepth = 0;\n const pendingInject: Entry[] = [];\n const pendingEvict: Entry[] = [];\n const listeners = new Set<(event: SparseEvent) => void>();\n\n const emit = (event: SparseEvent) => {\n for (const listener of listeners) {\n listener(event);\n }\n };\n\n const dispatchSparse = (type: SparseAction['type'], keys: readonly string[]) => {\n const action: SparseAction = { type, payload: { keys } };\n store.dispatch(action);\n };\n\n const flush = () => {\n if (batchDepth > 0) {\n return;\n }\n\n if (pendingInject.length > 0) {\n const injected = pendingInject.splice(0);\n const keys = injected.map((entry) => entry.segment.key);\n const sliceKeys: string[] = [];\n for (const entry of injected) {\n if (entry.segment.reducer) {\n slices.set(entry.segment.key, entry);\n sliceKeys.push(entry.segment.key);\n }\n }\n if (sliceKeys.length > 0) {\n dispatchSparse(SparseActionTypes.INJECT, sliceKeys);\n }\n // Started once their slices are in the state, deps first.\n for (const entry of injected) {\n if (!entry.paused) {\n startEpics(entry);\n }\n }\n emit({ type: 'inject', keys });\n }\n\n if (pendingEvict.length > 0) {\n const evicted = pendingEvict.splice(0);\n const keys = evicted.map((entry) => entry.segment.key);\n // Stopped while their slices are still in the state, dependents first.\n for (const entry of evicted) {\n stopEpics(entry);\n }\n const sliceKeys = keys.filter((key) => slices.delete(key));\n if (sliceKeys.length > 0) {\n evictingKeys = new Set(sliceKeys);\n try {\n dispatchSparse(SparseActionTypes.EVICT, sliceKeys);\n } finally {\n evictingKeys = emptyKeys;\n }\n }\n emit({ type: 'evict', keys });\n }\n };\n\n const startEpics = (entry: Entry) => {\n for (const epic of entry.segment.epics) {\n entry.stops.push(epicRunner!.run(epic, entry.segment));\n }\n };\n\n const stopEpics = (entry: Entry) => {\n for (const stop of entry.stops.splice(0)) {\n stop();\n }\n };\n\n const batch = <T>(fn: () => T): T => {\n batchDepth++;\n try {\n return fn();\n } finally {\n batchDepth--;\n flush();\n }\n };\n\n const assertCompatible = (segment: AnySegment, seen: Map<string, AnySegment>) => {\n const other = seen.get(segment.key) ?? entries.get(segment.key)?.segment;\n\n if (other === segment) {\n return;\n }\n\n if (other !== undefined) {\n throw new Error(`Two different segments use the key \"${segment.key}\"`);\n }\n\n if (segment.epics.length > 0 && epicRunner === undefined) {\n throw new Error(\n `Segment \"${segment.key}\" has epics, but sparseEnhancer was created without an epicRunner`,\n );\n }\n\n seen.set(segment.key, segment);\n\n for (const dep of segment.deps) {\n assertCompatible(dep, seen);\n }\n };\n\n const hold = (segment: AnySegment) => {\n const existing = entries.get(segment.key);\n\n if (existing) {\n if (existing.holders === 0) {\n revive(existing);\n }\n existing.holders++;\n return;\n }\n\n const evictIndex = pendingEvict.findIndex((entry) => entry.segment === segment);\n\n if (evictIndex !== -1) {\n // Released and held again before the eviction reached the store:\n // its slice is still there, so it only takes its holds back.\n const [entry] = pendingEvict.splice(evictIndex, 1);\n entry!.holders = 1;\n entries.set(segment.key, entry!);\n for (const dep of segment.deps) {\n hold(dep);\n }\n return;\n }\n\n const entry: Entry = {\n segment,\n holders: 1,\n stops: [],\n timer: undefined,\n remaining: Infinity,\n startedAt: 0,\n expired: false,\n paused: false,\n frozen: false,\n };\n entries.set(segment.key, entry);\n\n // Dependencies first, so they are injected before their dependents.\n for (const dep of segment.deps) {\n hold(dep);\n }\n\n pendingInject.push(entry);\n };\n\n /**\n * Drops a hold. A segment left without holders goes idle, unless it was\n * only held by a segment being evicted: then it is evicted with it.\n */\n const unhold = (segment: AnySegment, cascade = false) => {\n // Only reached for segments this store holds: a release undoes exactly\n // the holds its acquire took.\n const entry = entries.get(segment.key)!;\n\n entry.holders--;\n\n if (entry.holders > 0) {\n return;\n }\n\n const timeout = segment.idleTimeout ?? idleTimeout;\n\n if (cascade || timeout === 0) {\n evict(entry);\n } else {\n becomeIdle(entry, timeout);\n }\n };\n\n const becomeIdle = (entry: Entry, timeout: number) => {\n const policy = entry.segment.whileIdle ?? whileIdle;\n\n if (policy !== 'keep-running') {\n entry.paused = true;\n entry.frozen = policy === 'freeze';\n stopEpics(entry);\n }\n\n entry.remaining = timeout > maxTimerDelay ? Infinity : timeout;\n if (!isHidden()) {\n startTimer(entry);\n }\n idleEntries.add(entry);\n watchVisibility();\n emit({ type: 'idle', key: entry.segment.key });\n\n while (idleEntries.size > maxIdle) {\n evict(idleEntries.values().next().value!);\n }\n };\n\n const startTimer = (entry: Entry) => {\n if (entry.remaining !== Infinity) {\n entry.startedAt = Date.now();\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.expired = true;\n scheduleSweep();\n }, entry.remaining);\n }\n };\n\n const pauseTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer !== undefined) {\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.remaining = Math.max(0, entry.remaining - (Date.now() - entry.startedAt));\n }\n }\n };\n\n const resumeTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer === undefined && !entry.expired) {\n startTimer(entry);\n }\n }\n };\n\n const onVisibilityChange = () => {\n if (isHidden()) {\n pauseTimers();\n } else {\n resumeTimers();\n }\n };\n\n let watchingVisibility = false;\n\n /** Listens to the page visibility only while there are idle segments. */\n const watchVisibility = () => {\n const watch = idleEntries.size > 0;\n\n if (watch !== watchingVisibility && pauseWhenHidden && typeof document !== 'undefined') {\n watchingVisibility = watch;\n if (watch) {\n document.addEventListener('visibilitychange', onVisibilityChange);\n } else {\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n }\n };\n\n const revive = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.expired = false;\n entry.frozen = false;\n\n if (entry.paused) {\n entry.paused = false;\n // Not injected yet: its epics start once it is.\n if (!pendingInject.includes(entry)) {\n startEpics(entry);\n }\n }\n\n emit({ type: 'revive', key: entry.segment.key });\n };\n\n const evict = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entries.delete(entry.segment.key);\n\n const pendingIndex = pendingInject.indexOf(entry);\n if (pendingIndex !== -1) {\n // Released before it reached the store: nothing to evict.\n pendingInject.splice(pendingIndex, 1);\n } else {\n pendingEvict.push(entry);\n }\n\n // Dependents first, so they are evicted before their dependencies.\n for (const dep of entry.segment.deps) {\n unhold(dep, true);\n }\n };\n\n let sweepScheduled = false;\n\n /** Evicts every expired segment with a single action. */\n const sweep = () => {\n sweepScheduled = false;\n batch(() => {\n for (const entry of entries.values()) {\n if (entry.expired) {\n evict(entry);\n }\n }\n });\n };\n\n const scheduleSweep = () => {\n if (!sweepScheduled) {\n sweepScheduled = true;\n scheduleIdle(sweep);\n }\n };\n\n const sparse: SparseController = {\n acquire(...segments) {\n // Checked up front so that a refused acquire leaves no holds behind.\n const seen = new Map<string, AnySegment>();\n for (const segment of segments) {\n assertCompatible(segment, seen);\n }\n\n batch(() => {\n for (const segment of segments) {\n hold(segment);\n }\n });\n\n let released = false;\n\n return () => {\n if (released) {\n return;\n }\n released = true;\n\n batch(() => {\n for (const segment of segments) {\n unhold(segment);\n }\n });\n };\n },\n\n has(segmentOrKey) {\n const key = typeof segmentOrKey === 'string' ? segmentOrKey : segmentOrKey.key;\n return entries.has(key) && !pendingInject.includes(entries.get(key)!);\n },\n\n stats() {\n const stats: SparseStats = { active: [], idle: [] };\n for (const [key, entry] of entries) {\n if (!pendingInject.includes(entry)) {\n (entry.holders > 0 ? stats.active : stats.idle).push(key);\n }\n }\n return stats;\n },\n\n batch,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n\n return {\n ...store,\n replaceReducer(nextReducer: AnyReducer) {\n store.replaceReducer(wrapReducer(nextReducer));\n },\n sparse,\n };\n };\n\n return enhancer as StoreEnhancer<SparseStoreExt>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/sparseEnhancer.ts"],"sourcesContent":["import type { Action, Reducer, StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';\nimport type { AnySegment, SegmentEpic, WhileIdle } from './segment';\nimport { isSegmentKey } from './segment';\n\n/**\n * Actions the sparse store dispatches. Reducers outside the segments can\n * handle {@linkcode SparseActionTypes.EVICT} to drop data that refers to an\n * evicted segment.\n */\nexport const SparseActionTypes = {\n /** Segment slices with the keys in `payload.keys` were added to the state. */\n INJECT: '@@sparse/INJECT',\n /** Segment slices with the keys in `payload.keys` are removed by this action. */\n EVICT: '@@sparse/EVICT',\n} as const;\n\nexport interface SparseAction extends Action<\n (typeof SparseActionTypes)[keyof typeof SparseActionTypes]\n> {\n payload: { keys: readonly string[] };\n}\n\nexport type SparseEvent =\n | { type: 'inject'; keys: readonly string[] }\n | { type: 'evict'; keys: readonly string[] }\n /** A segment is no longer held and waits for its idle timeout. */\n | { type: 'idle'; key: string }\n /** An idle segment is held again before it was evicted. */\n | { type: 'revive'; key: string };\n\nexport interface SparseStats {\n /** Keys of present segments that something holds. */\n active: string[];\n /** Keys of present segments waiting for their idle timeout. */\n idle: string[];\n}\n\nexport interface SparseController {\n /**\n * Holds the given segments, injecting the ones that are not present yet\n * together with their dependencies. Returns a function that releases the\n * hold; calling it more than once has no further effect.\n */\n acquire(...segments: readonly AnySegment[]): () => void;\n /** Whether a segment with this key is present in the store. */\n has(segmentOrKey: AnySegment | string): boolean;\n /**\n * Whether the root reducer owns this key: a segment with a reducer cannot\n * be acquired under it.\n */\n isStatic(key: string): boolean;\n /**\n * Runs `fn`, deferring injections and evictions it causes until it returns,\n * so that they reach the store as a single action of each kind.\n */\n batch<T>(fn: () => T): T;\n /** Listens to segments being injected, going idle, revived and evicted. */\n subscribe(listener: (event: SparseEvent) => void): () => void;\n /** The keys of the present segments, by state. */\n stats(): SparseStats;\n}\n\nexport interface SparseStoreExt {\n readonly sparse: SparseController;\n}\n\n/**\n * Starts segment epics. `redux-sparse-kit/observable` provides one for\n * redux-observable.\n */\nexport interface EpicRunner {\n /** Starts `epic` and returns a function that stops it. */\n run(epic: SegmentEpic, segment: AnySegment): () => void;\n}\n\nexport interface SparseOptions {\n /** Required as soon as a held segment has epics. */\n epicRunner?: EpicRunner;\n /**\n * How long a segment nothing holds stays present before it is evicted, in\n * milliseconds. `0` evicts it right away, `Infinity` never. Segments can\n * override it. Defaults to one minute.\n */\n idleTimeout?: number;\n /**\n * What an idle segment does until it is evicted. Segments can override it.\n * Defaults to `keep-running`.\n */\n whileIdle?: WhileIdle;\n /**\n * How many idle segments may wait for their idle timeout at once. Past it,\n * the ones idle the longest are evicted right away. Defaults to `Infinity`.\n */\n maxIdle?: number;\n /**\n * Stops the idle clock while the page is hidden, so that time spent in a\n * background tab does not count towards the idle timeout. Defaults to `true`.\n */\n pauseWhenHidden?: boolean;\n}\n\ninterface Entry {\n readonly segment: AnySegment;\n /** Holds by `acquire` callers and by present segments that depend on it. */\n holders: number;\n /** Stops the segment's running epics. */\n stops: (() => void)[];\n /** Fires once the segment has been idle for its idle timeout. */\n timer: ReturnType<typeof setTimeout> | undefined;\n /** Idle time left before it expires; `Infinity` when it never does. */\n remaining: number;\n /** When the running timer was started. */\n startedAt: number;\n /** Idle for its whole idle timeout, so evicted by the next sweep. */\n expired: boolean;\n /** Idle with its epics stopped. */\n paused: boolean;\n /** Idle with its reducer skipped. */\n frozen: boolean;\n}\n\ntype AnyReducer = Reducer<any, Action, any>;\n\nconst emptyKeys: ReadonlySet<string> = new Set();\n\nconst defaultIdleTimeout = 60_000;\n\n// Reduced only by the root reducer, to learn the keys of the static state.\nconst probeActionType = '@@sparse/PROBE';\n\nconst whileIdleValues: readonly WhileIdle[] = ['keep-running', 'pause-epics', 'freeze'];\n\n// Longer delays overflow `setTimeout` and would fire right away.\nconst maxTimerDelay = 2 ** 31 - 1;\n\nconst scheduleIdle = (callback: () => void) => {\n if (typeof requestIdleCallback === 'function') {\n requestIdleCallback(callback, { timeout: 1000 });\n } else {\n setTimeout(callback, 0);\n }\n};\n\n/**\n * A store enhancer that lets segments be injected into and evicted from a\n * running store. The root reducer passed to `createStore` keeps owning the\n * static state; segment slices live next to it under their keys.\n */\nexport function sparseEnhancer(options: SparseOptions = {}): StoreEnhancer<SparseStoreExt> {\n const {\n epicRunner,\n idleTimeout = defaultIdleTimeout,\n whileIdle = 'keep-running',\n maxIdle = Infinity,\n pauseWhenHidden = true,\n } = options;\n\n const isHidden = () =>\n pauseWhenHidden && typeof document !== 'undefined' && document.visibilityState === 'hidden';\n\n if (!(idleTimeout >= 0)) {\n throw new RangeError('The idle timeout must be a non-negative number');\n }\n\n if (!(maxIdle >= 0)) {\n throw new RangeError('maxIdle must be a non-negative number');\n }\n\n if (!whileIdleValues.includes(whileIdle)) {\n throw new RangeError(`whileIdle must be one of ${whileIdleValues.join(', ')}`);\n }\n\n const enhancer =\n (createStore: StoreEnhancerStoreCreator) =>\n (rootReducer: AnyReducer, preloadedState?: unknown) => {\n const entries = new Map<string, Entry>();\n // Idle entries, the one idle the longest first.\n const idleEntries = new Set<Entry>();\n // Keys being removed by the EVICT action currently being reduced.\n let evictingKeys: ReadonlySet<string> = emptyKeys;\n\n // Injected segments that own a slice of the state, by key: only those\n // with a reducer. They keep reducing until the action that evicts them.\n const slices = new Map<string, Entry>();\n\n // Keys of the static state: whatever the root reducer returns. Never\n // treated as segment slices, even when a segment uses the same key.\n let staticKeys: ReadonlySet<string> = emptyKeys;\n\n const learnStaticKeys = (staticState: object) => {\n let count = 0;\n for (const key in staticState) {\n count++;\n if (!staticKeys.has(key)) {\n staticKeys = new Set(Object.keys(staticState));\n return;\n }\n }\n if (count !== staticKeys.size) {\n staticKeys = new Set(Object.keys(staticState));\n }\n };\n\n /** Asks a root reducer for its keys before it reduces a real state. */\n const probeStaticKeys = (root: AnyReducer) => {\n learnStaticKeys(root(undefined, { type: probeActionType }) as object);\n };\n\n const isDynamicKey = (key: string) =>\n !staticKeys.has(key) && (slices.has(key) || evictingKeys.has(key) || isSegmentKey(key));\n\n const wrapReducer =\n (root: AnyReducer): AnyReducer =>\n (state, action) => {\n const current = (state ?? {}) as Record<string, unknown>;\n\n let staticState: Record<string, unknown> | undefined;\n let hasDynamicKeys = false;\n\n for (const key in current) {\n if (isDynamicKey(key)) {\n hasDynamicKeys = true;\n break;\n }\n }\n\n // Slices of segments that are not present yet, e.g. from a preloaded\n // state: kept so that the segments start from them.\n const pendingSlices: string[] = [];\n\n if (hasDynamicKeys) {\n staticState = {};\n for (const key in current) {\n if (!isDynamicKey(key)) {\n staticState[key] = current[key];\n } else if (!slices.has(key) && !evictingKeys.has(key)) {\n pendingSlices.push(key);\n }\n }\n }\n\n const input = state === undefined ? undefined : (staticState ?? current);\n const nextStatic = root(input, action) as Record<string, unknown>;\n\n if (nextStatic !== input) {\n learnStaticKeys(nextStatic);\n }\n\n let changed = nextStatic !== input || evictingKeys.size > 0;\n const values: unknown[] = [];\n\n for (const [key, entry] of slices) {\n const previous = current[key];\n const value =\n entry.frozen && key in current ? previous : entry.segment.reducer!(previous, action);\n values.push(value);\n if (value !== previous || !(key in current)) {\n changed = true;\n }\n }\n\n if (!changed) {\n return current;\n }\n\n const next: Record<string, unknown> = { ...nextStatic };\n\n for (const key of pendingSlices) {\n next[key] = current[key];\n }\n\n let index = 0;\n for (const key of slices.keys()) {\n next[key] = values[index++];\n }\n\n return next;\n };\n\n probeStaticKeys(rootReducer);\n const store = createStore(wrapReducer(rootReducer), preloadedState);\n\n let batchDepth = 0;\n const pendingInject: Entry[] = [];\n const pendingEvict: Entry[] = [];\n const listeners = new Set<(event: SparseEvent) => void>();\n\n const emit = (event: SparseEvent) => {\n for (const listener of listeners) {\n listener(event);\n }\n };\n\n const dispatchSparse = (type: SparseAction['type'], keys: readonly string[]) => {\n const action: SparseAction = { type, payload: { keys } };\n store.dispatch(action);\n };\n\n const flush = () => {\n if (batchDepth > 0) {\n return;\n }\n\n if (pendingInject.length > 0) {\n const injected = pendingInject.splice(0);\n const keys = injected.map((entry) => entry.segment.key);\n const sliceKeys: string[] = [];\n for (const entry of injected) {\n if (entry.segment.reducer) {\n slices.set(entry.segment.key, entry);\n sliceKeys.push(entry.segment.key);\n }\n }\n if (sliceKeys.length > 0) {\n dispatchSparse(SparseActionTypes.INJECT, sliceKeys);\n }\n // Started once their slices are in the state, deps first.\n for (const entry of injected) {\n if (!entry.paused) {\n startEpics(entry);\n }\n }\n emit({ type: 'inject', keys });\n }\n\n if (pendingEvict.length > 0) {\n const evicted = pendingEvict.splice(0);\n const keys = evicted.map((entry) => entry.segment.key);\n // Stopped while their slices are still in the state, dependents first.\n for (const entry of evicted) {\n stopEpics(entry);\n }\n const sliceKeys = keys.filter((key) => slices.delete(key));\n if (sliceKeys.length > 0) {\n evictingKeys = new Set(sliceKeys);\n try {\n dispatchSparse(SparseActionTypes.EVICT, sliceKeys);\n } finally {\n evictingKeys = emptyKeys;\n }\n }\n emit({ type: 'evict', keys });\n }\n };\n\n const startEpics = (entry: Entry) => {\n for (const epic of entry.segment.epics) {\n entry.stops.push(epicRunner!.run(epic, entry.segment));\n }\n };\n\n const stopEpics = (entry: Entry) => {\n for (const stop of entry.stops.splice(0)) {\n stop();\n }\n };\n\n const batch = <T>(fn: () => T): T => {\n batchDepth++;\n try {\n return fn();\n } finally {\n batchDepth--;\n flush();\n }\n };\n\n const assertCompatible = (segment: AnySegment, seen: Map<string, AnySegment>) => {\n const other = seen.get(segment.key) ?? entries.get(segment.key)?.segment;\n\n if (other === segment) {\n return;\n }\n\n if (other !== undefined) {\n throw new Error(`Two different segments use the key \"${segment.key}\"`);\n }\n\n if (segment.reducer && staticKeys.has(segment.key)) {\n throw new Error(`Segment \"${segment.key}\" uses the key of a slice of the static state`);\n }\n\n if (segment.epics.length > 0 && epicRunner === undefined) {\n throw new Error(\n `Segment \"${segment.key}\" has epics, but sparseEnhancer was created without an epicRunner`,\n );\n }\n\n seen.set(segment.key, segment);\n\n for (const dep of segment.deps) {\n assertCompatible(dep, seen);\n }\n };\n\n const hold = (segment: AnySegment) => {\n const existing = entries.get(segment.key);\n\n if (existing) {\n if (existing.holders === 0) {\n revive(existing);\n }\n existing.holders++;\n return;\n }\n\n const evictIndex = pendingEvict.findIndex((entry) => entry.segment === segment);\n\n if (evictIndex !== -1) {\n // Released and held again before the eviction reached the store:\n // its slice is still there, so it only takes its holds back.\n const [entry] = pendingEvict.splice(evictIndex, 1);\n entry!.holders = 1;\n entries.set(segment.key, entry!);\n for (const dep of segment.deps) {\n hold(dep);\n }\n return;\n }\n\n const entry: Entry = {\n segment,\n holders: 1,\n stops: [],\n timer: undefined,\n remaining: Infinity,\n startedAt: 0,\n expired: false,\n paused: false,\n frozen: false,\n };\n entries.set(segment.key, entry);\n\n // Dependencies first, so they are injected before their dependents.\n for (const dep of segment.deps) {\n hold(dep);\n }\n\n pendingInject.push(entry);\n };\n\n /**\n * Drops a hold. A segment left without holders goes idle, unless it was\n * only held by a segment being evicted: then it is evicted with it.\n */\n const unhold = (segment: AnySegment, cascade = false) => {\n // Only reached for segments this store holds: a release undoes exactly\n // the holds its acquire took.\n const entry = entries.get(segment.key)!;\n\n entry.holders--;\n\n if (entry.holders > 0) {\n return;\n }\n\n const timeout = segment.idleTimeout ?? idleTimeout;\n\n if (cascade || timeout === 0) {\n evict(entry);\n } else {\n becomeIdle(entry, timeout);\n }\n };\n\n const becomeIdle = (entry: Entry, timeout: number) => {\n const policy = entry.segment.whileIdle ?? whileIdle;\n\n if (policy !== 'keep-running') {\n entry.paused = true;\n entry.frozen = policy === 'freeze';\n stopEpics(entry);\n }\n\n entry.remaining = timeout > maxTimerDelay ? Infinity : timeout;\n if (!isHidden()) {\n startTimer(entry);\n }\n idleEntries.add(entry);\n watchVisibility();\n emit({ type: 'idle', key: entry.segment.key });\n\n while (idleEntries.size > maxIdle) {\n evict(idleEntries.values().next().value!);\n }\n };\n\n const startTimer = (entry: Entry) => {\n if (entry.remaining !== Infinity) {\n entry.startedAt = Date.now();\n entry.timer = setTimeout(() => {\n entry.timer = undefined;\n entry.expired = true;\n scheduleSweep();\n }, entry.remaining);\n }\n };\n\n const pauseTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer !== undefined) {\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.remaining = Math.max(0, entry.remaining - (Date.now() - entry.startedAt));\n }\n }\n };\n\n const resumeTimers = () => {\n for (const entry of idleEntries) {\n if (entry.timer === undefined && !entry.expired) {\n startTimer(entry);\n }\n }\n };\n\n const onVisibilityChange = () => {\n if (isHidden()) {\n pauseTimers();\n } else {\n resumeTimers();\n }\n };\n\n let watchingVisibility = false;\n\n /** Listens to the page visibility only while there are idle segments. */\n const watchVisibility = () => {\n const watch = idleEntries.size > 0;\n\n if (watch !== watchingVisibility && pauseWhenHidden && typeof document !== 'undefined') {\n watchingVisibility = watch;\n if (watch) {\n document.addEventListener('visibilitychange', onVisibilityChange);\n } else {\n document.removeEventListener('visibilitychange', onVisibilityChange);\n }\n }\n };\n\n const revive = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entry.timer = undefined;\n entry.expired = false;\n entry.frozen = false;\n\n if (entry.paused) {\n entry.paused = false;\n // Not injected yet: its epics start once it is.\n if (!pendingInject.includes(entry)) {\n startEpics(entry);\n }\n }\n\n emit({ type: 'revive', key: entry.segment.key });\n };\n\n const evict = (entry: Entry) => {\n idleEntries.delete(entry);\n watchVisibility();\n clearTimeout(entry.timer);\n entries.delete(entry.segment.key);\n\n const pendingIndex = pendingInject.indexOf(entry);\n if (pendingIndex !== -1) {\n // Released before it reached the store: nothing to evict.\n pendingInject.splice(pendingIndex, 1);\n } else {\n pendingEvict.push(entry);\n }\n\n // Dependents first, so they are evicted before their dependencies.\n for (const dep of entry.segment.deps) {\n unhold(dep, true);\n }\n };\n\n let sweepScheduled = false;\n\n /** Evicts every expired segment with a single action. */\n const sweep = () => {\n sweepScheduled = false;\n batch(() => {\n for (const entry of entries.values()) {\n if (entry.expired) {\n evict(entry);\n }\n }\n });\n };\n\n const scheduleSweep = () => {\n if (!sweepScheduled) {\n sweepScheduled = true;\n scheduleIdle(sweep);\n }\n };\n\n const sparse: SparseController = {\n acquire(...segments) {\n // Checked up front so that a refused acquire leaves no holds behind.\n const seen = new Map<string, AnySegment>();\n for (const segment of segments) {\n assertCompatible(segment, seen);\n }\n\n batch(() => {\n for (const segment of segments) {\n hold(segment);\n }\n });\n\n let released = false;\n\n return () => {\n if (released) {\n return;\n }\n released = true;\n\n batch(() => {\n for (const segment of segments) {\n unhold(segment);\n }\n });\n };\n },\n\n has(segmentOrKey) {\n const key = typeof segmentOrKey === 'string' ? segmentOrKey : segmentOrKey.key;\n return entries.has(key) && !pendingInject.includes(entries.get(key)!);\n },\n\n isStatic(key) {\n return staticKeys.has(key);\n },\n\n stats() {\n const stats: SparseStats = { active: [], idle: [] };\n for (const [key, entry] of entries) {\n if (!pendingInject.includes(entry)) {\n (entry.holders > 0 ? stats.active : stats.idle).push(key);\n }\n }\n return stats;\n },\n\n batch,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n\n return {\n ...store,\n replaceReducer(nextReducer: AnyReducer) {\n probeStaticKeys(nextReducer);\n store.replaceReducer(wrapReducer(nextReducer));\n },\n sparse,\n };\n };\n\n return enhancer as StoreEnhancer<SparseStoreExt>;\n}\n"],"mappings":"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"}
|
package/dist/observable.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as EpicRunner } from "./sparseEnhancer-
|
|
1
|
+
import { t as EpicRunner } from "./sparseEnhancer-BNdAfdGG.mjs";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
//#region src/observable/createEpicRunner.d.ts
|
|
4
4
|
type AnyEpic = (action$: any, state$: any, dependencies: any) => Observable<unknown>;
|
package/dist/observable.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as EpicRunner } from "./sparseEnhancer-
|
|
1
|
+
import { t as EpicRunner } from "./sparseEnhancer-BNdAfdGG.js";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
//#region src/observable/createEpicRunner.d.ts
|
|
4
4
|
type AnyEpic = (action$: any, state$: any, dependencies: any) => Observable<unknown>;
|
package/dist/react.d.mts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import { c as SparseStoreExt, u as AnySegment } from "./sparseEnhancer-
|
|
1
|
+
import { c as SparseStoreExt, p as SegmentEpic, u as AnySegment, v as WhileIdle } from "./sparseEnhancer-BNdAfdGG.mjs";
|
|
2
2
|
import { Context, ReactNode } from "react";
|
|
3
|
-
import { Store } from "redux";
|
|
3
|
+
import { Reducer, Store } from "redux";
|
|
4
|
+
//#region src/react/injectedSegments.d.ts
|
|
5
|
+
interface InjectOptions {
|
|
6
|
+
/** The idle timeout of the segment made for the injection. */
|
|
7
|
+
idleTimeout?: number;
|
|
8
|
+
/** What the segment made for the injection does while idle. */
|
|
9
|
+
whileIdle?: WhileIdle;
|
|
10
|
+
}
|
|
11
|
+
interface InjectReducerOptions<State = any> extends InjectOptions {
|
|
12
|
+
key: string;
|
|
13
|
+
reducer: Reducer<State>;
|
|
14
|
+
}
|
|
15
|
+
interface InjectEpicOptions extends InjectOptions {
|
|
16
|
+
/** Nothing is injected while it is `undefined`. */
|
|
17
|
+
epic: SegmentEpic | undefined;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
4
20
|
//#region src/react/reactReduxContext.d.ts
|
|
5
21
|
/** The parts of a react-redux context value this package relies on. */
|
|
6
22
|
interface ReactReduxContextValueLike {
|
|
@@ -69,6 +85,20 @@ interface SparseReact {
|
|
|
69
85
|
* no segments checks the reads below it that no scope holds at all.
|
|
70
86
|
*/
|
|
71
87
|
SparseScope(props: SparseScopeProps): ReactNode;
|
|
88
|
+
/**
|
|
89
|
+
* Injects a reducer under a key for as long as the calling component is
|
|
90
|
+
* mounted, and returns whether its slice is present yet. The segment is made
|
|
91
|
+
* by the first injection under the key, with its reducer and options, and
|
|
92
|
+
* shared by every later one. A key of the static state is left to the root
|
|
93
|
+
* reducer.
|
|
94
|
+
*/
|
|
95
|
+
useInjectReducer<State>(options: InjectReducerOptions<State>): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Runs an epic for as long as the calling component is mounted, once
|
|
98
|
+
* however many components inject it. The segment that runs it is made by its
|
|
99
|
+
* first injection, with its options.
|
|
100
|
+
*/
|
|
101
|
+
useInjectEpic(options: InjectEpicOptions): boolean;
|
|
72
102
|
}
|
|
73
103
|
/** Creates the React bindings for stores made with `sparseEnhancer`. */
|
|
74
104
|
declare function createSparseReact(options?: SparseReactOptions): SparseReact;
|
|
@@ -78,6 +108,10 @@ declare function createSparseReact(options?: SparseReactOptions): SparseReact;
|
|
|
78
108
|
declare const SparseScope: SparseReact["SparseScope"];
|
|
79
109
|
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
80
110
|
declare const useSparseSegments: SparseReact["useSparseSegments"];
|
|
111
|
+
/** {@inheritDoc SparseReact.useInjectReducer} */
|
|
112
|
+
declare const useInjectReducer: SparseReact["useInjectReducer"];
|
|
113
|
+
/** {@inheritDoc SparseReact.useInjectEpic} */
|
|
114
|
+
declare const useInjectEpic: SparseReact["useInjectEpic"];
|
|
81
115
|
//#endregion
|
|
82
|
-
export { type ReactReduxContextLike, type ReactReduxContextValueLike, type ScopeViolation, type ScopeViolationHandler, type SparseReact, type SparseReactOptions, SparseScope, type SparseScopeProps, createSparseReact, getReactReduxContext, useSparseSegments };
|
|
116
|
+
export { type InjectEpicOptions, type InjectOptions, type InjectReducerOptions, type ReactReduxContextLike, type ReactReduxContextValueLike, type ScopeViolation, type ScopeViolationHandler, type SparseReact, type SparseReactOptions, SparseScope, type SparseScopeProps, createSparseReact, getReactReduxContext, useInjectEpic, useInjectReducer, useSparseSegments };
|
|
83
117
|
//# sourceMappingURL=react.d.mts.map
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import { c as SparseStoreExt, u as AnySegment } from "./sparseEnhancer-
|
|
2
|
-
import { Store } from "redux";
|
|
1
|
+
import { c as SparseStoreExt, p as SegmentEpic, u as AnySegment, v as WhileIdle } from "./sparseEnhancer-BNdAfdGG.js";
|
|
2
|
+
import { Reducer, Store } from "redux";
|
|
3
3
|
import { Context, ReactNode } from "react";
|
|
4
|
+
//#region src/react/injectedSegments.d.ts
|
|
5
|
+
interface InjectOptions {
|
|
6
|
+
/** The idle timeout of the segment made for the injection. */
|
|
7
|
+
idleTimeout?: number;
|
|
8
|
+
/** What the segment made for the injection does while idle. */
|
|
9
|
+
whileIdle?: WhileIdle;
|
|
10
|
+
}
|
|
11
|
+
interface InjectReducerOptions<State = any> extends InjectOptions {
|
|
12
|
+
key: string;
|
|
13
|
+
reducer: Reducer<State>;
|
|
14
|
+
}
|
|
15
|
+
interface InjectEpicOptions extends InjectOptions {
|
|
16
|
+
/** Nothing is injected while it is `undefined`. */
|
|
17
|
+
epic: SegmentEpic | undefined;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
4
20
|
//#region src/react/reactReduxContext.d.ts
|
|
5
21
|
/** The parts of a react-redux context value this package relies on. */
|
|
6
22
|
interface ReactReduxContextValueLike {
|
|
@@ -69,6 +85,20 @@ interface SparseReact {
|
|
|
69
85
|
* no segments checks the reads below it that no scope holds at all.
|
|
70
86
|
*/
|
|
71
87
|
SparseScope(props: SparseScopeProps): ReactNode;
|
|
88
|
+
/**
|
|
89
|
+
* Injects a reducer under a key for as long as the calling component is
|
|
90
|
+
* mounted, and returns whether its slice is present yet. The segment is made
|
|
91
|
+
* by the first injection under the key, with its reducer and options, and
|
|
92
|
+
* shared by every later one. A key of the static state is left to the root
|
|
93
|
+
* reducer.
|
|
94
|
+
*/
|
|
95
|
+
useInjectReducer<State>(options: InjectReducerOptions<State>): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Runs an epic for as long as the calling component is mounted, once
|
|
98
|
+
* however many components inject it. The segment that runs it is made by its
|
|
99
|
+
* first injection, with its options.
|
|
100
|
+
*/
|
|
101
|
+
useInjectEpic(options: InjectEpicOptions): boolean;
|
|
72
102
|
}
|
|
73
103
|
/** Creates the React bindings for stores made with `sparseEnhancer`. */
|
|
74
104
|
declare function createSparseReact(options?: SparseReactOptions): SparseReact;
|
|
@@ -78,6 +108,10 @@ declare function createSparseReact(options?: SparseReactOptions): SparseReact;
|
|
|
78
108
|
declare const SparseScope: SparseReact["SparseScope"];
|
|
79
109
|
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
80
110
|
declare const useSparseSegments: SparseReact["useSparseSegments"];
|
|
111
|
+
/** {@inheritDoc SparseReact.useInjectReducer} */
|
|
112
|
+
declare const useInjectReducer: SparseReact["useInjectReducer"];
|
|
113
|
+
/** {@inheritDoc SparseReact.useInjectEpic} */
|
|
114
|
+
declare const useInjectEpic: SparseReact["useInjectEpic"];
|
|
81
115
|
//#endregion
|
|
82
|
-
export { type ReactReduxContextLike, type ReactReduxContextValueLike, type ScopeViolation, type ScopeViolationHandler, type SparseReact, type SparseReactOptions, SparseScope, type SparseScopeProps, createSparseReact, getReactReduxContext, useSparseSegments };
|
|
116
|
+
export { type InjectEpicOptions, type InjectOptions, type InjectReducerOptions, type ReactReduxContextLike, type ReactReduxContextValueLike, type ScopeViolation, type ScopeViolationHandler, type SparseReact, type SparseReactOptions, SparseScope, type SparseScopeProps, createSparseReact, getReactReduxContext, useInjectEpic, useInjectReducer, useSparseSegments };
|
|
83
117
|
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.js
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_segment = require("./segment-Dqd6-ABS.js");
|
|
3
3
|
let react = require("react");
|
|
4
|
+
//#region src/react/injectedSegments.ts
|
|
5
|
+
const reducerSegments = /* @__PURE__ */ new Map();
|
|
6
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
7
|
+
/**
|
|
8
|
+
* The segment injected under a key: made by the first injection, with its
|
|
9
|
+
* reducer and options, and shared by every later one.
|
|
10
|
+
*/
|
|
11
|
+
const getReducerSegment = ({ key, reducer, idleTimeout, whileIdle }) => {
|
|
12
|
+
const existing = reducerSegments.get(key);
|
|
13
|
+
if (existing) {
|
|
14
|
+
if (existing.reducer !== reducer && process.env.NODE_ENV !== "production" && !warnedKeys.has(key)) {
|
|
15
|
+
warnedKeys.add(key);
|
|
16
|
+
console.warn(`redux-sparse-kit: "${key}" is injected with another reducer than before; the first one is kept.`);
|
|
17
|
+
}
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
const segment = require_segment.defineSegment({
|
|
21
|
+
key,
|
|
22
|
+
reducer,
|
|
23
|
+
idleTimeout,
|
|
24
|
+
whileIdle
|
|
25
|
+
});
|
|
26
|
+
reducerSegments.set(key, segment);
|
|
27
|
+
return segment;
|
|
28
|
+
};
|
|
29
|
+
const epicSegments = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
let nextEpicId = 0;
|
|
31
|
+
/** The segment that runs an epic: one per epic, made by its first injection. */
|
|
32
|
+
const getEpicSegment = (epic, { idleTimeout, whileIdle }) => {
|
|
33
|
+
let segment = epicSegments.get(epic);
|
|
34
|
+
if (!segment) {
|
|
35
|
+
segment = require_segment.defineSegment({
|
|
36
|
+
key: `@@epic/${epic.name || "anonymous"}#${nextEpicId++}`,
|
|
37
|
+
epics: [epic],
|
|
38
|
+
idleTimeout,
|
|
39
|
+
whileIdle
|
|
40
|
+
});
|
|
41
|
+
epicSegments.set(epic, segment);
|
|
42
|
+
}
|
|
43
|
+
return segment;
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
4
46
|
//#region src/react/reactReduxContext.ts
|
|
5
47
|
const contextKey = Symbol.for("react-redux-context");
|
|
6
48
|
/**
|
|
@@ -150,6 +192,12 @@ function createSparseReact(options = {}) {
|
|
|
150
192
|
}, [sparse, stable]);
|
|
151
193
|
return ready;
|
|
152
194
|
};
|
|
195
|
+
const noSegments = [];
|
|
196
|
+
const useInjectReducer = (injection) => {
|
|
197
|
+
const sparse = useSparseController();
|
|
198
|
+
return useSparseSegments(sparse.isStatic(injection.key) ? noSegments : [getReducerSegment(injection)]);
|
|
199
|
+
};
|
|
200
|
+
const useInjectEpic = ({ epic, ...injection }) => useSparseSegments(epic ? [getEpicSegment(epic, injection)] : noSegments);
|
|
153
201
|
function SparseScope({ segments, fallback = null, children }) {
|
|
154
202
|
return useSparseSegments(segments) ? children : fallback;
|
|
155
203
|
}
|
|
@@ -170,7 +218,9 @@ function createSparseReact(options = {}) {
|
|
|
170
218
|
}
|
|
171
219
|
return {
|
|
172
220
|
useSparseSegments,
|
|
173
|
-
SparseScope: checks ? CheckedSparseScope : SparseScope
|
|
221
|
+
SparseScope: checks ? CheckedSparseScope : SparseScope,
|
|
222
|
+
useInjectReducer,
|
|
223
|
+
useInjectEpic
|
|
174
224
|
};
|
|
175
225
|
}
|
|
176
226
|
//#endregion
|
|
@@ -180,10 +230,16 @@ const defaults = createSparseReact();
|
|
|
180
230
|
const SparseScope = defaults.SparseScope;
|
|
181
231
|
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
182
232
|
const useSparseSegments = defaults.useSparseSegments;
|
|
233
|
+
/** {@inheritDoc SparseReact.useInjectReducer} */
|
|
234
|
+
const useInjectReducer = defaults.useInjectReducer;
|
|
235
|
+
/** {@inheritDoc SparseReact.useInjectEpic} */
|
|
236
|
+
const useInjectEpic = defaults.useInjectEpic;
|
|
183
237
|
//#endregion
|
|
184
238
|
exports.SparseScope = SparseScope;
|
|
185
239
|
exports.createSparseReact = createSparseReact;
|
|
186
240
|
exports.getReactReduxContext = getReactReduxContext;
|
|
241
|
+
exports.useInjectEpic = useInjectEpic;
|
|
242
|
+
exports.useInjectReducer = useInjectReducer;
|
|
187
243
|
exports.useSparseSegments = useSparseSegments;
|
|
188
244
|
|
|
189
245
|
//# sourceMappingURL=react.js.map
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","names":["createContext","createContext","isSegmentKey","useEffect","useLayoutEffect","useMemo","useContext","useReducer","createElement"],"sources":["../src/react/reactReduxContext.ts","../src/react/scopeChecks.ts","../src/react/createSparseReact.ts","../src/react/index.ts"],"sourcesContent":["import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport type { SparseStoreExt } from '../sparseEnhancer';\n\n/** The parts of a react-redux context value this package relies on. */\nexport interface ReactReduxContextValueLike {\n store: Store & Partial<SparseStoreExt>;\n [key: string]: unknown;\n}\n\nexport type ReactReduxContextLike = Context<ReactReduxContextValueLike | null>;\n\nconst contextKey = Symbol.for('react-redux-context');\n\ntype ContextRegistry = Map<typeof createContext, ReactReduxContextLike>;\n\n/**\n * The context react-redux's `<Provider>` fills. React-redux keeps it in a\n * registry on `globalThis`, keyed by the React instance, so that every copy\n * of react-redux shares it; reading the same registry finds it without a\n * dependency on any particular build of react-redux.\n */\nexport function getReactReduxContext(): ReactReduxContextLike {\n const registry = globalThis as { [contextKey]?: ContextRegistry };\n const contexts = (registry[contextKey] ??= new Map());\n let context = contexts.get(createContext);\n\n if (!context) {\n context = createContext<ReactReduxContextValueLike | null>(null);\n context.displayName = 'ReactRedux';\n contexts.set(createContext, context);\n }\n\n return context;\n}\n","import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport { isSegmentKey } from '../segment';\nimport type { ReactReduxContextValueLike } from './reactReduxContext';\n\n/** A read of a segment's slice made outside every scope that holds it. */\nexport interface ScopeViolation {\n /** The key of the segment slice that was read. */\n key: string;\n /**\n * The selector that read it, when react-redux reports reads through\n * `onStateAccess`; otherwise `undefined`.\n */\n selector: Function | undefined;\n /** The segment keys available where it was read. */\n scope: readonly string[];\n}\n\nexport type ScopeViolationHandler = (violation: ScopeViolation) => void;\n\ntype StateAccessListener = (key: string, selector: Function) => void;\n\n/** The segment keys the enclosing scopes hold, with their dependencies. */\nexport const ScopeKeysContext: Context<ReadonlySet<string>> = createContext<ReadonlySet<string>>(\n new Set(),\n);\n\n// The listener a scope found in place, so that nested scopes call it instead\n// of the listener of the scope around them.\nconst baseListenerKey: unique symbol = Symbol('redux-sparse-kit.baseListener');\n\n// The store a scoped store wraps, so that nested scopes wrap the original\n// store rather than the scoped store of the scope around them.\nconst baseStoreKey: unique symbol = Symbol('redux-sparse-kit.baseStore');\n\ninterface CheckedContextValue extends ReactReduxContextValueLike {\n [baseListenerKey]?: StateAccessListener | undefined;\n}\n\ntype ScopedStore = Store & { [baseStoreKey]?: Store };\n\ntype Reported = Map<Function | undefined, Set<string>>;\n\n/** Whether this selector and key were seen before; records them if not. */\nconst seenBefore = (reported: Reported, selector: Function | undefined, key: string) => {\n let keys = reported.get(selector);\n if (!keys) {\n keys = new Set();\n reported.set(selector, keys);\n }\n\n if (keys.has(key)) {\n return true;\n }\n keys.add(key);\n return false;\n};\n\nconst reportedByDefault: Reported = new Map();\n\n/** Logs each key once per selector, across all scopes. */\nexport const reportViolationOnce: ScopeViolationHandler = ({ key, selector, scope }) => {\n if (seenBefore(reportedByDefault, selector, key)) {\n return;\n }\n\n const reader = selector ? `Selector \"${selector.name || 'anonymous'}\"` : 'A selector';\n const available = scope.length > 0 ? scope.map((name) => `\"${name}\"`).join(', ') : 'none';\n\n console.error(\n `redux-sparse-kit: ${reader} read state.${key} outside every SparseScope holding \"${key}\" ` +\n `(segments available here: ${available}).`,\n );\n};\n\nconst createScopedStore = (parent: ScopedStore, check: (key: string) => void): ScopedStore => {\n const store = parent[baseStoreKey] ?? parent;\n const proxies = new WeakMap<object, object>();\n\n return {\n ...store,\n [baseStoreKey]: store,\n getState() {\n // Always an object: the root state of a store made with sparseEnhancer.\n const state = store.getState() as object;\n\n let proxy = proxies.get(state);\n if (!proxy) {\n proxy = new Proxy(state, {\n get(target, property, receiver) {\n if (typeof property === 'string') {\n check(property);\n }\n return Reflect.get(target, property, receiver);\n },\n });\n proxies.set(state, proxy);\n }\n return proxy;\n },\n };\n};\n\n/**\n * The react-redux context value a scope passes down: it checks every state\n * read below it against the segment keys available there.\n */\nexport function createCheckedContextValue(\n parent: CheckedContextValue,\n available: ReadonlySet<string>,\n report: ScopeViolationHandler,\n): CheckedContextValue {\n const reported: Reported = new Map();\n\n // Each scope reports a selector reading a key once, however often it runs.\n const check = (key: string, selector: Function | undefined) => {\n if (isSegmentKey(key) && !available.has(key) && !seenBefore(reported, selector, key)) {\n report({ key, selector, scope: [...available] });\n }\n };\n\n if (parent.stateAccessTracking === true) {\n const base =\n baseListenerKey in parent\n ? parent[baseListenerKey]\n : (parent.onStateAccess as StateAccessListener | undefined);\n\n const onStateAccess: StateAccessListener = (key, selector) => {\n check(key, selector);\n base?.(key, selector);\n };\n\n return { ...parent, onStateAccess, [baseListenerKey]: base };\n }\n\n return {\n ...parent,\n store: createScopedStore(parent.store, (key) => check(key, undefined)),\n };\n}\n","import type { ReactNode } from 'react';\nimport { createElement, useContext, useEffect, useLayoutEffect, useMemo, useReducer } from 'react';\nimport type { AnySegment } from '../segment';\nimport type { SparseController } from '../sparseEnhancer';\nimport type { ReactReduxContextLike } from './reactReduxContext';\nimport { getReactReduxContext } from './reactReduxContext';\nimport type { ScopeViolationHandler } from './scopeChecks';\nimport { createCheckedContextValue, reportViolationOnce, ScopeKeysContext } from './scopeChecks';\n\n// Layout effects do not run on the server, and React warns about them there.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\nconst segmentIds = new WeakMap<AnySegment, number>();\nlet nextSegmentId = 0;\n\n/** Identifies a list of segments by the identity of its members. */\nconst signatureOf = (segments: readonly AnySegment[]) =>\n segments\n .map((segment) => {\n let id = segmentIds.get(segment);\n if (id === undefined) {\n id = nextSegmentId++;\n segmentIds.set(segment, id);\n }\n return id;\n })\n .join(',');\n\n/**\n * The same array for as long as it holds the same segments, so that a new\n * array literal on every render does not acquire them again.\n */\nconst useStableSegments = (segments: readonly AnySegment[]) => {\n const signature = signatureOf(segments);\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- tracked by their signature\n return useMemo(() => segments, [signature]);\n};\n\nconst collectKeys = (segments: readonly AnySegment[], keys: Set<string>) => {\n for (const segment of segments) {\n keys.add(segment.key);\n collectKeys(segment.deps, keys);\n }\n};\n\nexport interface SparseReactOptions {\n /**\n * The context the react-redux `<Provider>` was given. Defaults to\n * react-redux's own context.\n */\n context?: ReactReduxContextLike;\n /**\n * Checks that components read segment slices only inside a scope that holds\n * them. Defaults to `true` outside production builds.\n */\n checks?: boolean;\n /**\n * Called for each read of a segment slice outside the scopes that hold it.\n * Defaults to logging each key once per selector with `console.error`.\n */\n onViolation?: ScopeViolationHandler;\n}\n\nexport interface SparseScopeProps {\n /** Segments held for as long as the scope is mounted. */\n segments: readonly AnySegment[];\n /** Rendered until the segments are present in the store. */\n fallback?: ReactNode;\n children?: ReactNode;\n}\n\nexport interface SparseReact {\n /**\n * Holds `segments` for as long as the calling component is mounted and\n * returns whether they are all present in the store yet.\n */\n useSparseSegments(segments: readonly AnySegment[]): boolean;\n /**\n * Holds `segments` for as long as it is mounted, and renders its children\n * only once they are present in the store. With checks on, reads of segment\n * slices below it that no enclosing scope holds are reported; a scope with\n * no segments checks the reads below it that no scope holds at all.\n */\n SparseScope(props: SparseScopeProps): ReactNode;\n}\n\n/** Creates the React bindings for stores made with `sparseEnhancer`. */\nexport function createSparseReact(options: SparseReactOptions = {}): SparseReact {\n const { checks = process.env.NODE_ENV !== 'production', onViolation = reportViolationOnce } =\n options;\n\n const getContext = () => options.context ?? getReactReduxContext();\n\n const useSparseController = (): SparseController => {\n const value = useContext(getContext());\n\n if (!value) {\n throw new Error('redux-sparse-kit needs a react-redux <Provider> above this component');\n }\n\n if (!value.store.sparse) {\n throw new Error('redux-sparse-kit needs a store created with sparseEnhancer');\n }\n\n return value.store.sparse;\n };\n\n const useSparseSegments = (segments: readonly AnySegment[]): boolean => {\n const sparse = useSparseController();\n const [, rerender] = useReducer((count: number) => count + 1, 0);\n const ready = segments.every((segment) => sparse.has(segment));\n const stable = useStableSegments(segments);\n\n useIsomorphicLayoutEffect(() => {\n const release = sparse.acquire(...stable);\n if (!ready) {\n // Rendered before they were injected: render again, before paint.\n rerender();\n }\n return release;\n }, [sparse, stable]);\n\n return ready;\n };\n\n function SparseScope({ segments, fallback = null, children }: SparseScopeProps): ReactNode {\n return useSparseSegments(segments) ? children : fallback;\n }\n\n function CheckedSparseScope({\n segments,\n fallback = null,\n children,\n }: SparseScopeProps): ReactNode {\n const ready = useSparseSegments(segments);\n const context = getContext();\n const parent = useContext(context)!;\n const parentKeys = useContext(ScopeKeysContext);\n const stable = useStableSegments(segments);\n\n const available = useMemo(() => {\n const keys = new Set(parentKeys);\n collectKeys(stable, keys);\n return keys;\n }, [parentKeys, stable]);\n\n const value = useMemo(\n () => createCheckedContextValue(parent, available, onViolation),\n [parent, available],\n );\n\n if (!ready) {\n return fallback;\n }\n\n return createElement(\n ScopeKeysContext.Provider,\n { value: available },\n createElement(context.Provider, { value }, children),\n );\n }\n\n return { useSparseSegments, SparseScope: checks ? CheckedSparseScope : SparseScope };\n}\n","import { createSparseReact } from './createSparseReact';\nimport type { SparseReact } from './createSparseReact';\n\nconst defaults: SparseReact = createSparseReact();\n\n/** {@inheritDoc SparseReact.SparseScope} */\nexport const SparseScope: SparseReact['SparseScope'] = defaults.SparseScope;\n\n/** {@inheritDoc SparseReact.useSparseSegments} */\nexport const useSparseSegments: SparseReact['useSparseSegments'] = defaults.useSparseSegments;\n\nexport { createSparseReact } from './createSparseReact';\nexport type { SparseReact, SparseReactOptions, SparseScopeProps } from './createSparseReact';\nexport { getReactReduxContext } from './reactReduxContext';\nexport type { ScopeViolation, ScopeViolationHandler } from './scopeChecks';\nexport type { ReactReduxContextLike, ReactReduxContextValueLike } from './reactReduxContext';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react.js","names":["defineSegment","createContext","createContext","isSegmentKey","useEffect","useLayoutEffect","useMemo","useContext","useReducer","createElement"],"sources":["../src/react/injectedSegments.ts","../src/react/reactReduxContext.ts","../src/react/scopeChecks.ts","../src/react/createSparseReact.ts","../src/react/index.ts"],"sourcesContent":["import type { Reducer } from 'redux';\nimport type { AnySegment, SegmentEpic, WhileIdle } from '../segment';\nimport { defineSegment } from '../segment';\n\nexport interface InjectOptions {\n /** The idle timeout of the segment made for the injection. */\n idleTimeout?: number;\n /** What the segment made for the injection does while idle. */\n whileIdle?: WhileIdle;\n}\n\nexport interface InjectReducerOptions<State = any> extends InjectOptions {\n key: string;\n reducer: Reducer<State>;\n}\n\nexport interface InjectEpicOptions extends InjectOptions {\n /** Nothing is injected while it is `undefined`. */\n epic: SegmentEpic | undefined;\n}\n\nconst reducerSegments = new Map<string, AnySegment>();\nconst warnedKeys = new Set<string>();\n\n/**\n * The segment injected under a key: made by the first injection, with its\n * reducer and options, and shared by every later one.\n */\nexport const getReducerSegment = ({\n key,\n reducer,\n idleTimeout,\n whileIdle,\n}: InjectReducerOptions): AnySegment => {\n const existing = reducerSegments.get(key);\n\n if (existing) {\n if (\n existing.reducer !== reducer &&\n process.env.NODE_ENV !== 'production' &&\n !warnedKeys.has(key)\n ) {\n warnedKeys.add(key);\n console.warn(\n `redux-sparse-kit: \"${key}\" is injected with another reducer than before; the first one is kept.`,\n );\n }\n return existing;\n }\n\n const segment = defineSegment({ key, reducer, idleTimeout, whileIdle });\n reducerSegments.set(key, segment);\n return segment;\n};\n\nconst epicSegments = new WeakMap<SegmentEpic, AnySegment>();\nlet nextEpicId = 0;\n\n/** The segment that runs an epic: one per epic, made by its first injection. */\nexport const getEpicSegment = (\n epic: SegmentEpic,\n { idleTimeout, whileIdle }: InjectOptions,\n): AnySegment => {\n let segment = epicSegments.get(epic);\n\n if (!segment) {\n segment = defineSegment({\n key: `@@epic/${epic.name || 'anonymous'}#${nextEpicId++}`,\n epics: [epic],\n idleTimeout,\n whileIdle,\n });\n epicSegments.set(epic, segment);\n }\n\n return segment;\n};\n","import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport type { SparseStoreExt } from '../sparseEnhancer';\n\n/** The parts of a react-redux context value this package relies on. */\nexport interface ReactReduxContextValueLike {\n store: Store & Partial<SparseStoreExt>;\n [key: string]: unknown;\n}\n\nexport type ReactReduxContextLike = Context<ReactReduxContextValueLike | null>;\n\nconst contextKey = Symbol.for('react-redux-context');\n\ntype ContextRegistry = Map<typeof createContext, ReactReduxContextLike>;\n\n/**\n * The context react-redux's `<Provider>` fills. React-redux keeps it in a\n * registry on `globalThis`, keyed by the React instance, so that every copy\n * of react-redux shares it; reading the same registry finds it without a\n * dependency on any particular build of react-redux.\n */\nexport function getReactReduxContext(): ReactReduxContextLike {\n const registry = globalThis as { [contextKey]?: ContextRegistry };\n const contexts = (registry[contextKey] ??= new Map());\n let context = contexts.get(createContext);\n\n if (!context) {\n context = createContext<ReactReduxContextValueLike | null>(null);\n context.displayName = 'ReactRedux';\n contexts.set(createContext, context);\n }\n\n return context;\n}\n","import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport { isSegmentKey } from '../segment';\nimport type { ReactReduxContextValueLike } from './reactReduxContext';\n\n/** A read of a segment's slice made outside every scope that holds it. */\nexport interface ScopeViolation {\n /** The key of the segment slice that was read. */\n key: string;\n /**\n * The selector that read it, when react-redux reports reads through\n * `onStateAccess`; otherwise `undefined`.\n */\n selector: Function | undefined;\n /** The segment keys available where it was read. */\n scope: readonly string[];\n}\n\nexport type ScopeViolationHandler = (violation: ScopeViolation) => void;\n\ntype StateAccessListener = (key: string, selector: Function) => void;\n\n/** The segment keys the enclosing scopes hold, with their dependencies. */\nexport const ScopeKeysContext: Context<ReadonlySet<string>> = createContext<ReadonlySet<string>>(\n new Set(),\n);\n\n// The listener a scope found in place, so that nested scopes call it instead\n// of the listener of the scope around them.\nconst baseListenerKey: unique symbol = Symbol('redux-sparse-kit.baseListener');\n\n// The store a scoped store wraps, so that nested scopes wrap the original\n// store rather than the scoped store of the scope around them.\nconst baseStoreKey: unique symbol = Symbol('redux-sparse-kit.baseStore');\n\ninterface CheckedContextValue extends ReactReduxContextValueLike {\n [baseListenerKey]?: StateAccessListener | undefined;\n}\n\ntype ScopedStore = Store & { [baseStoreKey]?: Store };\n\ntype Reported = Map<Function | undefined, Set<string>>;\n\n/** Whether this selector and key were seen before; records them if not. */\nconst seenBefore = (reported: Reported, selector: Function | undefined, key: string) => {\n let keys = reported.get(selector);\n if (!keys) {\n keys = new Set();\n reported.set(selector, keys);\n }\n\n if (keys.has(key)) {\n return true;\n }\n keys.add(key);\n return false;\n};\n\nconst reportedByDefault: Reported = new Map();\n\n/** Logs each key once per selector, across all scopes. */\nexport const reportViolationOnce: ScopeViolationHandler = ({ key, selector, scope }) => {\n if (seenBefore(reportedByDefault, selector, key)) {\n return;\n }\n\n const reader = selector ? `Selector \"${selector.name || 'anonymous'}\"` : 'A selector';\n const available = scope.length > 0 ? scope.map((name) => `\"${name}\"`).join(', ') : 'none';\n\n console.error(\n `redux-sparse-kit: ${reader} read state.${key} outside every SparseScope holding \"${key}\" ` +\n `(segments available here: ${available}).`,\n );\n};\n\nconst createScopedStore = (parent: ScopedStore, check: (key: string) => void): ScopedStore => {\n const store = parent[baseStoreKey] ?? parent;\n const proxies = new WeakMap<object, object>();\n\n return {\n ...store,\n [baseStoreKey]: store,\n getState() {\n // Always an object: the root state of a store made with sparseEnhancer.\n const state = store.getState() as object;\n\n let proxy = proxies.get(state);\n if (!proxy) {\n proxy = new Proxy(state, {\n get(target, property, receiver) {\n if (typeof property === 'string') {\n check(property);\n }\n return Reflect.get(target, property, receiver);\n },\n });\n proxies.set(state, proxy);\n }\n return proxy;\n },\n };\n};\n\n/**\n * The react-redux context value a scope passes down: it checks every state\n * read below it against the segment keys available there.\n */\nexport function createCheckedContextValue(\n parent: CheckedContextValue,\n available: ReadonlySet<string>,\n report: ScopeViolationHandler,\n): CheckedContextValue {\n const reported: Reported = new Map();\n\n // Each scope reports a selector reading a key once, however often it runs.\n const check = (key: string, selector: Function | undefined) => {\n if (isSegmentKey(key) && !available.has(key) && !seenBefore(reported, selector, key)) {\n report({ key, selector, scope: [...available] });\n }\n };\n\n if (parent.stateAccessTracking === true) {\n const base =\n baseListenerKey in parent\n ? parent[baseListenerKey]\n : (parent.onStateAccess as StateAccessListener | undefined);\n\n const onStateAccess: StateAccessListener = (key, selector) => {\n check(key, selector);\n base?.(key, selector);\n };\n\n return { ...parent, onStateAccess, [baseListenerKey]: base };\n }\n\n return {\n ...parent,\n store: createScopedStore(parent.store, (key) => check(key, undefined)),\n };\n}\n","import type { ReactNode } from 'react';\nimport { createElement, useContext, useEffect, useLayoutEffect, useMemo, useReducer } from 'react';\nimport type { AnySegment } from '../segment';\nimport type { SparseController } from '../sparseEnhancer';\nimport type { InjectEpicOptions, InjectReducerOptions } from './injectedSegments';\nimport { getEpicSegment, getReducerSegment } from './injectedSegments';\nimport type { ReactReduxContextLike } from './reactReduxContext';\nimport { getReactReduxContext } from './reactReduxContext';\nimport type { ScopeViolationHandler } from './scopeChecks';\nimport { createCheckedContextValue, reportViolationOnce, ScopeKeysContext } from './scopeChecks';\n\n// Layout effects do not run on the server, and React warns about them there.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\nconst segmentIds = new WeakMap<AnySegment, number>();\nlet nextSegmentId = 0;\n\n/** Identifies a list of segments by the identity of its members. */\nconst signatureOf = (segments: readonly AnySegment[]) =>\n segments\n .map((segment) => {\n let id = segmentIds.get(segment);\n if (id === undefined) {\n id = nextSegmentId++;\n segmentIds.set(segment, id);\n }\n return id;\n })\n .join(',');\n\n/**\n * The same array for as long as it holds the same segments, so that a new\n * array literal on every render does not acquire them again.\n */\nconst useStableSegments = (segments: readonly AnySegment[]) => {\n const signature = signatureOf(segments);\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- tracked by their signature\n return useMemo(() => segments, [signature]);\n};\n\nconst collectKeys = (segments: readonly AnySegment[], keys: Set<string>) => {\n for (const segment of segments) {\n keys.add(segment.key);\n collectKeys(segment.deps, keys);\n }\n};\n\nexport interface SparseReactOptions {\n /**\n * The context the react-redux `<Provider>` was given. Defaults to\n * react-redux's own context.\n */\n context?: ReactReduxContextLike;\n /**\n * Checks that components read segment slices only inside a scope that holds\n * them. Defaults to `true` outside production builds.\n */\n checks?: boolean;\n /**\n * Called for each read of a segment slice outside the scopes that hold it.\n * Defaults to logging each key once per selector with `console.error`.\n */\n onViolation?: ScopeViolationHandler;\n}\n\nexport interface SparseScopeProps {\n /** Segments held for as long as the scope is mounted. */\n segments: readonly AnySegment[];\n /** Rendered until the segments are present in the store. */\n fallback?: ReactNode;\n children?: ReactNode;\n}\n\nexport interface SparseReact {\n /**\n * Holds `segments` for as long as the calling component is mounted and\n * returns whether they are all present in the store yet.\n */\n useSparseSegments(segments: readonly AnySegment[]): boolean;\n /**\n * Holds `segments` for as long as it is mounted, and renders its children\n * only once they are present in the store. With checks on, reads of segment\n * slices below it that no enclosing scope holds are reported; a scope with\n * no segments checks the reads below it that no scope holds at all.\n */\n SparseScope(props: SparseScopeProps): ReactNode;\n /**\n * Injects a reducer under a key for as long as the calling component is\n * mounted, and returns whether its slice is present yet. The segment is made\n * by the first injection under the key, with its reducer and options, and\n * shared by every later one. A key of the static state is left to the root\n * reducer.\n */\n useInjectReducer<State>(options: InjectReducerOptions<State>): boolean;\n /**\n * Runs an epic for as long as the calling component is mounted, once\n * however many components inject it. The segment that runs it is made by its\n * first injection, with its options.\n */\n useInjectEpic(options: InjectEpicOptions): boolean;\n}\n\n/** Creates the React bindings for stores made with `sparseEnhancer`. */\nexport function createSparseReact(options: SparseReactOptions = {}): SparseReact {\n const { checks = process.env.NODE_ENV !== 'production', onViolation = reportViolationOnce } =\n options;\n\n const getContext = () => options.context ?? getReactReduxContext();\n\n const useSparseController = (): SparseController => {\n const value = useContext(getContext());\n\n if (!value) {\n throw new Error('redux-sparse-kit needs a react-redux <Provider> above this component');\n }\n\n if (!value.store.sparse) {\n throw new Error('redux-sparse-kit needs a store created with sparseEnhancer');\n }\n\n return value.store.sparse;\n };\n\n const useSparseSegments = (segments: readonly AnySegment[]): boolean => {\n const sparse = useSparseController();\n const [, rerender] = useReducer((count: number) => count + 1, 0);\n const ready = segments.every((segment) => sparse.has(segment));\n const stable = useStableSegments(segments);\n\n useIsomorphicLayoutEffect(() => {\n const release = sparse.acquire(...stable);\n if (!ready) {\n // Rendered before they were injected: render again, before paint.\n rerender();\n }\n return release;\n }, [sparse, stable]);\n\n return ready;\n };\n\n const noSegments: readonly AnySegment[] = [];\n\n const useInjectReducer = <State>(injection: InjectReducerOptions<State>): boolean => {\n const sparse = useSparseController();\n return useSparseSegments(\n sparse.isStatic(injection.key) ? noSegments : [getReducerSegment(injection)],\n );\n };\n\n const useInjectEpic = ({ epic, ...injection }: InjectEpicOptions): boolean =>\n useSparseSegments(epic ? [getEpicSegment(epic, injection)] : noSegments);\n\n function SparseScope({ segments, fallback = null, children }: SparseScopeProps): ReactNode {\n return useSparseSegments(segments) ? children : fallback;\n }\n\n function CheckedSparseScope({\n segments,\n fallback = null,\n children,\n }: SparseScopeProps): ReactNode {\n const ready = useSparseSegments(segments);\n const context = getContext();\n const parent = useContext(context)!;\n const parentKeys = useContext(ScopeKeysContext);\n const stable = useStableSegments(segments);\n\n const available = useMemo(() => {\n const keys = new Set(parentKeys);\n collectKeys(stable, keys);\n return keys;\n }, [parentKeys, stable]);\n\n const value = useMemo(\n () => createCheckedContextValue(parent, available, onViolation),\n [parent, available],\n );\n\n if (!ready) {\n return fallback;\n }\n\n return createElement(\n ScopeKeysContext.Provider,\n { value: available },\n createElement(context.Provider, { value }, children),\n );\n }\n\n return {\n useSparseSegments,\n SparseScope: checks ? CheckedSparseScope : SparseScope,\n useInjectReducer,\n useInjectEpic,\n };\n}\n","import { createSparseReact } from './createSparseReact';\nimport type { SparseReact } from './createSparseReact';\n\nconst defaults: SparseReact = createSparseReact();\n\n/** {@inheritDoc SparseReact.SparseScope} */\nexport const SparseScope: SparseReact['SparseScope'] = defaults.SparseScope;\n\n/** {@inheritDoc SparseReact.useSparseSegments} */\nexport const useSparseSegments: SparseReact['useSparseSegments'] = defaults.useSparseSegments;\n\n/** {@inheritDoc SparseReact.useInjectReducer} */\nexport const useInjectReducer: SparseReact['useInjectReducer'] = defaults.useInjectReducer;\n\n/** {@inheritDoc SparseReact.useInjectEpic} */\nexport const useInjectEpic: SparseReact['useInjectEpic'] = defaults.useInjectEpic;\n\nexport { createSparseReact } from './createSparseReact';\nexport type { InjectEpicOptions, InjectOptions, InjectReducerOptions } from './injectedSegments';\nexport type { SparseReact, SparseReactOptions, SparseScopeProps } from './createSparseReact';\nexport { getReactReduxContext } from './reactReduxContext';\nexport type { ScopeViolation, ScopeViolationHandler } from './scopeChecks';\nexport type { ReactReduxContextLike, ReactReduxContextValueLike } from './reactReduxContext';\n"],"mappings":"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"}
|
package/dist/react.mjs
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
|
-
import { n as isSegmentKey } from "./segment-DFAgPcwA.mjs";
|
|
1
|
+
import { n as isSegmentKey, t as defineSegment } from "./segment-DFAgPcwA.mjs";
|
|
2
2
|
import { createContext, createElement, useContext, useEffect, useLayoutEffect, useMemo, useReducer } from "react";
|
|
3
|
+
//#region src/react/injectedSegments.ts
|
|
4
|
+
const reducerSegments = /* @__PURE__ */ new Map();
|
|
5
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
6
|
+
/**
|
|
7
|
+
* The segment injected under a key: made by the first injection, with its
|
|
8
|
+
* reducer and options, and shared by every later one.
|
|
9
|
+
*/
|
|
10
|
+
const getReducerSegment = ({ key, reducer, idleTimeout, whileIdle }) => {
|
|
11
|
+
const existing = reducerSegments.get(key);
|
|
12
|
+
if (existing) {
|
|
13
|
+
if (existing.reducer !== reducer && process.env.NODE_ENV !== "production" && !warnedKeys.has(key)) {
|
|
14
|
+
warnedKeys.add(key);
|
|
15
|
+
console.warn(`redux-sparse-kit: "${key}" is injected with another reducer than before; the first one is kept.`);
|
|
16
|
+
}
|
|
17
|
+
return existing;
|
|
18
|
+
}
|
|
19
|
+
const segment = defineSegment({
|
|
20
|
+
key,
|
|
21
|
+
reducer,
|
|
22
|
+
idleTimeout,
|
|
23
|
+
whileIdle
|
|
24
|
+
});
|
|
25
|
+
reducerSegments.set(key, segment);
|
|
26
|
+
return segment;
|
|
27
|
+
};
|
|
28
|
+
const epicSegments = /* @__PURE__ */ new WeakMap();
|
|
29
|
+
let nextEpicId = 0;
|
|
30
|
+
/** The segment that runs an epic: one per epic, made by its first injection. */
|
|
31
|
+
const getEpicSegment = (epic, { idleTimeout, whileIdle }) => {
|
|
32
|
+
let segment = epicSegments.get(epic);
|
|
33
|
+
if (!segment) {
|
|
34
|
+
segment = defineSegment({
|
|
35
|
+
key: `@@epic/${epic.name || "anonymous"}#${nextEpicId++}`,
|
|
36
|
+
epics: [epic],
|
|
37
|
+
idleTimeout,
|
|
38
|
+
whileIdle
|
|
39
|
+
});
|
|
40
|
+
epicSegments.set(epic, segment);
|
|
41
|
+
}
|
|
42
|
+
return segment;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
3
45
|
//#region src/react/reactReduxContext.ts
|
|
4
46
|
const contextKey = Symbol.for("react-redux-context");
|
|
5
47
|
/**
|
|
@@ -149,6 +191,12 @@ function createSparseReact(options = {}) {
|
|
|
149
191
|
}, [sparse, stable]);
|
|
150
192
|
return ready;
|
|
151
193
|
};
|
|
194
|
+
const noSegments = [];
|
|
195
|
+
const useInjectReducer = (injection) => {
|
|
196
|
+
const sparse = useSparseController();
|
|
197
|
+
return useSparseSegments(sparse.isStatic(injection.key) ? noSegments : [getReducerSegment(injection)]);
|
|
198
|
+
};
|
|
199
|
+
const useInjectEpic = ({ epic, ...injection }) => useSparseSegments(epic ? [getEpicSegment(epic, injection)] : noSegments);
|
|
152
200
|
function SparseScope({ segments, fallback = null, children }) {
|
|
153
201
|
return useSparseSegments(segments) ? children : fallback;
|
|
154
202
|
}
|
|
@@ -169,7 +217,9 @@ function createSparseReact(options = {}) {
|
|
|
169
217
|
}
|
|
170
218
|
return {
|
|
171
219
|
useSparseSegments,
|
|
172
|
-
SparseScope: checks ? CheckedSparseScope : SparseScope
|
|
220
|
+
SparseScope: checks ? CheckedSparseScope : SparseScope,
|
|
221
|
+
useInjectReducer,
|
|
222
|
+
useInjectEpic
|
|
173
223
|
};
|
|
174
224
|
}
|
|
175
225
|
//#endregion
|
|
@@ -179,7 +229,11 @@ const defaults = createSparseReact();
|
|
|
179
229
|
const SparseScope = defaults.SparseScope;
|
|
180
230
|
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
181
231
|
const useSparseSegments = defaults.useSparseSegments;
|
|
232
|
+
/** {@inheritDoc SparseReact.useInjectReducer} */
|
|
233
|
+
const useInjectReducer = defaults.useInjectReducer;
|
|
234
|
+
/** {@inheritDoc SparseReact.useInjectEpic} */
|
|
235
|
+
const useInjectEpic = defaults.useInjectEpic;
|
|
182
236
|
//#endregion
|
|
183
|
-
export { SparseScope, createSparseReact, getReactReduxContext, useSparseSegments };
|
|
237
|
+
export { SparseScope, createSparseReact, getReactReduxContext, useInjectEpic, useInjectReducer, useSparseSegments };
|
|
184
238
|
|
|
185
239
|
//# sourceMappingURL=react.mjs.map
|
package/dist/react.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/reactReduxContext.ts","../src/react/scopeChecks.ts","../src/react/createSparseReact.ts","../src/react/index.ts"],"sourcesContent":["import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport type { SparseStoreExt } from '../sparseEnhancer';\n\n/** The parts of a react-redux context value this package relies on. */\nexport interface ReactReduxContextValueLike {\n store: Store & Partial<SparseStoreExt>;\n [key: string]: unknown;\n}\n\nexport type ReactReduxContextLike = Context<ReactReduxContextValueLike | null>;\n\nconst contextKey = Symbol.for('react-redux-context');\n\ntype ContextRegistry = Map<typeof createContext, ReactReduxContextLike>;\n\n/**\n * The context react-redux's `<Provider>` fills. React-redux keeps it in a\n * registry on `globalThis`, keyed by the React instance, so that every copy\n * of react-redux shares it; reading the same registry finds it without a\n * dependency on any particular build of react-redux.\n */\nexport function getReactReduxContext(): ReactReduxContextLike {\n const registry = globalThis as { [contextKey]?: ContextRegistry };\n const contexts = (registry[contextKey] ??= new Map());\n let context = contexts.get(createContext);\n\n if (!context) {\n context = createContext<ReactReduxContextValueLike | null>(null);\n context.displayName = 'ReactRedux';\n contexts.set(createContext, context);\n }\n\n return context;\n}\n","import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport { isSegmentKey } from '../segment';\nimport type { ReactReduxContextValueLike } from './reactReduxContext';\n\n/** A read of a segment's slice made outside every scope that holds it. */\nexport interface ScopeViolation {\n /** The key of the segment slice that was read. */\n key: string;\n /**\n * The selector that read it, when react-redux reports reads through\n * `onStateAccess`; otherwise `undefined`.\n */\n selector: Function | undefined;\n /** The segment keys available where it was read. */\n scope: readonly string[];\n}\n\nexport type ScopeViolationHandler = (violation: ScopeViolation) => void;\n\ntype StateAccessListener = (key: string, selector: Function) => void;\n\n/** The segment keys the enclosing scopes hold, with their dependencies. */\nexport const ScopeKeysContext: Context<ReadonlySet<string>> = createContext<ReadonlySet<string>>(\n new Set(),\n);\n\n// The listener a scope found in place, so that nested scopes call it instead\n// of the listener of the scope around them.\nconst baseListenerKey: unique symbol = Symbol('redux-sparse-kit.baseListener');\n\n// The store a scoped store wraps, so that nested scopes wrap the original\n// store rather than the scoped store of the scope around them.\nconst baseStoreKey: unique symbol = Symbol('redux-sparse-kit.baseStore');\n\ninterface CheckedContextValue extends ReactReduxContextValueLike {\n [baseListenerKey]?: StateAccessListener | undefined;\n}\n\ntype ScopedStore = Store & { [baseStoreKey]?: Store };\n\ntype Reported = Map<Function | undefined, Set<string>>;\n\n/** Whether this selector and key were seen before; records them if not. */\nconst seenBefore = (reported: Reported, selector: Function | undefined, key: string) => {\n let keys = reported.get(selector);\n if (!keys) {\n keys = new Set();\n reported.set(selector, keys);\n }\n\n if (keys.has(key)) {\n return true;\n }\n keys.add(key);\n return false;\n};\n\nconst reportedByDefault: Reported = new Map();\n\n/** Logs each key once per selector, across all scopes. */\nexport const reportViolationOnce: ScopeViolationHandler = ({ key, selector, scope }) => {\n if (seenBefore(reportedByDefault, selector, key)) {\n return;\n }\n\n const reader = selector ? `Selector \"${selector.name || 'anonymous'}\"` : 'A selector';\n const available = scope.length > 0 ? scope.map((name) => `\"${name}\"`).join(', ') : 'none';\n\n console.error(\n `redux-sparse-kit: ${reader} read state.${key} outside every SparseScope holding \"${key}\" ` +\n `(segments available here: ${available}).`,\n );\n};\n\nconst createScopedStore = (parent: ScopedStore, check: (key: string) => void): ScopedStore => {\n const store = parent[baseStoreKey] ?? parent;\n const proxies = new WeakMap<object, object>();\n\n return {\n ...store,\n [baseStoreKey]: store,\n getState() {\n // Always an object: the root state of a store made with sparseEnhancer.\n const state = store.getState() as object;\n\n let proxy = proxies.get(state);\n if (!proxy) {\n proxy = new Proxy(state, {\n get(target, property, receiver) {\n if (typeof property === 'string') {\n check(property);\n }\n return Reflect.get(target, property, receiver);\n },\n });\n proxies.set(state, proxy);\n }\n return proxy;\n },\n };\n};\n\n/**\n * The react-redux context value a scope passes down: it checks every state\n * read below it against the segment keys available there.\n */\nexport function createCheckedContextValue(\n parent: CheckedContextValue,\n available: ReadonlySet<string>,\n report: ScopeViolationHandler,\n): CheckedContextValue {\n const reported: Reported = new Map();\n\n // Each scope reports a selector reading a key once, however often it runs.\n const check = (key: string, selector: Function | undefined) => {\n if (isSegmentKey(key) && !available.has(key) && !seenBefore(reported, selector, key)) {\n report({ key, selector, scope: [...available] });\n }\n };\n\n if (parent.stateAccessTracking === true) {\n const base =\n baseListenerKey in parent\n ? parent[baseListenerKey]\n : (parent.onStateAccess as StateAccessListener | undefined);\n\n const onStateAccess: StateAccessListener = (key, selector) => {\n check(key, selector);\n base?.(key, selector);\n };\n\n return { ...parent, onStateAccess, [baseListenerKey]: base };\n }\n\n return {\n ...parent,\n store: createScopedStore(parent.store, (key) => check(key, undefined)),\n };\n}\n","import type { ReactNode } from 'react';\nimport { createElement, useContext, useEffect, useLayoutEffect, useMemo, useReducer } from 'react';\nimport type { AnySegment } from '../segment';\nimport type { SparseController } from '../sparseEnhancer';\nimport type { ReactReduxContextLike } from './reactReduxContext';\nimport { getReactReduxContext } from './reactReduxContext';\nimport type { ScopeViolationHandler } from './scopeChecks';\nimport { createCheckedContextValue, reportViolationOnce, ScopeKeysContext } from './scopeChecks';\n\n// Layout effects do not run on the server, and React warns about them there.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\nconst segmentIds = new WeakMap<AnySegment, number>();\nlet nextSegmentId = 0;\n\n/** Identifies a list of segments by the identity of its members. */\nconst signatureOf = (segments: readonly AnySegment[]) =>\n segments\n .map((segment) => {\n let id = segmentIds.get(segment);\n if (id === undefined) {\n id = nextSegmentId++;\n segmentIds.set(segment, id);\n }\n return id;\n })\n .join(',');\n\n/**\n * The same array for as long as it holds the same segments, so that a new\n * array literal on every render does not acquire them again.\n */\nconst useStableSegments = (segments: readonly AnySegment[]) => {\n const signature = signatureOf(segments);\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- tracked by their signature\n return useMemo(() => segments, [signature]);\n};\n\nconst collectKeys = (segments: readonly AnySegment[], keys: Set<string>) => {\n for (const segment of segments) {\n keys.add(segment.key);\n collectKeys(segment.deps, keys);\n }\n};\n\nexport interface SparseReactOptions {\n /**\n * The context the react-redux `<Provider>` was given. Defaults to\n * react-redux's own context.\n */\n context?: ReactReduxContextLike;\n /**\n * Checks that components read segment slices only inside a scope that holds\n * them. Defaults to `true` outside production builds.\n */\n checks?: boolean;\n /**\n * Called for each read of a segment slice outside the scopes that hold it.\n * Defaults to logging each key once per selector with `console.error`.\n */\n onViolation?: ScopeViolationHandler;\n}\n\nexport interface SparseScopeProps {\n /** Segments held for as long as the scope is mounted. */\n segments: readonly AnySegment[];\n /** Rendered until the segments are present in the store. */\n fallback?: ReactNode;\n children?: ReactNode;\n}\n\nexport interface SparseReact {\n /**\n * Holds `segments` for as long as the calling component is mounted and\n * returns whether they are all present in the store yet.\n */\n useSparseSegments(segments: readonly AnySegment[]): boolean;\n /**\n * Holds `segments` for as long as it is mounted, and renders its children\n * only once they are present in the store. With checks on, reads of segment\n * slices below it that no enclosing scope holds are reported; a scope with\n * no segments checks the reads below it that no scope holds at all.\n */\n SparseScope(props: SparseScopeProps): ReactNode;\n}\n\n/** Creates the React bindings for stores made with `sparseEnhancer`. */\nexport function createSparseReact(options: SparseReactOptions = {}): SparseReact {\n const { checks = process.env.NODE_ENV !== 'production', onViolation = reportViolationOnce } =\n options;\n\n const getContext = () => options.context ?? getReactReduxContext();\n\n const useSparseController = (): SparseController => {\n const value = useContext(getContext());\n\n if (!value) {\n throw new Error('redux-sparse-kit needs a react-redux <Provider> above this component');\n }\n\n if (!value.store.sparse) {\n throw new Error('redux-sparse-kit needs a store created with sparseEnhancer');\n }\n\n return value.store.sparse;\n };\n\n const useSparseSegments = (segments: readonly AnySegment[]): boolean => {\n const sparse = useSparseController();\n const [, rerender] = useReducer((count: number) => count + 1, 0);\n const ready = segments.every((segment) => sparse.has(segment));\n const stable = useStableSegments(segments);\n\n useIsomorphicLayoutEffect(() => {\n const release = sparse.acquire(...stable);\n if (!ready) {\n // Rendered before they were injected: render again, before paint.\n rerender();\n }\n return release;\n }, [sparse, stable]);\n\n return ready;\n };\n\n function SparseScope({ segments, fallback = null, children }: SparseScopeProps): ReactNode {\n return useSparseSegments(segments) ? children : fallback;\n }\n\n function CheckedSparseScope({\n segments,\n fallback = null,\n children,\n }: SparseScopeProps): ReactNode {\n const ready = useSparseSegments(segments);\n const context = getContext();\n const parent = useContext(context)!;\n const parentKeys = useContext(ScopeKeysContext);\n const stable = useStableSegments(segments);\n\n const available = useMemo(() => {\n const keys = new Set(parentKeys);\n collectKeys(stable, keys);\n return keys;\n }, [parentKeys, stable]);\n\n const value = useMemo(\n () => createCheckedContextValue(parent, available, onViolation),\n [parent, available],\n );\n\n if (!ready) {\n return fallback;\n }\n\n return createElement(\n ScopeKeysContext.Provider,\n { value: available },\n createElement(context.Provider, { value }, children),\n );\n }\n\n return { useSparseSegments, SparseScope: checks ? CheckedSparseScope : SparseScope };\n}\n","import { createSparseReact } from './createSparseReact';\nimport type { SparseReact } from './createSparseReact';\n\nconst defaults: SparseReact = createSparseReact();\n\n/** {@inheritDoc SparseReact.SparseScope} */\nexport const SparseScope: SparseReact['SparseScope'] = defaults.SparseScope;\n\n/** {@inheritDoc SparseReact.useSparseSegments} */\nexport const useSparseSegments: SparseReact['useSparseSegments'] = defaults.useSparseSegments;\n\nexport { createSparseReact } from './createSparseReact';\nexport type { SparseReact, SparseReactOptions, SparseScopeProps } from './createSparseReact';\nexport { getReactReduxContext } from './reactReduxContext';\nexport type { ScopeViolation, ScopeViolationHandler } from './scopeChecks';\nexport type { ReactReduxContextLike, ReactReduxContextValueLike } from './reactReduxContext';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/injectedSegments.ts","../src/react/reactReduxContext.ts","../src/react/scopeChecks.ts","../src/react/createSparseReact.ts","../src/react/index.ts"],"sourcesContent":["import type { Reducer } from 'redux';\nimport type { AnySegment, SegmentEpic, WhileIdle } from '../segment';\nimport { defineSegment } from '../segment';\n\nexport interface InjectOptions {\n /** The idle timeout of the segment made for the injection. */\n idleTimeout?: number;\n /** What the segment made for the injection does while idle. */\n whileIdle?: WhileIdle;\n}\n\nexport interface InjectReducerOptions<State = any> extends InjectOptions {\n key: string;\n reducer: Reducer<State>;\n}\n\nexport interface InjectEpicOptions extends InjectOptions {\n /** Nothing is injected while it is `undefined`. */\n epic: SegmentEpic | undefined;\n}\n\nconst reducerSegments = new Map<string, AnySegment>();\nconst warnedKeys = new Set<string>();\n\n/**\n * The segment injected under a key: made by the first injection, with its\n * reducer and options, and shared by every later one.\n */\nexport const getReducerSegment = ({\n key,\n reducer,\n idleTimeout,\n whileIdle,\n}: InjectReducerOptions): AnySegment => {\n const existing = reducerSegments.get(key);\n\n if (existing) {\n if (\n existing.reducer !== reducer &&\n process.env.NODE_ENV !== 'production' &&\n !warnedKeys.has(key)\n ) {\n warnedKeys.add(key);\n console.warn(\n `redux-sparse-kit: \"${key}\" is injected with another reducer than before; the first one is kept.`,\n );\n }\n return existing;\n }\n\n const segment = defineSegment({ key, reducer, idleTimeout, whileIdle });\n reducerSegments.set(key, segment);\n return segment;\n};\n\nconst epicSegments = new WeakMap<SegmentEpic, AnySegment>();\nlet nextEpicId = 0;\n\n/** The segment that runs an epic: one per epic, made by its first injection. */\nexport const getEpicSegment = (\n epic: SegmentEpic,\n { idleTimeout, whileIdle }: InjectOptions,\n): AnySegment => {\n let segment = epicSegments.get(epic);\n\n if (!segment) {\n segment = defineSegment({\n key: `@@epic/${epic.name || 'anonymous'}#${nextEpicId++}`,\n epics: [epic],\n idleTimeout,\n whileIdle,\n });\n epicSegments.set(epic, segment);\n }\n\n return segment;\n};\n","import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport type { SparseStoreExt } from '../sparseEnhancer';\n\n/** The parts of a react-redux context value this package relies on. */\nexport interface ReactReduxContextValueLike {\n store: Store & Partial<SparseStoreExt>;\n [key: string]: unknown;\n}\n\nexport type ReactReduxContextLike = Context<ReactReduxContextValueLike | null>;\n\nconst contextKey = Symbol.for('react-redux-context');\n\ntype ContextRegistry = Map<typeof createContext, ReactReduxContextLike>;\n\n/**\n * The context react-redux's `<Provider>` fills. React-redux keeps it in a\n * registry on `globalThis`, keyed by the React instance, so that every copy\n * of react-redux shares it; reading the same registry finds it without a\n * dependency on any particular build of react-redux.\n */\nexport function getReactReduxContext(): ReactReduxContextLike {\n const registry = globalThis as { [contextKey]?: ContextRegistry };\n const contexts = (registry[contextKey] ??= new Map());\n let context = contexts.get(createContext);\n\n if (!context) {\n context = createContext<ReactReduxContextValueLike | null>(null);\n context.displayName = 'ReactRedux';\n contexts.set(createContext, context);\n }\n\n return context;\n}\n","import type { Context } from 'react';\nimport { createContext } from 'react';\nimport type { Store } from 'redux';\nimport { isSegmentKey } from '../segment';\nimport type { ReactReduxContextValueLike } from './reactReduxContext';\n\n/** A read of a segment's slice made outside every scope that holds it. */\nexport interface ScopeViolation {\n /** The key of the segment slice that was read. */\n key: string;\n /**\n * The selector that read it, when react-redux reports reads through\n * `onStateAccess`; otherwise `undefined`.\n */\n selector: Function | undefined;\n /** The segment keys available where it was read. */\n scope: readonly string[];\n}\n\nexport type ScopeViolationHandler = (violation: ScopeViolation) => void;\n\ntype StateAccessListener = (key: string, selector: Function) => void;\n\n/** The segment keys the enclosing scopes hold, with their dependencies. */\nexport const ScopeKeysContext: Context<ReadonlySet<string>> = createContext<ReadonlySet<string>>(\n new Set(),\n);\n\n// The listener a scope found in place, so that nested scopes call it instead\n// of the listener of the scope around them.\nconst baseListenerKey: unique symbol = Symbol('redux-sparse-kit.baseListener');\n\n// The store a scoped store wraps, so that nested scopes wrap the original\n// store rather than the scoped store of the scope around them.\nconst baseStoreKey: unique symbol = Symbol('redux-sparse-kit.baseStore');\n\ninterface CheckedContextValue extends ReactReduxContextValueLike {\n [baseListenerKey]?: StateAccessListener | undefined;\n}\n\ntype ScopedStore = Store & { [baseStoreKey]?: Store };\n\ntype Reported = Map<Function | undefined, Set<string>>;\n\n/** Whether this selector and key were seen before; records them if not. */\nconst seenBefore = (reported: Reported, selector: Function | undefined, key: string) => {\n let keys = reported.get(selector);\n if (!keys) {\n keys = new Set();\n reported.set(selector, keys);\n }\n\n if (keys.has(key)) {\n return true;\n }\n keys.add(key);\n return false;\n};\n\nconst reportedByDefault: Reported = new Map();\n\n/** Logs each key once per selector, across all scopes. */\nexport const reportViolationOnce: ScopeViolationHandler = ({ key, selector, scope }) => {\n if (seenBefore(reportedByDefault, selector, key)) {\n return;\n }\n\n const reader = selector ? `Selector \"${selector.name || 'anonymous'}\"` : 'A selector';\n const available = scope.length > 0 ? scope.map((name) => `\"${name}\"`).join(', ') : 'none';\n\n console.error(\n `redux-sparse-kit: ${reader} read state.${key} outside every SparseScope holding \"${key}\" ` +\n `(segments available here: ${available}).`,\n );\n};\n\nconst createScopedStore = (parent: ScopedStore, check: (key: string) => void): ScopedStore => {\n const store = parent[baseStoreKey] ?? parent;\n const proxies = new WeakMap<object, object>();\n\n return {\n ...store,\n [baseStoreKey]: store,\n getState() {\n // Always an object: the root state of a store made with sparseEnhancer.\n const state = store.getState() as object;\n\n let proxy = proxies.get(state);\n if (!proxy) {\n proxy = new Proxy(state, {\n get(target, property, receiver) {\n if (typeof property === 'string') {\n check(property);\n }\n return Reflect.get(target, property, receiver);\n },\n });\n proxies.set(state, proxy);\n }\n return proxy;\n },\n };\n};\n\n/**\n * The react-redux context value a scope passes down: it checks every state\n * read below it against the segment keys available there.\n */\nexport function createCheckedContextValue(\n parent: CheckedContextValue,\n available: ReadonlySet<string>,\n report: ScopeViolationHandler,\n): CheckedContextValue {\n const reported: Reported = new Map();\n\n // Each scope reports a selector reading a key once, however often it runs.\n const check = (key: string, selector: Function | undefined) => {\n if (isSegmentKey(key) && !available.has(key) && !seenBefore(reported, selector, key)) {\n report({ key, selector, scope: [...available] });\n }\n };\n\n if (parent.stateAccessTracking === true) {\n const base =\n baseListenerKey in parent\n ? parent[baseListenerKey]\n : (parent.onStateAccess as StateAccessListener | undefined);\n\n const onStateAccess: StateAccessListener = (key, selector) => {\n check(key, selector);\n base?.(key, selector);\n };\n\n return { ...parent, onStateAccess, [baseListenerKey]: base };\n }\n\n return {\n ...parent,\n store: createScopedStore(parent.store, (key) => check(key, undefined)),\n };\n}\n","import type { ReactNode } from 'react';\nimport { createElement, useContext, useEffect, useLayoutEffect, useMemo, useReducer } from 'react';\nimport type { AnySegment } from '../segment';\nimport type { SparseController } from '../sparseEnhancer';\nimport type { InjectEpicOptions, InjectReducerOptions } from './injectedSegments';\nimport { getEpicSegment, getReducerSegment } from './injectedSegments';\nimport type { ReactReduxContextLike } from './reactReduxContext';\nimport { getReactReduxContext } from './reactReduxContext';\nimport type { ScopeViolationHandler } from './scopeChecks';\nimport { createCheckedContextValue, reportViolationOnce, ScopeKeysContext } from './scopeChecks';\n\n// Layout effects do not run on the server, and React warns about them there.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\nconst segmentIds = new WeakMap<AnySegment, number>();\nlet nextSegmentId = 0;\n\n/** Identifies a list of segments by the identity of its members. */\nconst signatureOf = (segments: readonly AnySegment[]) =>\n segments\n .map((segment) => {\n let id = segmentIds.get(segment);\n if (id === undefined) {\n id = nextSegmentId++;\n segmentIds.set(segment, id);\n }\n return id;\n })\n .join(',');\n\n/**\n * The same array for as long as it holds the same segments, so that a new\n * array literal on every render does not acquire them again.\n */\nconst useStableSegments = (segments: readonly AnySegment[]) => {\n const signature = signatureOf(segments);\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- tracked by their signature\n return useMemo(() => segments, [signature]);\n};\n\nconst collectKeys = (segments: readonly AnySegment[], keys: Set<string>) => {\n for (const segment of segments) {\n keys.add(segment.key);\n collectKeys(segment.deps, keys);\n }\n};\n\nexport interface SparseReactOptions {\n /**\n * The context the react-redux `<Provider>` was given. Defaults to\n * react-redux's own context.\n */\n context?: ReactReduxContextLike;\n /**\n * Checks that components read segment slices only inside a scope that holds\n * them. Defaults to `true` outside production builds.\n */\n checks?: boolean;\n /**\n * Called for each read of a segment slice outside the scopes that hold it.\n * Defaults to logging each key once per selector with `console.error`.\n */\n onViolation?: ScopeViolationHandler;\n}\n\nexport interface SparseScopeProps {\n /** Segments held for as long as the scope is mounted. */\n segments: readonly AnySegment[];\n /** Rendered until the segments are present in the store. */\n fallback?: ReactNode;\n children?: ReactNode;\n}\n\nexport interface SparseReact {\n /**\n * Holds `segments` for as long as the calling component is mounted and\n * returns whether they are all present in the store yet.\n */\n useSparseSegments(segments: readonly AnySegment[]): boolean;\n /**\n * Holds `segments` for as long as it is mounted, and renders its children\n * only once they are present in the store. With checks on, reads of segment\n * slices below it that no enclosing scope holds are reported; a scope with\n * no segments checks the reads below it that no scope holds at all.\n */\n SparseScope(props: SparseScopeProps): ReactNode;\n /**\n * Injects a reducer under a key for as long as the calling component is\n * mounted, and returns whether its slice is present yet. The segment is made\n * by the first injection under the key, with its reducer and options, and\n * shared by every later one. A key of the static state is left to the root\n * reducer.\n */\n useInjectReducer<State>(options: InjectReducerOptions<State>): boolean;\n /**\n * Runs an epic for as long as the calling component is mounted, once\n * however many components inject it. The segment that runs it is made by its\n * first injection, with its options.\n */\n useInjectEpic(options: InjectEpicOptions): boolean;\n}\n\n/** Creates the React bindings for stores made with `sparseEnhancer`. */\nexport function createSparseReact(options: SparseReactOptions = {}): SparseReact {\n const { checks = process.env.NODE_ENV !== 'production', onViolation = reportViolationOnce } =\n options;\n\n const getContext = () => options.context ?? getReactReduxContext();\n\n const useSparseController = (): SparseController => {\n const value = useContext(getContext());\n\n if (!value) {\n throw new Error('redux-sparse-kit needs a react-redux <Provider> above this component');\n }\n\n if (!value.store.sparse) {\n throw new Error('redux-sparse-kit needs a store created with sparseEnhancer');\n }\n\n return value.store.sparse;\n };\n\n const useSparseSegments = (segments: readonly AnySegment[]): boolean => {\n const sparse = useSparseController();\n const [, rerender] = useReducer((count: number) => count + 1, 0);\n const ready = segments.every((segment) => sparse.has(segment));\n const stable = useStableSegments(segments);\n\n useIsomorphicLayoutEffect(() => {\n const release = sparse.acquire(...stable);\n if (!ready) {\n // Rendered before they were injected: render again, before paint.\n rerender();\n }\n return release;\n }, [sparse, stable]);\n\n return ready;\n };\n\n const noSegments: readonly AnySegment[] = [];\n\n const useInjectReducer = <State>(injection: InjectReducerOptions<State>): boolean => {\n const sparse = useSparseController();\n return useSparseSegments(\n sparse.isStatic(injection.key) ? noSegments : [getReducerSegment(injection)],\n );\n };\n\n const useInjectEpic = ({ epic, ...injection }: InjectEpicOptions): boolean =>\n useSparseSegments(epic ? [getEpicSegment(epic, injection)] : noSegments);\n\n function SparseScope({ segments, fallback = null, children }: SparseScopeProps): ReactNode {\n return useSparseSegments(segments) ? children : fallback;\n }\n\n function CheckedSparseScope({\n segments,\n fallback = null,\n children,\n }: SparseScopeProps): ReactNode {\n const ready = useSparseSegments(segments);\n const context = getContext();\n const parent = useContext(context)!;\n const parentKeys = useContext(ScopeKeysContext);\n const stable = useStableSegments(segments);\n\n const available = useMemo(() => {\n const keys = new Set(parentKeys);\n collectKeys(stable, keys);\n return keys;\n }, [parentKeys, stable]);\n\n const value = useMemo(\n () => createCheckedContextValue(parent, available, onViolation),\n [parent, available],\n );\n\n if (!ready) {\n return fallback;\n }\n\n return createElement(\n ScopeKeysContext.Provider,\n { value: available },\n createElement(context.Provider, { value }, children),\n );\n }\n\n return {\n useSparseSegments,\n SparseScope: checks ? CheckedSparseScope : SparseScope,\n useInjectReducer,\n useInjectEpic,\n };\n}\n","import { createSparseReact } from './createSparseReact';\nimport type { SparseReact } from './createSparseReact';\n\nconst defaults: SparseReact = createSparseReact();\n\n/** {@inheritDoc SparseReact.SparseScope} */\nexport const SparseScope: SparseReact['SparseScope'] = defaults.SparseScope;\n\n/** {@inheritDoc SparseReact.useSparseSegments} */\nexport const useSparseSegments: SparseReact['useSparseSegments'] = defaults.useSparseSegments;\n\n/** {@inheritDoc SparseReact.useInjectReducer} */\nexport const useInjectReducer: SparseReact['useInjectReducer'] = defaults.useInjectReducer;\n\n/** {@inheritDoc SparseReact.useInjectEpic} */\nexport const useInjectEpic: SparseReact['useInjectEpic'] = defaults.useInjectEpic;\n\nexport { createSparseReact } from './createSparseReact';\nexport type { InjectEpicOptions, InjectOptions, InjectReducerOptions } from './injectedSegments';\nexport type { SparseReact, SparseReactOptions, SparseScopeProps } from './createSparseReact';\nexport { getReactReduxContext } from './reactReduxContext';\nexport type { ScopeViolation, ScopeViolationHandler } from './scopeChecks';\nexport type { ReactReduxContextLike, ReactReduxContextValueLike } from './reactReduxContext';\n"],"mappings":"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"}
|
|
@@ -155,6 +155,11 @@ interface SparseController {
|
|
|
155
155
|
acquire(...segments: readonly AnySegment[]): () => void;
|
|
156
156
|
/** Whether a segment with this key is present in the store. */
|
|
157
157
|
has(segmentOrKey: AnySegment | string): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Whether the root reducer owns this key: a segment with a reducer cannot
|
|
160
|
+
* be acquired under it.
|
|
161
|
+
*/
|
|
162
|
+
isStatic(key: string): boolean;
|
|
158
163
|
/**
|
|
159
164
|
* Runs `fn`, deferring injections and evictions it causes until it returns,
|
|
160
165
|
* so that they reach the store as a single action of each kind.
|
|
@@ -209,4 +214,4 @@ interface SparseOptions {
|
|
|
209
214
|
declare function sparseEnhancer(options?: SparseOptions): StoreEnhancer<SparseStoreExt>;
|
|
210
215
|
//#endregion
|
|
211
216
|
export { StaticState as _, SparseEvent as a, isSegmentKey as b, SparseStoreExt as c, ScopeStateOf as d, Segment as f, StateOfSegments as g, SegmentOptions as h, SparseController as i, sparseEnhancer as l, SegmentEpicStateError as m, SparseAction as n, SparseOptions as o, SegmentEpic as p, SparseActionTypes as r, SparseStats as s, EpicRunner as t, AnySegment as u, WhileIdle as v, defineSegment as y };
|
|
212
|
-
//# sourceMappingURL=sparseEnhancer-
|
|
217
|
+
//# sourceMappingURL=sparseEnhancer-BNdAfdGG.d.mts.map
|
|
@@ -155,6 +155,11 @@ interface SparseController {
|
|
|
155
155
|
acquire(...segments: readonly AnySegment[]): () => void;
|
|
156
156
|
/** Whether a segment with this key is present in the store. */
|
|
157
157
|
has(segmentOrKey: AnySegment | string): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Whether the root reducer owns this key: a segment with a reducer cannot
|
|
160
|
+
* be acquired under it.
|
|
161
|
+
*/
|
|
162
|
+
isStatic(key: string): boolean;
|
|
158
163
|
/**
|
|
159
164
|
* Runs `fn`, deferring injections and evictions it causes until it returns,
|
|
160
165
|
* so that they reach the store as a single action of each kind.
|
|
@@ -209,4 +214,4 @@ interface SparseOptions {
|
|
|
209
214
|
declare function sparseEnhancer(options?: SparseOptions): StoreEnhancer<SparseStoreExt>;
|
|
210
215
|
//#endregion
|
|
211
216
|
export { StaticState as _, SparseEvent as a, isSegmentKey as b, SparseStoreExt as c, ScopeStateOf as d, Segment as f, StateOfSegments as g, SegmentOptions as h, SparseController as i, sparseEnhancer as l, SegmentEpicStateError as m, SparseAction as n, SparseOptions as o, SegmentEpic as p, SparseActionTypes as r, SparseStats as s, EpicRunner as t, AnySegment as u, WhileIdle as v, defineSegment as y };
|
|
212
|
-
//# sourceMappingURL=sparseEnhancer-
|
|
217
|
+
//# sourceMappingURL=sparseEnhancer-BNdAfdGG.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redux-sparse-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Dynamic reducers and epics for large Redux stores: only mounted features run on dispatch",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-splitting",
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
"main": "./dist/index.js",
|
|
29
29
|
"module": "./dist/index.mjs",
|
|
30
30
|
"types": "./dist/index.d.ts",
|
|
31
|
+
"typesVersions": {
|
|
32
|
+
"*": {
|
|
33
|
+
"observable": [
|
|
34
|
+
"./dist/observable.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"react": [
|
|
37
|
+
"./dist/react.d.ts"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
31
41
|
"exports": {
|
|
32
42
|
"./package.json": "./package.json",
|
|
33
43
|
".": {
|
|
@@ -63,6 +73,7 @@
|
|
|
63
73
|
},
|
|
64
74
|
"scripts": {
|
|
65
75
|
"build": "yarn clean && tsdown",
|
|
76
|
+
"prepack": "yarn build",
|
|
66
77
|
"test": "vitest run",
|
|
67
78
|
"test:watch": "vitest",
|
|
68
79
|
"test:coverage": "vitest run --coverage",
|