@radio-garden/rematch 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +445 -0
- package/dist/index.d.ts +543 -0
- package/dist/index.js +287 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// ../util/src/validation/type-guards.ts
|
|
2
|
+
function isFunction(value) {
|
|
3
|
+
return !!value && typeof value === "function";
|
|
4
|
+
}
|
|
5
|
+
function isNullish(val) {
|
|
6
|
+
return val === void 0 || val === null;
|
|
7
|
+
}
|
|
8
|
+
function isNotNullish(val) {
|
|
9
|
+
return !isNullish(val);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ../util/src/validation/assert.ts
|
|
13
|
+
function assert(condition, message = "assertion error") {
|
|
14
|
+
if (!condition) {
|
|
15
|
+
throw new Error(message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ../util/src/validation/guard.ts
|
|
20
|
+
function guarded(value, check, message) {
|
|
21
|
+
guard(value, check, message);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function guard(value, check, message = `Type guard check failed for value: ${value}`) {
|
|
25
|
+
assert(check(value), message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ../util/src/validation/assertions.ts
|
|
29
|
+
function assertedNotNullish(val, message = "Expected value to not be nullish") {
|
|
30
|
+
return guarded(val, isNotNullish, message);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/store.ts
|
|
34
|
+
import * as Redux from "redux";
|
|
35
|
+
import { createSelector } from "reselect";
|
|
36
|
+
|
|
37
|
+
// src/validate.ts
|
|
38
|
+
var validate = process.env.NODE_ENV === "production" ? void 0 : (run) => {
|
|
39
|
+
const errors = run().filter(([isInvalid]) => isInvalid).map(([, errorMessage]) => errorMessage);
|
|
40
|
+
if (errors.length > 0) {
|
|
41
|
+
throw new Error(errors.join(", "));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/store.ts
|
|
46
|
+
var INITIAL_SENTINEL = /* @__PURE__ */ Symbol("INITIAL_SENTINEL");
|
|
47
|
+
function isRematchAction(action) {
|
|
48
|
+
return typeof action === "object" && action !== null && "type" in action && typeof action.type === "string";
|
|
49
|
+
}
|
|
50
|
+
var unnamedStoreCount = 0;
|
|
51
|
+
function createRematchStore(initConfig) {
|
|
52
|
+
const allReducers = {};
|
|
53
|
+
const allEffects = {};
|
|
54
|
+
const config = normalizeConfig(initConfig);
|
|
55
|
+
validate?.(() => [
|
|
56
|
+
[
|
|
57
|
+
!Array.isArray(config.redux.middlewares),
|
|
58
|
+
"init config.redux.middlewares must be an array"
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
!Array.isArray(config.redux.enhancers),
|
|
62
|
+
"init config.redux.enhancers must be an array of functions"
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
config.redux.createStore && typeof config.redux.createStore !== "function",
|
|
66
|
+
"init config.redux.createStore must be a function"
|
|
67
|
+
]
|
|
68
|
+
]);
|
|
69
|
+
const watchSelector = /* @__PURE__ */ (() => {
|
|
70
|
+
let watchers = [];
|
|
71
|
+
let timeoutId;
|
|
72
|
+
let subscribed = false;
|
|
73
|
+
function checkWatchers() {
|
|
74
|
+
timeoutId = void 0;
|
|
75
|
+
const state = getState();
|
|
76
|
+
for (const watcher of watchers) {
|
|
77
|
+
if (!watcher.active) continue;
|
|
78
|
+
const current = watcher.selector(state);
|
|
79
|
+
if (current !== watcher.prev) {
|
|
80
|
+
const prevValue = watcher.prev === INITIAL_SENTINEL ? current : watcher.prev;
|
|
81
|
+
void watcher.onChange(current, prevValue);
|
|
82
|
+
watcher.prev = current;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function scheduleCheck() {
|
|
87
|
+
timeoutId ??= setTimeout(checkWatchers, 0);
|
|
88
|
+
}
|
|
89
|
+
return (selector2, onChange, { initial = false }) => {
|
|
90
|
+
if (!subscribed) {
|
|
91
|
+
subscribed = true;
|
|
92
|
+
reduxSubscribe(scheduleCheck);
|
|
93
|
+
}
|
|
94
|
+
const watcher = {
|
|
95
|
+
selector: selector2,
|
|
96
|
+
prev: initial ? INITIAL_SENTINEL : selector2(getState()),
|
|
97
|
+
onChange,
|
|
98
|
+
active: true
|
|
99
|
+
};
|
|
100
|
+
watchers.push(watcher);
|
|
101
|
+
if (initial) {
|
|
102
|
+
scheduleCheck();
|
|
103
|
+
}
|
|
104
|
+
return () => {
|
|
105
|
+
watcher.active = false;
|
|
106
|
+
watchers = watchers.filter((w) => w.active);
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
})();
|
|
110
|
+
const selector = {};
|
|
111
|
+
const select = {};
|
|
112
|
+
const dispatch = {};
|
|
113
|
+
const store = {
|
|
114
|
+
selector,
|
|
115
|
+
select,
|
|
116
|
+
dispatch,
|
|
117
|
+
getState: () => getState(),
|
|
118
|
+
subscribe: (arg) => reduxSubscribe(arg),
|
|
119
|
+
name: config.name,
|
|
120
|
+
createSelector(callback) {
|
|
121
|
+
let selectorFn;
|
|
122
|
+
return (state) => (selectorFn ??= callback(createSelector))(state);
|
|
123
|
+
},
|
|
124
|
+
watchSelector(selectorOrSelectors, callback, watchConfig) {
|
|
125
|
+
return watchSelector(
|
|
126
|
+
Array.isArray(selectorOrSelectors) ? createSelector(selectorOrSelectors, (...args) => args) : selectorOrSelectors,
|
|
127
|
+
callback,
|
|
128
|
+
{ ...watchConfig }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
for (const [name, m] of Object.entries(config.models)) {
|
|
133
|
+
const model = assertedNotNullish(m);
|
|
134
|
+
const reducers = model.reducers ?? {};
|
|
135
|
+
let effects = isFunction(model.effects) ? void 0 : model.effects;
|
|
136
|
+
const { state, selectors } = model;
|
|
137
|
+
validate?.(() => [
|
|
138
|
+
[typeof name !== "string", 'model "name" [string] is required'],
|
|
139
|
+
[state === void 0, 'model "state" is required']
|
|
140
|
+
]);
|
|
141
|
+
const modelReducers = {};
|
|
142
|
+
dispatch[name] = {};
|
|
143
|
+
for (const reducerKey of Object.keys(reducers)) {
|
|
144
|
+
const type = reducerKey.includes("/") ? reducerKey : `${name}/${reducerKey}`;
|
|
145
|
+
modelReducers[type] = reducers[reducerKey];
|
|
146
|
+
dispatch[name][reducerKey] = (payload, meta) => reduxDispatch({
|
|
147
|
+
type,
|
|
148
|
+
payload,
|
|
149
|
+
meta
|
|
150
|
+
});
|
|
151
|
+
validate?.(() => [
|
|
152
|
+
[!!reducerKey.match(/\/.+\//), `Invalid reducer name (${type})`],
|
|
153
|
+
[
|
|
154
|
+
typeof modelReducers[type] !== "function",
|
|
155
|
+
`Invalid reducer (${type}). Must be a function`
|
|
156
|
+
]
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
allReducers[name] = (reducerState = model.state, {
|
|
160
|
+
type,
|
|
161
|
+
payload,
|
|
162
|
+
meta
|
|
163
|
+
}) => modelReducers[type] ? modelReducers[type](reducerState, payload, meta) : reducerState;
|
|
164
|
+
const selectorProps = {};
|
|
165
|
+
const selectProps = {};
|
|
166
|
+
for (const key of Object.keys(state)) {
|
|
167
|
+
selectorProps[key] = (rootState) => (
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- accessing dynamic key from model state
|
|
169
|
+
rootState[name][key]
|
|
170
|
+
);
|
|
171
|
+
selectProps[key] = () => (
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- accessing dynamic key from model state
|
|
173
|
+
getState()[name][key]
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const resolvedSelectors = isFunction(selectors) ? (
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- store is built incrementally, TypeScript can't verify it matches RematchStore yet
|
|
178
|
+
selectors(store)
|
|
179
|
+
) : selectors ?? {};
|
|
180
|
+
for (const key of Object.keys(resolvedSelectors)) {
|
|
181
|
+
const selectorFn = resolvedSelectors[key];
|
|
182
|
+
selectorProps[key] = selectorFn;
|
|
183
|
+
selectProps[key] = () => selectorFn(getState());
|
|
184
|
+
}
|
|
185
|
+
const modelSelector = new Proxy((rootState) => rootState[name], {
|
|
186
|
+
get(target, prop, receiver) {
|
|
187
|
+
if (typeof prop === "string" && prop in selectorProps) {
|
|
188
|
+
return selectorProps[prop];
|
|
189
|
+
}
|
|
190
|
+
return Reflect.get(target, prop, receiver);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const modelSelect = new Proxy(() => getState()[name], {
|
|
194
|
+
get(target, prop, receiver) {
|
|
195
|
+
if (typeof prop === "string" && prop in selectProps) {
|
|
196
|
+
return selectProps[prop];
|
|
197
|
+
}
|
|
198
|
+
return Reflect.get(target, prop, receiver);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
selector[name] = modelSelector;
|
|
202
|
+
select[name] = modelSelect;
|
|
203
|
+
if (isFunction(model.effects)) {
|
|
204
|
+
effects = model.effects(store);
|
|
205
|
+
}
|
|
206
|
+
if (effects) {
|
|
207
|
+
const dispatcher = dispatch[name];
|
|
208
|
+
for (const [effectName, effect] of Object.entries(effects)) {
|
|
209
|
+
const type = `${name}/${effectName}`;
|
|
210
|
+
validate?.(() => [
|
|
211
|
+
[!!effectName.match(/\//), `Invalid effect name (${type})`],
|
|
212
|
+
[
|
|
213
|
+
typeof effect !== "function",
|
|
214
|
+
`Invalid effect (${type}). Must be a function`
|
|
215
|
+
]
|
|
216
|
+
]);
|
|
217
|
+
const boundEffect = effect.bind(
|
|
218
|
+
dispatcher
|
|
219
|
+
);
|
|
220
|
+
allEffects[type] = boundEffect;
|
|
221
|
+
dispatcher[effectName] = (payload, meta) => reduxDispatch({
|
|
222
|
+
type,
|
|
223
|
+
payload,
|
|
224
|
+
meta
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const { devtoolOptions, devtoolCompose } = config.redux;
|
|
230
|
+
const composeWithDevTools = devtoolOptions?.disabled ? void 0 : devtoolCompose ?? (typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ?? void 0;
|
|
231
|
+
const {
|
|
232
|
+
getState,
|
|
233
|
+
subscribe: reduxSubscribe,
|
|
234
|
+
dispatch: reduxDispatch
|
|
235
|
+
} = Redux.legacy_createStore(
|
|
236
|
+
Redux.combineReducers(allReducers),
|
|
237
|
+
{},
|
|
238
|
+
(composeWithDevTools ? composeWithDevTools(devtoolOptions) : Redux.compose)(
|
|
239
|
+
Redux.applyMiddleware(
|
|
240
|
+
...config.redux.middlewares,
|
|
241
|
+
({ getState: getState2 }) => (next) => (action) => {
|
|
242
|
+
if (!isRematchAction(action)) {
|
|
243
|
+
return next(action);
|
|
244
|
+
}
|
|
245
|
+
if (action.type in allEffects) {
|
|
246
|
+
next(action);
|
|
247
|
+
return allEffects[action.type](
|
|
248
|
+
action.payload,
|
|
249
|
+
getState2(),
|
|
250
|
+
action.meta
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return next(action);
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
return store;
|
|
259
|
+
}
|
|
260
|
+
function normalizeConfig(initConfig) {
|
|
261
|
+
const {
|
|
262
|
+
name = `Rematch Store ${unnamedStoreCount++}`,
|
|
263
|
+
models,
|
|
264
|
+
redux
|
|
265
|
+
} = initConfig;
|
|
266
|
+
return {
|
|
267
|
+
name,
|
|
268
|
+
models: models ?? {},
|
|
269
|
+
redux: {
|
|
270
|
+
enhancers: [],
|
|
271
|
+
middlewares: [],
|
|
272
|
+
...redux,
|
|
273
|
+
devtoolOptions: {
|
|
274
|
+
name,
|
|
275
|
+
...redux?.devtoolOptions ?? {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/index.ts
|
|
282
|
+
var createModel = () => (mo) => mo;
|
|
283
|
+
export {
|
|
284
|
+
createModel,
|
|
285
|
+
createRematchStore
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../util/src/validation/type-guards.ts","../../util/src/validation/assert.ts","../../util/src/validation/guard.ts","../../util/src/validation/assertions.ts","../src/store.ts","../src/validate.ts","../src/index.ts"],"sourcesContent":["import type { AnyFunction } from '../typescript/index.ts';\n\n/**\n * Creates a type checking function for primitive types that also matches their Object wrappers.\n *\n * @param name - The type name (e.g., 'String', 'Number', 'Boolean')\n * @returns A type checking function\n * @internal\n */\nconst getPrimitiveCheck = /*#__PURE__*/ (() => {\n const { toString } = Object.prototype;\n\n // From @ditojs/utils\n return (name: string) => {\n // Create checking function for all primitive types (number, string, boolean)\n // that also matches their Object wrappers. We can't check `valueOf()` returns\n // here because `new Date().valueOf()` also returns a number.\n const typeName = name.toLowerCase();\n const toStringName = `[object ${name}]`;\n return function (arg: unknown) {\n const type = typeof arg;\n return (\n type === typeName ||\n (!!arg && type === 'object' && toString.call(arg) === toStringName)\n );\n };\n };\n})();\n\n/**\n * Type guard that checks if a value is an array where every element passes the provided type check.\n * This function combines array validation with element type validation in a single operation.\n *\n * @template T - The expected type of array elements after validation\n * @param value - The value to check (can be any type, but only arrays will pass)\n * @param check - A type guard function to validate each array element\n * @returns True if the value is an array and all elements pass the type check, false otherwise\n * @example\n * if (isEvery(data, isString)) {\n * // data is now typed as readonly string[]\n * console.log(data.map(s => s.toUpperCase()));\n * }\n *\n * @example\n * const mixedArray: unknown[] = [1, 2, 3];\n * if (isEvery(mixedArray, isNumber)) {\n * // mixedArray is now typed as readonly number[]\n * const sum = mixedArray.reduce((a, b) => a + b, 0);\n * }\n *\n * @example\n * // Custom type guard for specific object shape\n * const isUser = (obj: any): obj is { name: string; age: number } =>\n * isPlainObject(obj) && isString(obj.name) && isNumber(obj.age);\n * if (isEvery(data, isUser)) {\n * // data is now typed as readonly { name: string; age: number }[]\n * console.log(data.map(user => user.name));\n * }\n *\n * @example\n * isEvery([1, 2, 3], isNumber); // true\n * isEvery(['a', 'b'], isString); // true\n * isEvery([1, 'mixed'], isNumber); // false\n * isEvery([], isString); // true (empty array)\n * isEvery('not-array', isString); // false\n */\nexport function isEvery<T>(\n value: readonly unknown[] | unknown,\n check: (value: unknown) => value is T\n): value is readonly T[] {\n return isArray(value) && value.every(check);\n}\n\n/**\n * Type guard that checks if a value is an array.\n *\n * @template T - The expected type of array elements\n * @param value - The value to check\n * @returns True if the value is an array, false otherwise\n * @example\n * if (isArray(data)) {\n * // data is now typed as unknown[]\n * console.log(data.length);\n * }\n *\n * @example\n * const result = isArray<string>(value);\n * // Type guard for string arrays\n */\nexport function isArray<T>(value: unknown): value is T[] {\n return Array.isArray(value);\n}\n\n/**\n * Type guard that checks if a value is a plain object (not an array, function, class instance, etc.).\n * A plain object is created by Object literal syntax or Object constructor.\n *\n * @template Value - The expected type of object values\n * @param value - The value to check\n * @returns True if the value is a plain object, false otherwise\n * @example\n * if (isPlainObject(data)) {\n * // data is now typed as Record<PropertyKey, unknown>\n * console.log(Object.keys(data));\n * }\n *\n * @example\n * isPlainObject({}); // true\n * isPlainObject([]); // false\n * isPlainObject(new Date()); // false\n * isPlainObject(null); // false\n *\n * @see https://github.com/sindresorhus/is-plain-obj\n */\nexport function isPlainObject<Value>(\n value: unknown\n): value is Record<PropertyKey, Value> {\n // From: https://github.com/sindresorhus/is-plain-obj/blob/main/index.js\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const prototype = Object.getPrototypeOf(value);\n return (\n (prototype === null ||\n prototype === Object.prototype ||\n Object.getPrototypeOf(prototype) === null) &&\n !(Symbol.toStringTag in value) &&\n !(Symbol.iterator in value)\n );\n}\n\n/**\n * Type guard that checks if a value is a valid URL string.\n * Uses the native URL constructor to validate URL format.\n *\n * @param value - The value to check\n * @returns True if the value is a valid URL string, false otherwise\n * @example\n * if (isUrl(input)) {\n * // input is now typed as string and is a valid URL\n * const url = new URL(input);\n * }\n *\n * @example\n * isUrl('https://example.com'); // true\n * isUrl('ftp://files.example.com'); // true\n * isUrl('not-a-url'); // false\n * isUrl(123); // false\n */\nexport function isUrl(value: unknown): value is string {\n if (!isString(value)) {\n return false;\n }\n try {\n new URL(value);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Type guard that checks if a value is an Error object.\n *\n * @param value - The value to check\n * @returns True if the value is an Error object, false otherwise\n * @example\n * if (isError(caught)) {\n * // caught is now typed as Error\n * console.log(caught.message);\n * }\n *\n * @example\n * isError(new Error('test')); // true\n * isError(new TypeError('test')); // true\n * isError('error string'); // false\n */\nexport function isError(value: unknown): value is Error {\n return value instanceof Error;\n}\n\n/**\n * Type guard that checks if a value is a number (including Number objects).\n *\n * @param value - The value to check\n * @returns True if the value is a number, false otherwise\n * @example\n * if (isNumber(input)) {\n * // input is now typed as number\n * console.log(input.toFixed(2));\n * }\n *\n * @example\n * isNumber(42); // true\n * isNumber(new Number(42)); // true\n * isNumber('42'); // false\n * isNumber(NaN); // true (NaN is of type number)\n */\nexport function isNumber(value: unknown): value is number {\n return getPrimitiveCheck('Number')(value);\n}\n\n/**\n * Type guard that checks if a value is a string (including String objects).\n *\n * @param value - The value to check\n * @returns True if the value is a string, false otherwise\n * @example\n * if (isString(input)) {\n * // input is now typed as string\n * console.log(input.toUpperCase());\n * }\n *\n * @example\n * isString('hello'); // true\n * isString(new String('hello')); // true\n * isString(123); // false\n */\nexport function isString(value: unknown): value is string {\n return getPrimitiveCheck('String')(value);\n}\n\n/**\n * Type guard that checks if a value is a function.\n *\n * @param value - The value to check\n * @returns True if the value is a function, false otherwise\n * @example\n * if (isFunction(callback)) {\n * // callback is now typed as AnyFunction\n * callback();\n * }\n *\n * @example\n * isFunction(() => {}); // true\n * isFunction(function() {}); // true\n * isFunction(Math.max); // true\n * isFunction('function'); // false\n */\nexport function isFunction(value: unknown): value is AnyFunction {\n return !!value && typeof value === 'function';\n}\n\n/**\n * Type guard that checks if a value is an instance of a specific constructor.\n * This is a helper function for use with the three-argument version of safe().\n *\n * @template T - The constructor type\n * @param value - The value to check\n * @param constructor - The constructor to check against\n * @returns True if the value is an instance of the constructor, false otherwise\n * @example\n * const element = safe(value, isInstanceOf, HTMLElement);\n * const date = safe(value, isInstanceOf, Date);\n * const error = safe(value, isInstanceOf, Error);\n */\nexport function isInstanceOf<T extends new (...args: any[]) => any>(\n value: unknown,\n constructor: T\n): value is InstanceType<T> {\n return value instanceof constructor;\n}\n\n/**\n * Type guard that checks if a value is a boolean.\n * Only returns true for actual boolean values (true or false), not truthy/falsy values.\n *\n * @param value - The value to check\n * @returns True if the value is a boolean, false otherwise\n * @example\n * if (isBoolean(input)) {\n * // input is now typed as boolean\n * console.log(input ? 'yes' : 'no');\n * }\n *\n * @example\n * isBoolean(true); // true\n * isBoolean(false); // true\n * isBoolean(1); // false\n * isBoolean(0); // false\n * isBoolean('true'); // false\n */\nexport function isBoolean(value: unknown): value is boolean {\n return value === true || value === false;\n}\n\n/**\n * Type guard that checks if a value is nullish (null, undefined, or void).\n *\n * @template T - The original type that might be nullish\n * @param val - The value to check\n * @returns True if the value is nullish, false otherwise\n * @example\n * if (isNullish(user.avatar)) {\n * // user.avatar is null, undefined, or void\n * setDefaultAvatar();\n * }\n *\n * @example\n * isNullish(null); // true\n * isNullish(undefined); // true\n * isNullish(0); // false\n * isNullish(false); // false\n * isNullish(''); // false\n */\nexport function isNullish<T>(\n val: T | undefined | null | void\n): val is undefined | null | void {\n return val === undefined || val === null;\n}\n\n/**\n * Type guard that checks if a value is not nullish (not null, undefined, or void).\n *\n * @template T - The expected type when the value is not nullish\n * @param val - The value to check\n * @returns True if the value is not nullish, false otherwise\n * @example\n * if (isNotNullish(user.email)) {\n * // user.email is now typed without null/undefined\n * console.log(user.email.toLowerCase());\n * }\n *\n * @example\n * isNotNullish('hello'); // true\n * isNotNullish(0); // true\n * isNotNullish(false); // true\n * isNotNullish(null); // false\n * isNotNullish(undefined); // false\n */\nexport function isNotNullish<T>(val: T | undefined | null | void): val is T {\n return !isNullish(val);\n}\n\ntype Falsy = false | 0 | '' | null | undefined;\n\nexport type Nullish = null | undefined | void;\n\n/**\n * Type guard that checks if a value is truthy (not falsy).\n * Falsy values are: false, 0, '', null, undefined.\n *\n * @template T - The expected type when the value is truthy\n * @param val - The value to check\n * @returns True if the value is truthy, false otherwise\n * @example\n * if (isTruthy(user.name)) {\n * // user.name is now typed without falsy values\n * console.log(user.name.length);\n * }\n *\n * @example\n * isTruthy('hello'); // true\n * isTruthy(1); // true\n * isTruthy([]); // true\n * isTruthy(0); // false\n * isTruthy(''); // false\n * isTruthy(null); // false\n */\nexport function isTruthy<T>(val: T | Falsy): val is T {\n return !!val;\n}\n\n/**\n * Type guard that checks if a value is falsy.\n * Falsy values are: false, 0, '', null, undefined.\n *\n * @template T - The original type that might be falsy\n * @param val - The value to check\n * @returns True if the value is falsy, false otherwise\n * @example\n * if (isFalsy(response)) {\n * // response is now typed as Falsy\n * console.log('No response received');\n * }\n *\n * @example\n * isFalsy(false); // true\n * isFalsy(0); // true\n * isFalsy(''); // true\n * isFalsy(null); // true\n * isFalsy(undefined); // true\n * isFalsy('hello'); // false\n * isFalsy(1); // false\n */\nexport function isFalsy<T>(val: T | Falsy): val is Falsy {\n return !val;\n}\n","import { isFunction } from './type-guards.ts';\n\n/**\n * Asserts that a condition is truthy and throws an error if it's not.\n * Uses TypeScript assertion signatures to provide type narrowing.\n *\n * @param condition - The condition to assert\n * @param message - Optional error message (defaults to 'assertion error')\n * @throws Throws an Error if the condition is falsy\n * @example\n * assert(user.id, 'User ID is required');\n * assert(items.length > 0, 'Items array cannot be empty');\n *\n * @example\n * // TypeScript type narrowing\n * const value: string | null = getValue();\n * assert(value);\n * // value is now typed as string (not null)\n */\nexport function assert(\n condition: any,\n message = 'assertion error'\n): asserts condition {\n if (!condition) {\n throw new Error(message);\n }\n}\n\n/**\n * Asserts that a predicate function returns a truthy value for every item in an array.\n * This is useful for validating business rules or properties on an already-typed array.\n *\n * @template T - The type of items in the array.\n * @param array - The array to check.\n * @param predicate - A function that takes an item and its index and returns a condition to be asserted.\n * @param message - Optional. A static string or a function that returns a custom error message.\n * If it's a function, it receives the failing item and its index.\n * @throws Throws an Error if the predicate returns a falsy value for any item.\n * @example\n * // Basic validation\n * const userAges = [25, 30, 19];\n * assertEvery(userAges, (age) => age >= 18, 'All users must be 18 or older.');\n *\n * @example\n * // Validating non-empty strings\n * const usernames = ['Alice', 'Bob', ''];\n * try {\n * assertEvery(usernames, (name) => name.length > 0);\n * } catch (e) {\n * // Throws: Assertion failed for item \"\" at index 2.\n * }\n *\n * @example\n * // With a dynamic error message\n * const products = [\n * { name: 'Thing 1', stock: 10 },\n * { name: 'Thing 2', stock: 0 },\n * ];\n * assertEvery(\n * products,\n * (p) => p.stock > 0,\n * (p) => `Product \"${p.name}\" is out of stock.`\n * );\n * // Throws: Product \"Thing 2\" is out of stock.\n */\nexport function assertEvery<T>(\n array: readonly T[],\n predicate: (item: T, index: number) => any,\n message?: string | ((item: T, index: number) => string)\n): void {\n for (let i = 0; i < array.length; i++) {\n const item = array[i];\n if (!predicate(item, i)) {\n throw new Error(\n isFunction(message)\n ? message(item, i)\n : (message ??\n `Assertion failed for item \"${String(item)}\" at index ${i}.`)\n );\n }\n }\n}\n","import { assert, assertEvery } from './assert.ts';\nimport { isArray } from './type-guards.ts';\n\n/**\n * Validates a value against a type guard and returns it with the narrowed type.\n * This is useful when you need the validated value as a return value.\n *\n * @template T - The expected type after the guard check\n * @param value - The value to check and return\n * @param check - A type guard function that returns true if the value is of type T\n * @param message - Optional custom error message\n * @returns The value, now typed as T\n * @throws Throws an Error if the type guard check fails\n * @example\n * // Basic usage with built-in type guards\n * const str = guarded(unknownValue, isString);\n * // str is now typed as string\n * console.log(str.toUpperCase());\n *\n * @example\n * // With custom error message\n * const email = guarded(\n * formData.email,\n * isString,\n * 'Email must be a string'\n * );\n *\n * @example\n * // Custom type guard\n * interface User {\n * name: string;\n * age: number;\n * }\n *\n * const isUser = (v: unknown): v is User =>\n * typeof v === 'object' && v !== null &&\n * 'name' in v && typeof v.name === 'string' &&\n * 'age' in v && typeof v.age === 'number';\n *\n * const user = guarded(apiResponse, isUser, 'Invalid user data from API');\n * // user is now typed as User\n *\n * @example\n * // Common use case: error handling\n * try {\n * somethingThatMightThrow();\n * } catch (caught) {\n * const error = guarded(caught, isError, 'Expected an Error object');\n * logger.error(error.message); // error is typed as Error\n * }\n */\nexport function guarded<T>(\n value: unknown,\n check: (value: any) => value is T,\n message?: string\n): T {\n guard(value, check, message);\n return value;\n}\n\n/**\n * Asserts that a value satisfies a type guard using TypeScript's assertion signatures.\n * This is useful when you want to narrow the type of an existing variable in the current scope.\n *\n * @template T - The expected type after the guard check\n * @param value - The value to check\n * @param check - A type guard function that returns true if the value is of type T\n * @param message - Optional custom error message\n * @throws Throws an Error if the type guard check fails\n * @example\n * // Narrowing an existing variable\n * let data: string | number | null = getData();\n * guard(data, isString);\n * // data is now typed as string in this scope\n * console.log(data.toUpperCase());\n *\n * @example\n * // With custom error message\n * let email: unknown = formData.email;\n * guard(email, isString, 'Email field must be a string');\n * // email is now typed as string\n * validateEmail(email);\n *\n * @example\n * // Useful in conditional flows\n * function processValue(value: unknown) {\n * guard(value, isArray, 'Value must be an array to process');\n * // value is now typed as unknown[] for the rest of this function\n * return value.map(item => processItem(item));\n * }\n *\n * @example\n * // Multiple guards in sequence\n * let response: unknown = await fetch('/api/data');\n * guard(response, isPlainObject, 'API response must be an object');\n * // response is now typed as Record<PropertyKey, unknown>\n * guard(response.data, isArray, 'Response data must be an array');\n * // response.data is now typed as unknown[]\n */\nexport function guard<T>(\n value: unknown,\n check: (value: any) => value is T,\n message = `Type guard check failed for value: ${value}`\n): asserts value is T {\n assert(check(value), message);\n}\n\n/**\n * Asserts that a value is an array and that every one of its members satisfies a type guard.\n * This narrows the type of the array variable in the current scope.\n *\n * @template T - The expected type of array members after the guard check.\n * @param value - The value to check, which must be an array.\n * @param check - A type guard function to apply to every member of the array.\n * @param message - Optional custom error message. Can be a string or a function that\n * receives the failing item and its index and returns an error message.\n * @throws Throws an Error if the value is not an array or if any member fails the check.\n * @example\n * // Narrowing an existing array variable\n * let data: unknown = [1, 2, 3];\n * guardEvery(data, isNumber);\n * // data is now typed as readonly number[] in this scope\n */\nexport function guardEvery<T>(\n value: readonly unknown[] | unknown,\n check: (value: any) => value is T,\n message?: string | ((item: unknown, index: number) => string)\n): asserts value is readonly T[] {\n // Guard that the input itself is an array.\n guard(\n value,\n isArray,\n typeof message === 'string' ? message : 'Input must be an array.'\n );\n\n assertEvery(\n value,\n check,\n message ??\n ((item, i) =>\n `Type guard check failed for item at index ${i}: ${String(item)}`)\n );\n}\n\n/**\n * Validates that a value is an array and that every one of its members satisfies a type guard,\n * returning the array with the narrowed member type.\n *\n * @template T - The expected type of array members after the guard check.\n * @param value - The value to check, which must be an array.\n * @param check - A type guard function to apply to every member of the array.\n * @param message - Optional custom error message. Can be a string or a function that\n * receives the failing item and its index and returns an error message.\n * @returns The array, now typed as readonly T[].\n * @throws Throws an Error if the value is not an array or if any member fails the check.\n * @example\n * const apiResponse: unknown = [{ id: 1 }, { id: 2 }];\n * const items = guardedEvery(apiResponse, isItemWithId);\n * // items is now typed as readonly { id: number }[]\n */\nexport function guardedEvery<T>(\n value: readonly unknown[] | unknown,\n check: (value: any) => value is T,\n message?: string | ((item: unknown, index: number) => string)\n): readonly T[] {\n guardEvery(value, check, message);\n return value;\n}\n","import { assert } from './assert.ts';\nimport { guarded } from './guard.ts';\nimport { isError, isNotNullish, isNullish } from './type-guards.ts';\n\n/**\n * Asserts that a value is not nullish and narrows its type accordingly.\n *\n * @template T - The expected type when the value is not nullish\n * @param val - The value to assert\n * @param message - Optional error message\n * @throws Throws an Error if the value is nullish\n * @example\n * assertNotNullish(user.email, 'Email is required');\n * // user.email is now typed without null/undefined\n *\n * @example\n * const result: string | null = getValue();\n * assertNotNullish(result);\n * // result is now typed as string\n */\nexport function assertNotNullish<T>(\n val: T | undefined | null | void,\n message = 'Expected value to not be nullish'\n): asserts val is T {\n assert(isNotNullish(val), message);\n}\n\n/**\n * Asserts that a value is nullish (null, undefined, or void) and narrows its type accordingly.\n * This is useful for validating that optional values are not provided.\n *\n * @template T - The original type of the value\n * @param val - The value to check for nullishness\n * @param message - Optional custom error message\n * @throws Throws an Error if the value is not nullish\n * @example\n * // Validating that boolean cache control directives have no value\n * function parseBoolean(value?: string) {\n * assertNullish(value); // throws if value exists\n * return true;\n * }\n *\n * @example\n * // With custom error message\n * assertNullish(optionalParam, 'Parameter should not be provided');\n */\nexport function assertNullish<T>(\n val: T | undefined | null | void,\n message = 'Expected value to be nullish'\n): asserts val is undefined | null | void {\n assert(isNullish(val), message);\n}\n\n/**\n * Asserts that a value is not nullish and returns the value with narrowed type.\n * Similar to assertNotNullish but returns the value instead of using assertion signatures.\n *\n * @template T - The expected type when the value is not nullish\n * @param val - The value to assert and return\n * @param message - Optional error message\n * @returns The value, now typed as T\n * @throws Throws an Error if the value is nullish\n * @example\n * const email = assertedNotNullish(user.email, 'Email is required');\n * // email is typed as string (not null/undefined)\n *\n * @example\n * const result = assertedNotNullish(array[0]);\n * // result is the first array element, guaranteed to be not nullish\n */\nexport function assertedNotNullish<T>(\n val: T | undefined | null | void,\n message = 'Expected value to not be nullish'\n): T {\n return guarded(val, isNotNullish, message);\n}\n\n/**\n * Asserts that a value is nullish and returns the value with narrowed type.\n * Similar to assertNullish but returns the value instead of using assertion signatures.\n * This is useful when you need to both validate nullishness and use the value in an expression.\n *\n * @template T - The original type of the value\n * @param val - The value to check for nullishness\n * @param message - Optional custom error message\n * @returns The value with type narrowed to undefined | null | void\n * @throws Throws an Error if the value is not nullish\n * @example\n * // Using in expressions with comma operator\n * const result = (assertedNullish(optionalValue), true);\n *\n * @example\n * // Functional validation chain\n * const parseBoolean = (value?: string) => (assertedNullish(value), true);\n */\nexport function assertedNullish<T>(\n val: T | undefined | null | void,\n message = 'Expected value to be nullish'\n): undefined | null | void {\n return guarded(val, isNullish, message);\n}\n\n/**\n * Asserts that a value is an Error and narrows its type accordingly.\n * Uses assertion signatures to narrow the type of an existing variable.\n *\n * @param error - The value to assert as an Error\n * @param message - Optional error message\n * @throws Throws an Error if the value is not an Error\n * @example\n * let caught: unknown = getCaughtError();\n * assertError(caught);\n * // caught is now typed as Error\n * console.log(caught.message);\n *\n * @example\n * function handleError(error: unknown) {\n * assertError(error, 'Expected Error instance');\n * // error is now typed as Error for the rest of this function\n * logger.error(error.stack);\n * }\n */\nexport function assertError(\n error: unknown,\n message = 'Expected value to be an Error'\n): asserts error is Error {\n assert(isError(error), message);\n}\n\n/**\n * Asserts that a value is an Error and returns it with narrowed type.\n * Uses the guard function to ensure type safety.\n *\n * @param error - The value to assert as an Error\n * @param message - Optional error message\n * @returns The value, now typed as Error\n * @throws Throws an Error if the value is not an Error\n * @example\n * try {\n * somethingThatMightThrow();\n * } catch (caught) {\n * const error = assertedError(caught);\n * // error is now typed as Error\n * console.log(error.message);\n * }\n */\nexport function assertedError(\n error: unknown,\n message = 'Expected value to be an Error'\n): Error {\n return guarded(error, isError, message);\n}\n\n/**\n * Asserts that a value is an instance of a specific constructor and narrows its type accordingly.\n * Uses assertion signatures to narrow the type of an existing variable.\n *\n * @template T - The expected instance type\n * @param value - The value to check for instanceof\n * @param Constructor - The constructor function to check against\n * @param message - Optional custom error message\n * @throws Throws an Error if the value is not an instance of the constructor\n * @example\n * // Basic usage with existing variable\n * let data: unknown = getApiResponse();\n * assertInstanceOf(data, Date);\n * // data is now typed as Date\n * console.log(data.getFullYear());\n *\n * @example\n * // Function parameter validation\n * function processError(error: unknown) {\n * assertInstanceOf(error, NetworkError, 'Expected NetworkError');\n * // error is now typed as NetworkError for the rest of this function\n * console.log(error.statusCode);\n * }\n *\n * @example\n * // Conditional narrowing\n * function handleValue(value: unknown) {\n * if (someCondition) {\n * assertInstanceOf(value, CustomClass);\n * // value is now typed as CustomClass in this block\n * value.customMethod();\n * }\n * }\n */\nexport function assertInstanceOf<T>(\n value: unknown,\n Constructor: new (...args: any[]) => T,\n message?: string\n): asserts value is T {\n if (!(value instanceof Constructor)) {\n const defaultMessage = `Expected value to be an instance of ${Constructor.name}, but got ${typeof value}`;\n throw new Error(message ?? defaultMessage);\n }\n}\n\n/**\n * Asserts that a value is an instance of a specific constructor and returns it with narrowed type.\n * Uses instanceof checking to ensure runtime type safety.\n *\n * @template T - The expected instance type\n * @param value - The value to check for instanceof\n * @param Constructor - The constructor function to check against\n * @param message - Optional custom error message\n * @returns The value, now typed as T\n * @throws Throws an Error if the value is not an instance of the constructor\n * @example\n * // Basic usage with built-in types\n * const date = assertedInstanceOf(unknownValue, Date);\n * // date is now typed as Date\n * console.log(date.getFullYear());\n *\n * @example\n * // With custom classes\n * class NetworkError extends Error {\n * constructor(message: string, public statusCode: number) {\n * super(message);\n * }\n * }\n *\n * try {\n * await fetchData();\n * } catch (caught) {\n * const networkError = assertedInstanceOf(caught, NetworkError);\n * // networkError is now typed as NetworkError\n * console.log(networkError.statusCode);\n * }\n *\n * @example\n * // With custom error message\n * const user = assertedInstanceOf(\n * apiResponse,\n * User,\n * 'API response must be a User instance'\n * );\n *\n * @example\n * // Common use cases\n * const buffer = assertedInstanceOf(data, Buffer, 'Expected Buffer data');\n * const regex = assertedInstanceOf(pattern, RegExp, 'Pattern must be a RegExp');\n * const map = assertedInstanceOf(collection, Map, 'Expected Map collection');\n */\nexport function assertedInstanceOf<T>(\n value: unknown,\n Constructor: new (...args: any[]) => T,\n message?: string\n): T {\n if (!(value instanceof Constructor)) {\n const defaultMessage = `Expected value to be an instance of ${Constructor.name}, but got ${typeof value}`;\n throw new Error(message ?? defaultMessage);\n }\n return value;\n}\n","import { assertedNotNullish, isFunction } from '@rg/util';\nimport type { Reducer as ReduxReducer, compose } from 'redux';\nimport * as Redux from 'redux';\nimport { createSelector } from 'reselect';\nimport type {\n Config,\n InitConfig,\n ModelEffectThisTyped,\n ModelEffects,\n ModelReducers,\n Models,\n RematchStore\n} from './types.ts';\nimport { validate } from './validate.ts';\n\ndeclare const window:\n | {\n __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: (\n options?: unknown\n ) => typeof compose;\n }\n | undefined;\n\n// Internal types for the watcher system\n\n// Special marker used for watchers with initial=true option.\n// When a watcher is first created with initial=true, this sentinel value\n// indicates that the \"prev\" value should be set to the current value\n// when triggering the initial callback.\nconst INITIAL_SENTINEL = Symbol('INITIAL_SENTINEL');\n\ntype Selector<TState, TResult> = (state: TState) => TResult;\ntype WatcherOnChange<T> = (newValue: T, oldValue: T) => void | Promise<void>;\n\ninterface Watcher<TState, T> {\n selector: Selector<TState, T>;\n prev: T | typeof INITIAL_SENTINEL;\n onChange: WatcherOnChange<T>;\n active: boolean;\n}\n\ntype RootState = Record<string, unknown>;\n\ninterface RematchAction {\n type: string;\n payload?: unknown;\n meta?: unknown;\n}\n\nfunction isRematchAction(action: unknown): action is RematchAction {\n return (\n typeof action === 'object' &&\n action !== null &&\n 'type' in action &&\n typeof action.type === 'string'\n );\n}\n\nlet unnamedStoreCount = 0;\nexport function createRematchStore<TModels extends Models<TModels>>(\n initConfig: InitConfig<TModels>\n): RematchStore<TModels> {\n type EffectFn = (\n payload: unknown,\n rootState: RootState,\n meta: unknown\n ) => unknown;\n\n const allReducers: Record<string, ReduxReducer> = {};\n const allEffects: Record<string, EffectFn> = {};\n\n const config = normalizeConfig(initConfig);\n\n validate?.(() => [\n [\n !Array.isArray(config.redux.middlewares),\n 'init config.redux.middlewares must be an array'\n ],\n [\n !Array.isArray(config.redux.enhancers),\n 'init config.redux.enhancers must be an array of functions'\n ],\n [\n config.redux.createStore &&\n typeof config.redux.createStore !== 'function',\n 'init config.redux.createStore must be a function'\n ]\n ]);\n\n const watchSelector = (() => {\n let watchers: Watcher<RootState, unknown>[] = [];\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n let subscribed = false;\n\n function checkWatchers() {\n timeoutId = undefined;\n const state = getState();\n for (const watcher of watchers) {\n if (!watcher.active) continue;\n const current = watcher.selector(state);\n if (current !== watcher.prev) {\n const prevValue =\n watcher.prev === INITIAL_SENTINEL ? current : watcher.prev;\n\n void watcher.onChange(current, prevValue);\n watcher.prev = current;\n }\n }\n }\n\n function scheduleCheck() {\n timeoutId ??= setTimeout(checkWatchers, 0);\n }\n\n return (\n selector: Selector<RootState, unknown>,\n onChange: WatcherOnChange<unknown>,\n { initial = false }: { initial?: boolean }\n ) => {\n if (!subscribed) {\n subscribed = true;\n reduxSubscribe(scheduleCheck);\n }\n\n const watcher: Watcher<RootState, unknown> = {\n selector,\n prev: initial ? INITIAL_SENTINEL : selector(getState()),\n onChange,\n active: true\n };\n\n watchers.push(watcher);\n\n if (initial) {\n scheduleCheck();\n }\n\n return () => {\n watcher.active = false;\n watchers = watchers.filter(w => w.active);\n };\n };\n })();\n\n const selector: Record<string, unknown> = {};\n const select: Record<string, unknown> = {};\n const dispatch: Record<string, Record<string, unknown>> = {};\n const store = {\n selector,\n select,\n dispatch,\n getState: () => getState(),\n subscribe: (arg: () => void) => reduxSubscribe(arg),\n name: config.name,\n createSelector<TReturn>(\n callback: (create: typeof createSelector) => (state: RootState) => TReturn\n ) {\n let selectorFn: ((state: RootState) => TReturn) | undefined;\n return (state: RootState) =>\n (selectorFn ??= callback(createSelector))(state);\n },\n watchSelector(\n selectorOrSelectors:\n | Selector<RootState, unknown>\n | Selector<RootState, unknown>[],\n callback: WatcherOnChange<unknown>,\n watchConfig?: { initial?: boolean }\n ) {\n return watchSelector(\n Array.isArray(selectorOrSelectors)\n ? createSelector(selectorOrSelectors, (...args: unknown[]) => args)\n : selectorOrSelectors,\n callback,\n { ...watchConfig }\n );\n }\n };\n\n for (const [name, m] of Object.entries(config.models)) {\n const model = assertedNotNullish(m);\n const reducers: ModelReducers = model.reducers ?? {};\n let effects: ModelEffects<TModels> | undefined = isFunction(model.effects)\n ? undefined\n : model.effects;\n const { state, selectors } = model;\n\n validate?.(() => [\n [typeof name !== 'string', 'model \"name\" [string] is required'],\n [state === undefined, 'model \"state\" is required']\n ]);\n\n const modelReducers: Record<\n string,\n (state: unknown, payload: unknown, meta: unknown) => unknown\n > = {};\n dispatch[name] = {};\n for (const reducerKey of Object.keys(reducers)) {\n const type = reducerKey.includes('/') // is already action name\n ? reducerKey\n : `${name}/${reducerKey}`;\n\n modelReducers[type] = reducers[reducerKey];\n dispatch[name][reducerKey] = (payload?: unknown, meta?: unknown) =>\n reduxDispatch({\n type,\n payload,\n meta\n });\n validate?.(() => [\n [!!reducerKey.match(/\\/.+\\//), `Invalid reducer name (${type})`],\n [\n typeof modelReducers[type] !== 'function',\n `Invalid reducer (${type}). Must be a function`\n ]\n ]);\n }\n\n allReducers[name] = (\n reducerState = model.state,\n {\n type,\n payload,\n meta\n }: { type: string; payload?: unknown; meta?: unknown }\n ) =>\n modelReducers[type]\n ? modelReducers[type](reducerState, payload, meta)\n : reducerState;\n\n // Build property selectors for each state key\n const selectorProps: Record<string, (state: RootState) => unknown> = {};\n const selectProps: Record<string, () => unknown> = {};\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- model state is generic TState, must cast to iterate over keys\n for (const key of Object.keys(state as Record<string, unknown>)) {\n selectorProps[key] = (rootState: RootState) =>\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- accessing dynamic key from model state\n (rootState[name] as Record<string, unknown>)[key];\n selectProps[key] = () =>\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- accessing dynamic key from model state\n (getState()[name] as Record<string, unknown>)[key];\n }\n\n const resolvedSelectors = isFunction(selectors)\n ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- store is built incrementally, TypeScript can't verify it matches RematchStore yet\n selectors(store as RematchStore<TModels>)\n : (selectors ?? {});\n for (const key of Object.keys(resolvedSelectors)) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- selector expects typed state, we widen to accept RootState\n const selectorFn = resolvedSelectors[key] as (\n state: RootState\n ) => unknown;\n selectorProps[key] = selectorFn;\n selectProps[key] = () => selectorFn(getState());\n }\n\n // Use Proxy to allow property names like 'name' and 'length' which are\n // reserved on function objects. The Proxy intercepts property access and\n // returns custom selectors instead of the function's built-in properties.\n const modelSelector = new Proxy((rootState: RootState) => rootState[name], {\n get(target, prop, receiver) {\n if (typeof prop === 'string' && prop in selectorProps) {\n return selectorProps[prop];\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n\n const modelSelect = new Proxy(() => getState()[name], {\n get(target, prop, receiver) {\n if (typeof prop === 'string' && prop in selectProps) {\n return selectProps[prop];\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n\n selector[name] = modelSelector;\n select[name] = modelSelect;\n\n // Resolve effects if it's a function\n if (isFunction(model.effects)) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- store is built incrementally, TypeScript can't verify it matches RematchStore yet\n effects = model.effects(store as RematchStore<TModels>);\n }\n\n if (effects) {\n // map effects names to dispatch actions\n const dispatcher = dispatch[name];\n for (const [effectName, effect] of Object.entries(effects)) {\n const type = `${name}/${effectName}`;\n validate?.(() => [\n [!!effectName.match(/\\//), `Invalid effect name (${type})`],\n [\n typeof effect !== 'function',\n `Invalid effect (${type}). Must be a function`\n ]\n ]);\n /* eslint-disable @typescript-eslint/consistent-type-assertions -- dispatcher is built dynamically, effect expects typed state but we use RootState internally */\n const boundEffect = effect.bind(\n dispatcher as unknown as ModelEffectThisTyped\n ) as EffectFn;\n /* eslint-enable @typescript-eslint/consistent-type-assertions */\n allEffects[type] = boundEffect;\n dispatcher[effectName] = (payload: unknown, meta: unknown) =>\n reduxDispatch({\n type,\n payload,\n meta\n });\n }\n }\n }\n\n const { devtoolOptions, devtoolCompose } = config.redux;\n // Use custom devtoolCompose if provided (e.g., for React Native),\n // otherwise auto-detect browser Redux DevTools Extension\n const composeWithDevTools = devtoolOptions?.disabled\n ? undefined\n : (devtoolCompose ??\n (typeof window === 'object' &&\n window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ??\n undefined);\n const {\n getState,\n subscribe: reduxSubscribe,\n dispatch: reduxDispatch\n } = Redux.legacy_createStore(\n Redux.combineReducers(allReducers),\n {},\n (composeWithDevTools ? composeWithDevTools(devtoolOptions) : Redux.compose)(\n Redux.applyMiddleware(\n ...config.redux.middlewares,\n ({ getState }) =>\n next =>\n (action): unknown => {\n if (!isRematchAction(action)) {\n return next(action);\n }\n // If it is an effect, first run as a reducer, then run the effect\n // and return its result:\n if (action.type in allEffects) {\n next(action);\n return allEffects[action.type](\n action.payload,\n getState(),\n action.meta\n );\n }\n\n return next(action);\n }\n )\n )\n );\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- store is built incrementally with Record types, final shape matches RematchStore but TypeScript can't verify\n return store as RematchStore<TModels>;\n}\n\nfunction normalizeConfig<TModels extends Models<TModels>>(\n initConfig: InitConfig<TModels>\n): Config<TModels> {\n const {\n name = `Rematch Store ${unnamedStoreCount++}`,\n models,\n redux\n } = initConfig;\n\n return {\n name,\n models: models ?? {},\n redux: {\n enhancers: [],\n middlewares: [],\n ...redux,\n devtoolOptions: {\n name,\n ...(redux?.devtoolOptions ?? {})\n }\n }\n };\n}\n","/**\n * Takes an array of arrays of validations. Collects all errors and throws.\n */\nexport const validate =\n process.env.NODE_ENV === 'production'\n ? undefined\n : (run: () => [boolean | undefined, string][]): void => {\n const errors = run()\n .filter(([isInvalid]) => isInvalid)\n .map(([, errorMessage]) => errorMessage);\n\n if (errors.length > 0) {\n throw new Error(errors.join(', '));\n }\n };\n","export { createRematchStore } from './store.ts';\nimport type { ModelCreator } from './types.ts';\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-assertions\nexport const createModel: ModelCreator = () => mo => mo as any;\n\nexport type * from './types.ts';\n"],"mappings":"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","names":["selector","getState"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radio-garden/rematch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"redux": "^5.0.1",
|
|
21
|
+
"reselect": "5.1.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.0.0",
|
|
25
|
+
"@rg/tooling": "1.0.0",
|
|
26
|
+
"@rg/util": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"coverage": "vitest --run --coverage",
|
|
34
|
+
"format": "prettier --write src/",
|
|
35
|
+
"lint": "eslint . --flag unstable_native_nodejs_ts_config",
|
|
36
|
+
"publish-major": "pnpm version major && pnpm run publish-npm",
|
|
37
|
+
"publish-minor": "pnpm version minor && pnpm run publish-npm",
|
|
38
|
+
"publish-npm": "npm pkg set name=@radio-garden/rematch && pnpm publish && npm pkg set name=@rg/rematch",
|
|
39
|
+
"publish-patch": "pnpm version patch && pnpm run publish-npm",
|
|
40
|
+
"reference": "reference",
|
|
41
|
+
"test": "vitest --run --passWithNoTests",
|
|
42
|
+
"types": "tsc --noEmit true"
|
|
43
|
+
},
|
|
44
|
+
"module": "./dist/index.js",
|
|
45
|
+
"types": "./dist/index.d.ts"
|
|
46
|
+
}
|