redux-sparse-kit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +332 -2
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +359 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +355 -0
- package/dist/index.mjs.map +1 -0
- package/dist/observable.d.mts +17 -0
- package/dist/observable.d.ts +17 -0
- package/dist/observable.js +24 -0
- package/dist/observable.js.map +1 -0
- package/dist/observable.mjs +23 -0
- package/dist/observable.mjs.map +1 -0
- package/dist/react.d.mts +83 -0
- package/dist/react.d.ts +83 -0
- package/dist/react.js +189 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +185 -0
- package/dist/react.mjs.map +1 -0
- package/dist/segment-DFAgPcwA.mjs +33 -0
- package/dist/segment-DFAgPcwA.mjs.map +1 -0
- package/dist/segment-Dqd6-ABS.js +44 -0
- package/dist/segment-Dqd6-ABS.js.map +1 -0
- package/dist/sparseEnhancer-CIcdhh-P.d.mts +212 -0
- package/dist/sparseEnhancer-CIcdhh-P.d.ts +212 -0
- package/package.json +112 -4
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { c as SparseStoreExt, u as AnySegment } from "./sparseEnhancer-CIcdhh-P.js";
|
|
2
|
+
import { Store } from "redux";
|
|
3
|
+
import { Context, ReactNode } from "react";
|
|
4
|
+
//#region src/react/reactReduxContext.d.ts
|
|
5
|
+
/** The parts of a react-redux context value this package relies on. */
|
|
6
|
+
interface ReactReduxContextValueLike {
|
|
7
|
+
store: Store & Partial<SparseStoreExt>;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
type ReactReduxContextLike = Context<ReactReduxContextValueLike | null>;
|
|
11
|
+
/**
|
|
12
|
+
* The context react-redux's `<Provider>` fills. React-redux keeps it in a
|
|
13
|
+
* registry on `globalThis`, keyed by the React instance, so that every copy
|
|
14
|
+
* of react-redux shares it; reading the same registry finds it without a
|
|
15
|
+
* dependency on any particular build of react-redux.
|
|
16
|
+
*/
|
|
17
|
+
declare function getReactReduxContext(): ReactReduxContextLike;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/react/scopeChecks.d.ts
|
|
20
|
+
/** A read of a segment's slice made outside every scope that holds it. */
|
|
21
|
+
interface ScopeViolation {
|
|
22
|
+
/** The key of the segment slice that was read. */
|
|
23
|
+
key: string;
|
|
24
|
+
/**
|
|
25
|
+
* The selector that read it, when react-redux reports reads through
|
|
26
|
+
* `onStateAccess`; otherwise `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
selector: Function | undefined;
|
|
29
|
+
/** The segment keys available where it was read. */
|
|
30
|
+
scope: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
type ScopeViolationHandler = (violation: ScopeViolation) => void;
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/react/createSparseReact.d.ts
|
|
35
|
+
interface SparseReactOptions {
|
|
36
|
+
/**
|
|
37
|
+
* The context the react-redux `<Provider>` was given. Defaults to
|
|
38
|
+
* react-redux's own context.
|
|
39
|
+
*/
|
|
40
|
+
context?: ReactReduxContextLike;
|
|
41
|
+
/**
|
|
42
|
+
* Checks that components read segment slices only inside a scope that holds
|
|
43
|
+
* them. Defaults to `true` outside production builds.
|
|
44
|
+
*/
|
|
45
|
+
checks?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Called for each read of a segment slice outside the scopes that hold it.
|
|
48
|
+
* Defaults to logging each key once per selector with `console.error`.
|
|
49
|
+
*/
|
|
50
|
+
onViolation?: ScopeViolationHandler;
|
|
51
|
+
}
|
|
52
|
+
interface SparseScopeProps {
|
|
53
|
+
/** Segments held for as long as the scope is mounted. */
|
|
54
|
+
segments: readonly AnySegment[];
|
|
55
|
+
/** Rendered until the segments are present in the store. */
|
|
56
|
+
fallback?: ReactNode;
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
interface SparseReact {
|
|
60
|
+
/**
|
|
61
|
+
* Holds `segments` for as long as the calling component is mounted and
|
|
62
|
+
* returns whether they are all present in the store yet.
|
|
63
|
+
*/
|
|
64
|
+
useSparseSegments(segments: readonly AnySegment[]): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Holds `segments` for as long as it is mounted, and renders its children
|
|
67
|
+
* only once they are present in the store. With checks on, reads of segment
|
|
68
|
+
* slices below it that no enclosing scope holds are reported; a scope with
|
|
69
|
+
* no segments checks the reads below it that no scope holds at all.
|
|
70
|
+
*/
|
|
71
|
+
SparseScope(props: SparseScopeProps): ReactNode;
|
|
72
|
+
}
|
|
73
|
+
/** Creates the React bindings for stores made with `sparseEnhancer`. */
|
|
74
|
+
declare function createSparseReact(options?: SparseReactOptions): SparseReact;
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/react/index.d.ts
|
|
77
|
+
/** {@inheritDoc SparseReact.SparseScope} */
|
|
78
|
+
declare const SparseScope: SparseReact["SparseScope"];
|
|
79
|
+
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
80
|
+
declare const useSparseSegments: SparseReact["useSparseSegments"];
|
|
81
|
+
//#endregion
|
|
82
|
+
export { type ReactReduxContextLike, type ReactReduxContextValueLike, type ScopeViolation, type ScopeViolationHandler, type SparseReact, type SparseReactOptions, SparseScope, type SparseScopeProps, createSparseReact, getReactReduxContext, useSparseSegments };
|
|
83
|
+
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_segment = require("./segment-Dqd6-ABS.js");
|
|
3
|
+
let react = require("react");
|
|
4
|
+
//#region src/react/reactReduxContext.ts
|
|
5
|
+
const contextKey = Symbol.for("react-redux-context");
|
|
6
|
+
/**
|
|
7
|
+
* The context react-redux's `<Provider>` fills. React-redux keeps it in a
|
|
8
|
+
* registry on `globalThis`, keyed by the React instance, so that every copy
|
|
9
|
+
* of react-redux shares it; reading the same registry finds it without a
|
|
10
|
+
* dependency on any particular build of react-redux.
|
|
11
|
+
*/
|
|
12
|
+
function getReactReduxContext() {
|
|
13
|
+
var _registry$contextKey;
|
|
14
|
+
const registry = globalThis;
|
|
15
|
+
const contexts = (_registry$contextKey = registry[contextKey]) !== null && _registry$contextKey !== void 0 ? _registry$contextKey : registry[contextKey] = /* @__PURE__ */ new Map();
|
|
16
|
+
let context = contexts.get(react.createContext);
|
|
17
|
+
if (!context) {
|
|
18
|
+
context = (0, react.createContext)(null);
|
|
19
|
+
context.displayName = "ReactRedux";
|
|
20
|
+
contexts.set(react.createContext, context);
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/react/scopeChecks.ts
|
|
26
|
+
/** The segment keys the enclosing scopes hold, with their dependencies. */
|
|
27
|
+
const ScopeKeysContext = (0, react.createContext)(/* @__PURE__ */ new Set());
|
|
28
|
+
const baseListenerKey = Symbol("redux-sparse-kit.baseListener");
|
|
29
|
+
const baseStoreKey = Symbol("redux-sparse-kit.baseStore");
|
|
30
|
+
/** Whether this selector and key were seen before; records them if not. */
|
|
31
|
+
const seenBefore = (reported, selector, key) => {
|
|
32
|
+
let keys = reported.get(selector);
|
|
33
|
+
if (!keys) {
|
|
34
|
+
keys = /* @__PURE__ */ new Set();
|
|
35
|
+
reported.set(selector, keys);
|
|
36
|
+
}
|
|
37
|
+
if (keys.has(key)) return true;
|
|
38
|
+
keys.add(key);
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
const reportedByDefault = /* @__PURE__ */ new Map();
|
|
42
|
+
/** Logs each key once per selector, across all scopes. */
|
|
43
|
+
const reportViolationOnce = ({ key, selector, scope }) => {
|
|
44
|
+
if (seenBefore(reportedByDefault, selector, key)) return;
|
|
45
|
+
const reader = selector ? `Selector "${selector.name || "anonymous"}"` : "A selector";
|
|
46
|
+
const available = scope.length > 0 ? scope.map((name) => `"${name}"`).join(", ") : "none";
|
|
47
|
+
console.error(`redux-sparse-kit: ${reader} read state.${key} outside every SparseScope holding "${key}" (segments available here: ${available}).`);
|
|
48
|
+
};
|
|
49
|
+
const createScopedStore = (parent, check) => {
|
|
50
|
+
var _parent$baseStoreKey;
|
|
51
|
+
const store = (_parent$baseStoreKey = parent[baseStoreKey]) !== null && _parent$baseStoreKey !== void 0 ? _parent$baseStoreKey : parent;
|
|
52
|
+
const proxies = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
return {
|
|
54
|
+
...store,
|
|
55
|
+
[baseStoreKey]: store,
|
|
56
|
+
getState() {
|
|
57
|
+
const state = store.getState();
|
|
58
|
+
let proxy = proxies.get(state);
|
|
59
|
+
if (!proxy) {
|
|
60
|
+
proxy = new Proxy(state, { get(target, property, receiver) {
|
|
61
|
+
if (typeof property === "string") check(property);
|
|
62
|
+
return Reflect.get(target, property, receiver);
|
|
63
|
+
} });
|
|
64
|
+
proxies.set(state, proxy);
|
|
65
|
+
}
|
|
66
|
+
return proxy;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The react-redux context value a scope passes down: it checks every state
|
|
72
|
+
* read below it against the segment keys available there.
|
|
73
|
+
*/
|
|
74
|
+
function createCheckedContextValue(parent, available, report) {
|
|
75
|
+
const reported = /* @__PURE__ */ new Map();
|
|
76
|
+
const check = (key, selector) => {
|
|
77
|
+
if (require_segment.isSegmentKey(key) && !available.has(key) && !seenBefore(reported, selector, key)) report({
|
|
78
|
+
key,
|
|
79
|
+
selector,
|
|
80
|
+
scope: [...available]
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
if (parent.stateAccessTracking === true) {
|
|
84
|
+
const base = baseListenerKey in parent ? parent[baseListenerKey] : parent.onStateAccess;
|
|
85
|
+
const onStateAccess = (key, selector) => {
|
|
86
|
+
check(key, selector);
|
|
87
|
+
base === null || base === void 0 || base(key, selector);
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
...parent,
|
|
91
|
+
onStateAccess,
|
|
92
|
+
[baseListenerKey]: base
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...parent,
|
|
97
|
+
store: createScopedStore(parent.store, (key) => check(key, void 0))
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/react/createSparseReact.ts
|
|
102
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
|
|
103
|
+
const segmentIds = /* @__PURE__ */ new WeakMap();
|
|
104
|
+
let nextSegmentId = 0;
|
|
105
|
+
/** Identifies a list of segments by the identity of its members. */
|
|
106
|
+
const signatureOf = (segments) => segments.map((segment) => {
|
|
107
|
+
let id = segmentIds.get(segment);
|
|
108
|
+
if (id === void 0) {
|
|
109
|
+
id = nextSegmentId++;
|
|
110
|
+
segmentIds.set(segment, id);
|
|
111
|
+
}
|
|
112
|
+
return id;
|
|
113
|
+
}).join(",");
|
|
114
|
+
/**
|
|
115
|
+
* The same array for as long as it holds the same segments, so that a new
|
|
116
|
+
* array literal on every render does not acquire them again.
|
|
117
|
+
*/
|
|
118
|
+
const useStableSegments = (segments) => {
|
|
119
|
+
const signature = signatureOf(segments);
|
|
120
|
+
return (0, react.useMemo)(() => segments, [signature]);
|
|
121
|
+
};
|
|
122
|
+
const collectKeys = (segments, keys) => {
|
|
123
|
+
for (const segment of segments) {
|
|
124
|
+
keys.add(segment.key);
|
|
125
|
+
collectKeys(segment.deps, keys);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
/** Creates the React bindings for stores made with `sparseEnhancer`. */
|
|
129
|
+
function createSparseReact(options = {}) {
|
|
130
|
+
const { checks = process.env.NODE_ENV !== "production", onViolation = reportViolationOnce } = options;
|
|
131
|
+
const getContext = () => {
|
|
132
|
+
var _options$context;
|
|
133
|
+
return (_options$context = options.context) !== null && _options$context !== void 0 ? _options$context : getReactReduxContext();
|
|
134
|
+
};
|
|
135
|
+
const useSparseController = () => {
|
|
136
|
+
const value = (0, react.useContext)(getContext());
|
|
137
|
+
if (!value) throw new Error("redux-sparse-kit needs a react-redux <Provider> above this component");
|
|
138
|
+
if (!value.store.sparse) throw new Error("redux-sparse-kit needs a store created with sparseEnhancer");
|
|
139
|
+
return value.store.sparse;
|
|
140
|
+
};
|
|
141
|
+
const useSparseSegments = (segments) => {
|
|
142
|
+
const sparse = useSparseController();
|
|
143
|
+
const [, rerender] = (0, react.useReducer)((count) => count + 1, 0);
|
|
144
|
+
const ready = segments.every((segment) => sparse.has(segment));
|
|
145
|
+
const stable = useStableSegments(segments);
|
|
146
|
+
useIsomorphicLayoutEffect(() => {
|
|
147
|
+
const release = sparse.acquire(...stable);
|
|
148
|
+
if (!ready) rerender();
|
|
149
|
+
return release;
|
|
150
|
+
}, [sparse, stable]);
|
|
151
|
+
return ready;
|
|
152
|
+
};
|
|
153
|
+
function SparseScope({ segments, fallback = null, children }) {
|
|
154
|
+
return useSparseSegments(segments) ? children : fallback;
|
|
155
|
+
}
|
|
156
|
+
function CheckedSparseScope({ segments, fallback = null, children }) {
|
|
157
|
+
const ready = useSparseSegments(segments);
|
|
158
|
+
const context = getContext();
|
|
159
|
+
const parent = (0, react.useContext)(context);
|
|
160
|
+
const parentKeys = (0, react.useContext)(ScopeKeysContext);
|
|
161
|
+
const stable = useStableSegments(segments);
|
|
162
|
+
const available = (0, react.useMemo)(() => {
|
|
163
|
+
const keys = new Set(parentKeys);
|
|
164
|
+
collectKeys(stable, keys);
|
|
165
|
+
return keys;
|
|
166
|
+
}, [parentKeys, stable]);
|
|
167
|
+
const value = (0, react.useMemo)(() => createCheckedContextValue(parent, available, onViolation), [parent, available]);
|
|
168
|
+
if (!ready) return fallback;
|
|
169
|
+
return (0, react.createElement)(ScopeKeysContext.Provider, { value: available }, (0, react.createElement)(context.Provider, { value }, children));
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
useSparseSegments,
|
|
173
|
+
SparseScope: checks ? CheckedSparseScope : SparseScope
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/react/index.ts
|
|
178
|
+
const defaults = createSparseReact();
|
|
179
|
+
/** {@inheritDoc SparseReact.SparseScope} */
|
|
180
|
+
const SparseScope = defaults.SparseScope;
|
|
181
|
+
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
182
|
+
const useSparseSegments = defaults.useSparseSegments;
|
|
183
|
+
//#endregion
|
|
184
|
+
exports.SparseScope = SparseScope;
|
|
185
|
+
exports.createSparseReact = createSparseReact;
|
|
186
|
+
exports.getReactReduxContext = getReactReduxContext;
|
|
187
|
+
exports.useSparseSegments = useSparseSegments;
|
|
188
|
+
|
|
189
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +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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"}
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { n as isSegmentKey } from "./segment-DFAgPcwA.mjs";
|
|
2
|
+
import { createContext, createElement, useContext, useEffect, useLayoutEffect, useMemo, useReducer } from "react";
|
|
3
|
+
//#region src/react/reactReduxContext.ts
|
|
4
|
+
const contextKey = Symbol.for("react-redux-context");
|
|
5
|
+
/**
|
|
6
|
+
* The context react-redux's `<Provider>` fills. React-redux keeps it in a
|
|
7
|
+
* registry on `globalThis`, keyed by the React instance, so that every copy
|
|
8
|
+
* of react-redux shares it; reading the same registry finds it without a
|
|
9
|
+
* dependency on any particular build of react-redux.
|
|
10
|
+
*/
|
|
11
|
+
function getReactReduxContext() {
|
|
12
|
+
var _registry$contextKey;
|
|
13
|
+
const registry = globalThis;
|
|
14
|
+
const contexts = (_registry$contextKey = registry[contextKey]) !== null && _registry$contextKey !== void 0 ? _registry$contextKey : registry[contextKey] = /* @__PURE__ */ new Map();
|
|
15
|
+
let context = contexts.get(createContext);
|
|
16
|
+
if (!context) {
|
|
17
|
+
context = createContext(null);
|
|
18
|
+
context.displayName = "ReactRedux";
|
|
19
|
+
contexts.set(createContext, context);
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/react/scopeChecks.ts
|
|
25
|
+
/** The segment keys the enclosing scopes hold, with their dependencies. */
|
|
26
|
+
const ScopeKeysContext = createContext(/* @__PURE__ */ new Set());
|
|
27
|
+
const baseListenerKey = Symbol("redux-sparse-kit.baseListener");
|
|
28
|
+
const baseStoreKey = Symbol("redux-sparse-kit.baseStore");
|
|
29
|
+
/** Whether this selector and key were seen before; records them if not. */
|
|
30
|
+
const seenBefore = (reported, selector, key) => {
|
|
31
|
+
let keys = reported.get(selector);
|
|
32
|
+
if (!keys) {
|
|
33
|
+
keys = /* @__PURE__ */ new Set();
|
|
34
|
+
reported.set(selector, keys);
|
|
35
|
+
}
|
|
36
|
+
if (keys.has(key)) return true;
|
|
37
|
+
keys.add(key);
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
const reportedByDefault = /* @__PURE__ */ new Map();
|
|
41
|
+
/** Logs each key once per selector, across all scopes. */
|
|
42
|
+
const reportViolationOnce = ({ key, selector, scope }) => {
|
|
43
|
+
if (seenBefore(reportedByDefault, selector, key)) return;
|
|
44
|
+
const reader = selector ? `Selector "${selector.name || "anonymous"}"` : "A selector";
|
|
45
|
+
const available = scope.length > 0 ? scope.map((name) => `"${name}"`).join(", ") : "none";
|
|
46
|
+
console.error(`redux-sparse-kit: ${reader} read state.${key} outside every SparseScope holding "${key}" (segments available here: ${available}).`);
|
|
47
|
+
};
|
|
48
|
+
const createScopedStore = (parent, check) => {
|
|
49
|
+
var _parent$baseStoreKey;
|
|
50
|
+
const store = (_parent$baseStoreKey = parent[baseStoreKey]) !== null && _parent$baseStoreKey !== void 0 ? _parent$baseStoreKey : parent;
|
|
51
|
+
const proxies = /* @__PURE__ */ new WeakMap();
|
|
52
|
+
return {
|
|
53
|
+
...store,
|
|
54
|
+
[baseStoreKey]: store,
|
|
55
|
+
getState() {
|
|
56
|
+
const state = store.getState();
|
|
57
|
+
let proxy = proxies.get(state);
|
|
58
|
+
if (!proxy) {
|
|
59
|
+
proxy = new Proxy(state, { get(target, property, receiver) {
|
|
60
|
+
if (typeof property === "string") check(property);
|
|
61
|
+
return Reflect.get(target, property, receiver);
|
|
62
|
+
} });
|
|
63
|
+
proxies.set(state, proxy);
|
|
64
|
+
}
|
|
65
|
+
return proxy;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The react-redux context value a scope passes down: it checks every state
|
|
71
|
+
* read below it against the segment keys available there.
|
|
72
|
+
*/
|
|
73
|
+
function createCheckedContextValue(parent, available, report) {
|
|
74
|
+
const reported = /* @__PURE__ */ new Map();
|
|
75
|
+
const check = (key, selector) => {
|
|
76
|
+
if (isSegmentKey(key) && !available.has(key) && !seenBefore(reported, selector, key)) report({
|
|
77
|
+
key,
|
|
78
|
+
selector,
|
|
79
|
+
scope: [...available]
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
if (parent.stateAccessTracking === true) {
|
|
83
|
+
const base = baseListenerKey in parent ? parent[baseListenerKey] : parent.onStateAccess;
|
|
84
|
+
const onStateAccess = (key, selector) => {
|
|
85
|
+
check(key, selector);
|
|
86
|
+
base === null || base === void 0 || base(key, selector);
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
...parent,
|
|
90
|
+
onStateAccess,
|
|
91
|
+
[baseListenerKey]: base
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...parent,
|
|
96
|
+
store: createScopedStore(parent.store, (key) => check(key, void 0))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/react/createSparseReact.ts
|
|
101
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
102
|
+
const segmentIds = /* @__PURE__ */ new WeakMap();
|
|
103
|
+
let nextSegmentId = 0;
|
|
104
|
+
/** Identifies a list of segments by the identity of its members. */
|
|
105
|
+
const signatureOf = (segments) => segments.map((segment) => {
|
|
106
|
+
let id = segmentIds.get(segment);
|
|
107
|
+
if (id === void 0) {
|
|
108
|
+
id = nextSegmentId++;
|
|
109
|
+
segmentIds.set(segment, id);
|
|
110
|
+
}
|
|
111
|
+
return id;
|
|
112
|
+
}).join(",");
|
|
113
|
+
/**
|
|
114
|
+
* The same array for as long as it holds the same segments, so that a new
|
|
115
|
+
* array literal on every render does not acquire them again.
|
|
116
|
+
*/
|
|
117
|
+
const useStableSegments = (segments) => {
|
|
118
|
+
const signature = signatureOf(segments);
|
|
119
|
+
return useMemo(() => segments, [signature]);
|
|
120
|
+
};
|
|
121
|
+
const collectKeys = (segments, keys) => {
|
|
122
|
+
for (const segment of segments) {
|
|
123
|
+
keys.add(segment.key);
|
|
124
|
+
collectKeys(segment.deps, keys);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
/** Creates the React bindings for stores made with `sparseEnhancer`. */
|
|
128
|
+
function createSparseReact(options = {}) {
|
|
129
|
+
const { checks = process.env.NODE_ENV !== "production", onViolation = reportViolationOnce } = options;
|
|
130
|
+
const getContext = () => {
|
|
131
|
+
var _options$context;
|
|
132
|
+
return (_options$context = options.context) !== null && _options$context !== void 0 ? _options$context : getReactReduxContext();
|
|
133
|
+
};
|
|
134
|
+
const useSparseController = () => {
|
|
135
|
+
const value = useContext(getContext());
|
|
136
|
+
if (!value) throw new Error("redux-sparse-kit needs a react-redux <Provider> above this component");
|
|
137
|
+
if (!value.store.sparse) throw new Error("redux-sparse-kit needs a store created with sparseEnhancer");
|
|
138
|
+
return value.store.sparse;
|
|
139
|
+
};
|
|
140
|
+
const useSparseSegments = (segments) => {
|
|
141
|
+
const sparse = useSparseController();
|
|
142
|
+
const [, rerender] = useReducer((count) => count + 1, 0);
|
|
143
|
+
const ready = segments.every((segment) => sparse.has(segment));
|
|
144
|
+
const stable = useStableSegments(segments);
|
|
145
|
+
useIsomorphicLayoutEffect(() => {
|
|
146
|
+
const release = sparse.acquire(...stable);
|
|
147
|
+
if (!ready) rerender();
|
|
148
|
+
return release;
|
|
149
|
+
}, [sparse, stable]);
|
|
150
|
+
return ready;
|
|
151
|
+
};
|
|
152
|
+
function SparseScope({ segments, fallback = null, children }) {
|
|
153
|
+
return useSparseSegments(segments) ? children : fallback;
|
|
154
|
+
}
|
|
155
|
+
function CheckedSparseScope({ segments, fallback = null, children }) {
|
|
156
|
+
const ready = useSparseSegments(segments);
|
|
157
|
+
const context = getContext();
|
|
158
|
+
const parent = useContext(context);
|
|
159
|
+
const parentKeys = useContext(ScopeKeysContext);
|
|
160
|
+
const stable = useStableSegments(segments);
|
|
161
|
+
const available = useMemo(() => {
|
|
162
|
+
const keys = new Set(parentKeys);
|
|
163
|
+
collectKeys(stable, keys);
|
|
164
|
+
return keys;
|
|
165
|
+
}, [parentKeys, stable]);
|
|
166
|
+
const value = useMemo(() => createCheckedContextValue(parent, available, onViolation), [parent, available]);
|
|
167
|
+
if (!ready) return fallback;
|
|
168
|
+
return createElement(ScopeKeysContext.Provider, { value: available }, createElement(context.Provider, { value }, children));
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
useSparseSegments,
|
|
172
|
+
SparseScope: checks ? CheckedSparseScope : SparseScope
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/react/index.ts
|
|
177
|
+
const defaults = createSparseReact();
|
|
178
|
+
/** {@inheritDoc SparseReact.SparseScope} */
|
|
179
|
+
const SparseScope = defaults.SparseScope;
|
|
180
|
+
/** {@inheritDoc SparseReact.useSparseSegments} */
|
|
181
|
+
const useSparseSegments = defaults.useSparseSegments;
|
|
182
|
+
//#endregion
|
|
183
|
+
export { SparseScope, createSparseReact, getReactReduxContext, useSparseSegments };
|
|
184
|
+
|
|
185
|
+
//# sourceMappingURL=react.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/segment.ts
|
|
2
|
+
const definedKeys = /* @__PURE__ */ new Set();
|
|
3
|
+
/**
|
|
4
|
+
* Whether a segment with a reducer has been defined under this key. Lets dev
|
|
5
|
+
* checks tell a read of a segment's slice from a read of static state.
|
|
6
|
+
*/
|
|
7
|
+
const isSegmentKey = (key) => definedKeys.has(key);
|
|
8
|
+
/**
|
|
9
|
+
* Defines a segment: a reducer, the epics that drive it and the segments it
|
|
10
|
+
* depends on, injected together and evicted together. Each part is optional.
|
|
11
|
+
*
|
|
12
|
+
* A dependency has to be defined before the segment that lists it, so the
|
|
13
|
+
* dependency graph cannot contain a cycle.
|
|
14
|
+
*/
|
|
15
|
+
function defineSegment(options) {
|
|
16
|
+
const { key, reducer, deps = [], epics = [], idleTimeout, whileIdle } = options;
|
|
17
|
+
if (typeof key !== "string" || key === "") throw new TypeError("A segment key must be a non-empty string");
|
|
18
|
+
if (reducer !== void 0 && typeof reducer !== "function") throw new TypeError(`The reducer of segment "${key}" must be a function`);
|
|
19
|
+
if (idleTimeout !== void 0 && !(idleTimeout >= 0)) throw new RangeError(`The idle timeout of segment "${key}" must be a non-negative number`);
|
|
20
|
+
if (reducer !== void 0) definedKeys.add(key);
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
key,
|
|
23
|
+
reducer,
|
|
24
|
+
deps: Object.freeze([...deps]),
|
|
25
|
+
epics: Object.freeze([...epics]),
|
|
26
|
+
idleTimeout,
|
|
27
|
+
whileIdle
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { isSegmentKey as n, defineSegment as t };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=segment-DFAgPcwA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-DFAgPcwA.mjs","names":[],"sources":["../src/segment.ts"],"sourcesContent":["import type { Reducer } from 'redux';\n\n/**\n * The state that is always present in the store, outside of any segment.\n * Register it once so that segment epics can read it:\n *\n * ```ts\n * declare module 'redux-sparse-kit' {\n * interface StaticState extends RootStaticState {}\n * }\n * ```\n */\n// oxlint-disable-next-line typescript/no-empty-interface, typescript/no-empty-object-type\nexport interface StaticState {}\n\n/**\n * What an idle segment (one no longer held by anything) does until it is\n * evicted:\n * - `keep-running` - its reducer and epics keep running, so its state stays\n * current.\n * - `pause-epics` - its reducer keeps running, its epics are stopped and\n * started again when the segment is held again.\n * - `freeze` - its epics are stopped and its reducer is skipped, so its state\n * is kept exactly as it was.\n */\nexport type WhileIdle = 'keep-running' | 'pause-epics' | 'freeze';\n\n/**\n * An epic as far as the core is concerned: a function the epic runner starts.\n * The `redux-sparse-kit/observable` entry point runs redux-observable epics.\n */\nexport type SegmentEpic = (...args: any[]) => unknown;\n\n/**\n * A reducer, the epics that drive it and the segments it depends on, injected\n * together and evicted together. Its `State` is `never` when it has no\n * reducer: it then owns no slice of the state.\n */\nexport interface Segment<\n Key extends string = string,\n State = any,\n Deps extends readonly AnySegment[] = readonly AnySegment[],\n> {\n /**\n * Names the segment, and is the top-level key of the state its reducer\n * owns when it has one.\n */\n readonly key: Key;\n readonly reducer: unknown extends State\n ? Reducer<any> | undefined\n : [State] extends [never]\n ? undefined\n : Reducer<State>;\n /** Segments that must be present for as long as this one is. */\n readonly deps: Deps;\n readonly epics: readonly SegmentEpic[];\n /** Overrides the store's idle timeout for this segment, in milliseconds. */\n readonly idleTimeout: number | undefined;\n /** Overrides the store's idle behaviour for this segment. */\n readonly whileIdle: WhileIdle | undefined;\n}\n\n/**\n * Any segment, with or without a reducer. Spelled out rather than written as\n * `Segment<string, any>`, which a segment without a reducer would not match.\n */\nexport interface AnySegment {\n readonly key: string;\n readonly reducer: Reducer<any> | undefined;\n readonly deps: readonly AnySegment[];\n readonly epics: readonly SegmentEpic[];\n readonly idleTimeout: number | undefined;\n readonly whileIdle: WhileIdle | undefined;\n}\n\ntype SliceKeyOf<S> =\n S extends Segment<infer Key, infer State, any> ? ([State] extends [never] ? never : Key) : never;\n\n/**\n * The state slice each of the given segments contributes to the store;\n * segments without a reducer contribute none.\n */\nexport type StateOfSegments<Segments extends AnySegment> = {\n [S in Segments as SliceKeyOf<S>]: S extends Segment<string, infer State, any> ? State : never;\n};\n\ntype OwnSliceOf<Key extends string, State> = [State] extends [never] ? {} : { [K in Key]: State };\n\n/**\n * The state a segment may read: the static state, its own slice and the\n * slices of its direct dependencies.\n */\nexport type ScopeStateOf<S extends AnySegment> = StaticState &\n StateOfSegments<S | S['deps'][number]>;\n\ntype EpicStateOf<Epic> = Epic extends (action$: any, state$: infer State$, ...rest: any[]) => any\n ? State$ extends { readonly value: infer State }\n ? State\n : unknown\n : unknown;\n\ndeclare const segmentEpicError: unique symbol;\n\n/**\n * Shown when an epic declares a state the segment cannot guarantee: it reads\n * a segment that is neither the segment itself nor one of its dependencies.\n */\nexport interface SegmentEpicStateError<Readable, Declared> {\n readonly [segmentEpicError]: 'The epic reads state outside the segment and its deps';\n readonly readable: Readable;\n readonly declared: Declared;\n}\n\n/** An epic typed with a `void`, `unknown` or `any` state does not read it. */\ntype ReadsState<State> = [State] extends [void] ? false : unknown extends State ? false : true;\n\ntype ValidateEpics<Epics extends readonly unknown[], Scope> = {\n [I in keyof Epics]: ReadsState<EpicStateOf<Epics[I]>> extends false\n ? unknown\n : Scope extends EpicStateOf<Epics[I]>\n ? unknown\n : SegmentEpicStateError<Scope, EpicStateOf<Epics[I]>>;\n};\n\nexport interface SegmentOptions<\n Key extends string,\n State,\n Deps extends readonly AnySegment[],\n Epics extends readonly SegmentEpic[],\n> {\n key: Key;\n /** Omitted for a segment that only runs epics or only bundles its deps. */\n reducer?: Reducer<State>;\n deps?: Deps;\n epics?: Epics &\n ValidateEpics<Epics, StaticState & StateOfSegments<Deps[number]> & OwnSliceOf<Key, State>>;\n idleTimeout?: number;\n whileIdle?: WhileIdle;\n}\n\nconst definedKeys = new Set<string>();\n\n/**\n * Whether a segment with a reducer has been defined under this key. Lets dev\n * checks tell a read of a segment's slice from a read of static state.\n */\nexport const isSegmentKey = (key: string): boolean => definedKeys.has(key);\n\n/**\n * Defines a segment: a reducer, the epics that drive it and the segments it\n * depends on, injected together and evicted together. Each part is optional.\n *\n * A dependency has to be defined before the segment that lists it, so the\n * dependency graph cannot contain a cycle.\n */\nexport function defineSegment<\n const Key extends string,\n State = never,\n const Deps extends readonly AnySegment[] = [],\n const Epics extends readonly SegmentEpic[] = [],\n>(options: SegmentOptions<Key, State, Deps, Epics>): Segment<Key, State, Deps> {\n const {\n key,\n reducer,\n deps = [] as unknown as Deps,\n epics = [],\n idleTimeout,\n whileIdle,\n } = options;\n\n if (typeof key !== 'string' || key === '') {\n throw new TypeError('A segment key must be a non-empty string');\n }\n\n if (reducer !== undefined && typeof reducer !== 'function') {\n throw new TypeError(`The reducer of segment \"${key}\" must be a function`);\n }\n\n if (idleTimeout !== undefined && !(idleTimeout >= 0)) {\n throw new RangeError(`The idle timeout of segment \"${key}\" must be a non-negative number`);\n }\n\n if (reducer !== undefined) {\n definedKeys.add(key);\n }\n\n const segment = Object.freeze({\n key,\n reducer,\n deps: Object.freeze([...deps]) as unknown as Deps,\n epics: Object.freeze([...(epics as readonly SegmentEpic[])]),\n idleTimeout,\n whileIdle,\n });\n\n return segment as Segment<Key, State, Deps>;\n}\n"],"mappings":";AA4IA,MAAM,8BAAc,IAAI,IAAY;;;;;AAMpC,MAAa,gBAAgB,QAAyB,YAAY,IAAI,GAAG;;;;;;;;AASzE,SAAgB,cAKd,SAA6E;CAC7E,MAAM,EACJ,KACA,SACA,OAAO,CAAC,GACR,QAAQ,CAAC,GACT,aACA,cACE;CAEJ,IAAI,OAAO,QAAQ,YAAY,QAAQ,IACrC,MAAM,IAAI,UAAU,0CAA0C;CAGhE,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,YAC9C,MAAM,IAAI,UAAU,2BAA2B,IAAI,qBAAqB;CAG1E,IAAI,gBAAgB,KAAA,KAAa,EAAE,eAAe,IAChD,MAAM,IAAI,WAAW,gCAAgC,IAAI,gCAAgC;CAG3F,IAAI,YAAY,KAAA,GACd,YAAY,IAAI,GAAG;CAYrB,OATgB,OAAO,OAAO;EAC5B;EACA;EACA,MAAM,OAAO,OAAO,CAAC,GAAG,IAAI,CAAC;EAC7B,OAAO,OAAO,OAAO,CAAC,GAAI,KAAgC,CAAC;EAC3D;EACA;CACF,CAEa;AACf"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/segment.ts
|
|
2
|
+
const definedKeys = /* @__PURE__ */ new Set();
|
|
3
|
+
/**
|
|
4
|
+
* Whether a segment with a reducer has been defined under this key. Lets dev
|
|
5
|
+
* checks tell a read of a segment's slice from a read of static state.
|
|
6
|
+
*/
|
|
7
|
+
const isSegmentKey = (key) => definedKeys.has(key);
|
|
8
|
+
/**
|
|
9
|
+
* Defines a segment: a reducer, the epics that drive it and the segments it
|
|
10
|
+
* depends on, injected together and evicted together. Each part is optional.
|
|
11
|
+
*
|
|
12
|
+
* A dependency has to be defined before the segment that lists it, so the
|
|
13
|
+
* dependency graph cannot contain a cycle.
|
|
14
|
+
*/
|
|
15
|
+
function defineSegment(options) {
|
|
16
|
+
const { key, reducer, deps = [], epics = [], idleTimeout, whileIdle } = options;
|
|
17
|
+
if (typeof key !== "string" || key === "") throw new TypeError("A segment key must be a non-empty string");
|
|
18
|
+
if (reducer !== void 0 && typeof reducer !== "function") throw new TypeError(`The reducer of segment "${key}" must be a function`);
|
|
19
|
+
if (idleTimeout !== void 0 && !(idleTimeout >= 0)) throw new RangeError(`The idle timeout of segment "${key}" must be a non-negative number`);
|
|
20
|
+
if (reducer !== void 0) definedKeys.add(key);
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
key,
|
|
23
|
+
reducer,
|
|
24
|
+
deps: Object.freeze([...deps]),
|
|
25
|
+
epics: Object.freeze([...epics]),
|
|
26
|
+
idleTimeout,
|
|
27
|
+
whileIdle
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
Object.defineProperty(exports, "defineSegment", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
get: function() {
|
|
34
|
+
return defineSegment;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(exports, "isSegmentKey", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
get: function() {
|
|
40
|
+
return isSegmentKey;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=segment-Dqd6-ABS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-Dqd6-ABS.js","names":[],"sources":["../src/segment.ts"],"sourcesContent":["import type { Reducer } from 'redux';\n\n/**\n * The state that is always present in the store, outside of any segment.\n * Register it once so that segment epics can read it:\n *\n * ```ts\n * declare module 'redux-sparse-kit' {\n * interface StaticState extends RootStaticState {}\n * }\n * ```\n */\n// oxlint-disable-next-line typescript/no-empty-interface, typescript/no-empty-object-type\nexport interface StaticState {}\n\n/**\n * What an idle segment (one no longer held by anything) does until it is\n * evicted:\n * - `keep-running` - its reducer and epics keep running, so its state stays\n * current.\n * - `pause-epics` - its reducer keeps running, its epics are stopped and\n * started again when the segment is held again.\n * - `freeze` - its epics are stopped and its reducer is skipped, so its state\n * is kept exactly as it was.\n */\nexport type WhileIdle = 'keep-running' | 'pause-epics' | 'freeze';\n\n/**\n * An epic as far as the core is concerned: a function the epic runner starts.\n * The `redux-sparse-kit/observable` entry point runs redux-observable epics.\n */\nexport type SegmentEpic = (...args: any[]) => unknown;\n\n/**\n * A reducer, the epics that drive it and the segments it depends on, injected\n * together and evicted together. Its `State` is `never` when it has no\n * reducer: it then owns no slice of the state.\n */\nexport interface Segment<\n Key extends string = string,\n State = any,\n Deps extends readonly AnySegment[] = readonly AnySegment[],\n> {\n /**\n * Names the segment, and is the top-level key of the state its reducer\n * owns when it has one.\n */\n readonly key: Key;\n readonly reducer: unknown extends State\n ? Reducer<any> | undefined\n : [State] extends [never]\n ? undefined\n : Reducer<State>;\n /** Segments that must be present for as long as this one is. */\n readonly deps: Deps;\n readonly epics: readonly SegmentEpic[];\n /** Overrides the store's idle timeout for this segment, in milliseconds. */\n readonly idleTimeout: number | undefined;\n /** Overrides the store's idle behaviour for this segment. */\n readonly whileIdle: WhileIdle | undefined;\n}\n\n/**\n * Any segment, with or without a reducer. Spelled out rather than written as\n * `Segment<string, any>`, which a segment without a reducer would not match.\n */\nexport interface AnySegment {\n readonly key: string;\n readonly reducer: Reducer<any> | undefined;\n readonly deps: readonly AnySegment[];\n readonly epics: readonly SegmentEpic[];\n readonly idleTimeout: number | undefined;\n readonly whileIdle: WhileIdle | undefined;\n}\n\ntype SliceKeyOf<S> =\n S extends Segment<infer Key, infer State, any> ? ([State] extends [never] ? never : Key) : never;\n\n/**\n * The state slice each of the given segments contributes to the store;\n * segments without a reducer contribute none.\n */\nexport type StateOfSegments<Segments extends AnySegment> = {\n [S in Segments as SliceKeyOf<S>]: S extends Segment<string, infer State, any> ? State : never;\n};\n\ntype OwnSliceOf<Key extends string, State> = [State] extends [never] ? {} : { [K in Key]: State };\n\n/**\n * The state a segment may read: the static state, its own slice and the\n * slices of its direct dependencies.\n */\nexport type ScopeStateOf<S extends AnySegment> = StaticState &\n StateOfSegments<S | S['deps'][number]>;\n\ntype EpicStateOf<Epic> = Epic extends (action$: any, state$: infer State$, ...rest: any[]) => any\n ? State$ extends { readonly value: infer State }\n ? State\n : unknown\n : unknown;\n\ndeclare const segmentEpicError: unique symbol;\n\n/**\n * Shown when an epic declares a state the segment cannot guarantee: it reads\n * a segment that is neither the segment itself nor one of its dependencies.\n */\nexport interface SegmentEpicStateError<Readable, Declared> {\n readonly [segmentEpicError]: 'The epic reads state outside the segment and its deps';\n readonly readable: Readable;\n readonly declared: Declared;\n}\n\n/** An epic typed with a `void`, `unknown` or `any` state does not read it. */\ntype ReadsState<State> = [State] extends [void] ? false : unknown extends State ? false : true;\n\ntype ValidateEpics<Epics extends readonly unknown[], Scope> = {\n [I in keyof Epics]: ReadsState<EpicStateOf<Epics[I]>> extends false\n ? unknown\n : Scope extends EpicStateOf<Epics[I]>\n ? unknown\n : SegmentEpicStateError<Scope, EpicStateOf<Epics[I]>>;\n};\n\nexport interface SegmentOptions<\n Key extends string,\n State,\n Deps extends readonly AnySegment[],\n Epics extends readonly SegmentEpic[],\n> {\n key: Key;\n /** Omitted for a segment that only runs epics or only bundles its deps. */\n reducer?: Reducer<State>;\n deps?: Deps;\n epics?: Epics &\n ValidateEpics<Epics, StaticState & StateOfSegments<Deps[number]> & OwnSliceOf<Key, State>>;\n idleTimeout?: number;\n whileIdle?: WhileIdle;\n}\n\nconst definedKeys = new Set<string>();\n\n/**\n * Whether a segment with a reducer has been defined under this key. Lets dev\n * checks tell a read of a segment's slice from a read of static state.\n */\nexport const isSegmentKey = (key: string): boolean => definedKeys.has(key);\n\n/**\n * Defines a segment: a reducer, the epics that drive it and the segments it\n * depends on, injected together and evicted together. Each part is optional.\n *\n * A dependency has to be defined before the segment that lists it, so the\n * dependency graph cannot contain a cycle.\n */\nexport function defineSegment<\n const Key extends string,\n State = never,\n const Deps extends readonly AnySegment[] = [],\n const Epics extends readonly SegmentEpic[] = [],\n>(options: SegmentOptions<Key, State, Deps, Epics>): Segment<Key, State, Deps> {\n const {\n key,\n reducer,\n deps = [] as unknown as Deps,\n epics = [],\n idleTimeout,\n whileIdle,\n } = options;\n\n if (typeof key !== 'string' || key === '') {\n throw new TypeError('A segment key must be a non-empty string');\n }\n\n if (reducer !== undefined && typeof reducer !== 'function') {\n throw new TypeError(`The reducer of segment \"${key}\" must be a function`);\n }\n\n if (idleTimeout !== undefined && !(idleTimeout >= 0)) {\n throw new RangeError(`The idle timeout of segment \"${key}\" must be a non-negative number`);\n }\n\n if (reducer !== undefined) {\n definedKeys.add(key);\n }\n\n const segment = Object.freeze({\n key,\n reducer,\n deps: Object.freeze([...deps]) as unknown as Deps,\n epics: Object.freeze([...(epics as readonly SegmentEpic[])]),\n idleTimeout,\n whileIdle,\n });\n\n return segment as Segment<Key, State, Deps>;\n}\n"],"mappings":";AA4IA,MAAM,8BAAc,IAAI,IAAY;;;;;AAMpC,MAAa,gBAAgB,QAAyB,YAAY,IAAI,GAAG;;;;;;;;AASzE,SAAgB,cAKd,SAA6E;CAC7E,MAAM,EACJ,KACA,SACA,OAAO,CAAC,GACR,QAAQ,CAAC,GACT,aACA,cACE;CAEJ,IAAI,OAAO,QAAQ,YAAY,QAAQ,IACrC,MAAM,IAAI,UAAU,0CAA0C;CAGhE,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,YAC9C,MAAM,IAAI,UAAU,2BAA2B,IAAI,qBAAqB;CAG1E,IAAI,gBAAgB,KAAA,KAAa,EAAE,eAAe,IAChD,MAAM,IAAI,WAAW,gCAAgC,IAAI,gCAAgC;CAG3F,IAAI,YAAY,KAAA,GACd,YAAY,IAAI,GAAG;CAYrB,OATgB,OAAO,OAAO;EAC5B;EACA;EACA,MAAM,OAAO,OAAO,CAAC,GAAG,IAAI,CAAC;EAC7B,OAAO,OAAO,OAAO,CAAC,GAAI,KAAgC,CAAC;EAC3D;EACA;CACF,CAEa;AACf"}
|