state-invariant 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # state-invariant
2
+
3
+ Catch illegal **state transitions** at runtime, in any state management library.
4
+
5
+ Most validation libraries (Zod, Yup, io-ts) check the *shape* of one object at one
6
+ moment in time. `state-invariant` checks something different: given the state
7
+ **before**, the state **after**, and the **action** that caused the change — was
8
+ that transition actually legal?
9
+
10
+ ```
11
+ prev state + action → next state
12
+ ↑ ↑
13
+ └──── is this legal? ──────┘
14
+ ```
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install state-invariant
20
+ ```
21
+
22
+ ## Core usage (works anywhere)
23
+
24
+ ```ts
25
+ import { createInvariantChecker } from 'state-invariant';
26
+
27
+ const check = createInvariantChecker([
28
+ {
29
+ name: 'balance-never-negative',
30
+ check: (prev, next) => next.balance >= 0,
31
+ message: (prev, next, action) =>
32
+ `action ${action.type} would make balance negative: ${next.balance}`,
33
+ },
34
+ {
35
+ name: 'logout-clears-user',
36
+ when: (action) => action.type === 'LOGOUT',
37
+ check: (prev, next) => next.user === null,
38
+ },
39
+ ]);
40
+
41
+ check(prevState, nextState, action); // throws InvariantViolationError if a rule fails
42
+ ```
43
+
44
+ ## With Redux
45
+
46
+ ```ts
47
+ import { configureStore } from '@reduxjs/toolkit';
48
+ import { createInvariantMiddleware } from 'state-invariant/redux';
49
+
50
+ const middleware = createInvariantMiddleware([
51
+ { name: 'balance-never-negative', check: (p, n) => n.balance >= 0 },
52
+ ]);
53
+
54
+ const store = configureStore({
55
+ reducer,
56
+ middleware: (getDefault) => getDefault().concat(middleware),
57
+ });
58
+ ```
59
+
60
+ ## With Zustand
61
+
62
+ ```ts
63
+ import { create } from 'zustand';
64
+ import { invariant } from 'state-invariant/zustand';
65
+
66
+ const useStore = create(
67
+ invariant([{ name: 'count-never-negative', check: (p, n) => n.count >= 0 }])(
68
+ (set) => ({ count: 0, decrement: () => set((s) => ({ count: s.count - 1 })) })
69
+ )
70
+ );
71
+ ```
72
+
73
+ ## With anything else (plain reducer)
74
+
75
+ ```ts
76
+ import { wrapReducer } from 'state-invariant/generic';
77
+
78
+ const checkedReducer = wrapReducer(myReducer, [
79
+ { name: 'total-conserved', check: (p, n) => p.total === n.total },
80
+ ]);
81
+ ```
82
+
83
+ Or bracket any mutation manually, for stores with no middleware hooks at all:
84
+
85
+ ```ts
86
+ import { wrapManual } from 'state-invariant/generic';
87
+
88
+ const checker = wrapManual(myVanillaStore, rules);
89
+ checker.before();
90
+ myVanillaStore.doSomething();
91
+ checker.after('doSomething');
92
+ ```
93
+
94
+ ## With APIs & Backends (Database Mutations)
95
+
96
+ This pattern is incredibly powerful for backend APIs and distributed systems where you want to guarantee that a database mutation doesn't violate core business rules.
97
+
98
+ ```ts
99
+ import { createInvariantTransaction } from 'state-invariant/backend';
100
+
101
+ const safeTransfer = createInvariantTransaction([
102
+ {
103
+ name: 'balance-never-negative',
104
+ check: (prev, next) => next.balance >= 0 || 'Insufficient funds'
105
+ }
106
+ ]);
107
+
108
+ // Inside your Express/Next.js route handler
109
+ app.post('/transfer', async (req, res) => {
110
+ try {
111
+ const result = await safeTransfer(req.body, {
112
+ read: () => db.accounts.findById(req.body.accountId),
113
+ compute: (prev) => ({
114
+ ...prev,
115
+ balance: prev.balance - req.body.amount
116
+ }),
117
+ write: (next) => db.accounts.save(next)
118
+ });
119
+ res.json(result);
120
+ } catch (error) {
121
+ res.status(400).json({ error: error.message });
122
+ }
123
+ });
124
+ ```
125
+ The invariant check runs *after* `compute` but *before* `write`. If the invariant fails, it throws, meaning your database is never corrupted.
126
+
127
+ ## Error handling modes
128
+
129
+ ```ts
130
+ createInvariantChecker(rules, { mode: 'throw' }); // default: throw immediately
131
+ createInvariantChecker(rules, { mode: 'warn' }); // console.warn, keep running
132
+ createInvariantChecker(rules, { mode: 'collect' }); // gather violations silently
133
+ ```
134
+
135
+ In `collect` mode, read them back with `check.getViolations()`.
136
+
137
+ ## Why not just Zod?
138
+
139
+ Zod validates one object's shape. It can't tell you whether `next.balance`
140
+ is a *legal consequence* of `prev.balance` and the action that ran. That
141
+ requires seeing both states plus the action together — which is what this
142
+ package is for. Use Zod for shape, `state-invariant` for transitions. They
143
+ compose fine together.
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/backend.ts
21
+ var backend_exports = {};
22
+ __export(backend_exports, {
23
+ createInvariantTransaction: () => createInvariantTransaction
24
+ });
25
+ module.exports = __toCommonJS(backend_exports);
26
+
27
+ // src/core.ts
28
+ var InvariantViolationError = class extends Error {
29
+ constructor(message, details) {
30
+ super(message);
31
+ this.name = "InvariantViolationError";
32
+ this.ruleName = details.ruleName;
33
+ this.prev = details.prev;
34
+ this.next = details.next;
35
+ this.action = details.action;
36
+ }
37
+ };
38
+ function createInvariantChecker(rules, options = {}) {
39
+ var _a;
40
+ const mode = (_a = options.mode) != null ? _a : "throw";
41
+ const violations = [];
42
+ function check(prev, next, action) {
43
+ for (const rule of rules) {
44
+ if (rule.when && !rule.when(action)) continue;
45
+ let ok;
46
+ try {
47
+ ok = rule.check(prev, next, action);
48
+ } catch (err) {
49
+ ok = false;
50
+ }
51
+ if (ok === false || typeof ok === "string") {
52
+ const message = typeof ok === "string" ? ok : rule.message ? rule.message(prev, next, action) : `Invariant "${rule.name}" violated`;
53
+ const error = new InvariantViolationError(message, {
54
+ ruleName: rule.name,
55
+ prev,
56
+ next,
57
+ action
58
+ });
59
+ if (mode === "throw") {
60
+ throw error;
61
+ } else if (mode === "warn") {
62
+ console.warn(`[state-invariant] ${message}`, { prev, next, action });
63
+ } else {
64
+ violations.push(error);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ check.getViolations = () => violations.slice();
70
+ check.clearViolations = () => {
71
+ violations.length = 0;
72
+ };
73
+ return check;
74
+ }
75
+
76
+ // src/backend.ts
77
+ function createInvariantTransaction(rules, options = {}) {
78
+ const check = createInvariantChecker(rules, options);
79
+ return async (action, handlers) => {
80
+ const prev = await handlers.read();
81
+ const next = await handlers.compute(prev);
82
+ check(prev, next, action);
83
+ return await handlers.write(next);
84
+ };
85
+ }
86
+ // Annotate the CommonJS export names for ESM import in node:
87
+ 0 && (module.exports = {
88
+ createInvariantTransaction
89
+ });
90
+ //# sourceMappingURL=backend.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backend.ts","../src/core.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Orchestrates a backend data transaction (read, compute, write).\n * Ensures that the transition from `prev` to `next` state is validated\n * against the provided invariant rules BEFORE the `write` handler is executed.\n * \n * Perfect for API routes, database operations, and event processors.\n */\nexport function createInvariantTransaction<State = any, Action = any, Result = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return async (\n action: Action,\n handlers: {\n /** Fetch the current state from your database/source */\n read: () => Promise<State> | State;\n /** Compute the proposed new state based on the current state */\n compute: (prev: State) => Promise<State> | State;\n /** Save the new state. This will NOT be called if the invariant check fails. */\n write: (next: State) => Promise<Result> | Result;\n }\n ): Promise<Result> => {\n const prev = await handlers.read();\n const next = await handlers.compute(prev);\n \n // Check if the transition is legal before saving\n check(prev, next, action);\n \n return await handlers.write(next);\n };\n}\n","/**\n * state-invariant/core\n *\n * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.\n * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused\n * by some action) was legal. This is different from Zod/Yup: those check the shape\n * of ONE object. This checks the relationship between two states and the action\n * that connected them.\n */\n\nexport interface InvariantRule<State = any, Action = any> {\n /** Human-readable name, shown in error messages and stack traces. */\n name: string;\n /**\n * Optional filter. If provided, the rule only runs when this returns true.\n * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'\n */\n when?: (action: Action) => boolean;\n /**\n * The actual check. Return true if the transition is valid, false if it violates\n * the invariant. Alternatively, return a string to indicate a violation and use\n * that string as the error message.\n */\n check: (prev: State, next: State, action: Action) => boolean | string;\n /**\n * Optional custom error message builder. Falls back to a generic message.\n * Ignored if `check` returns a string.\n */\n message?: (prev: State, next: State, action: Action) => string;\n}\n\nexport class InvariantViolationError extends Error {\n ruleName: string;\n prev: unknown;\n next: unknown;\n action: unknown;\n\n constructor(\n message: string,\n details: { ruleName: string; prev: unknown; next: unknown; action: unknown }\n ) {\n super(message);\n this.name = 'InvariantViolationError';\n this.ruleName = details.ruleName;\n this.prev = details.prev;\n this.next = details.next;\n this.action = details.action;\n }\n}\n\nexport interface CheckerOptions {\n /**\n * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.\n * 'warn' - console.warn and continue. Useful in production if you don't want to crash.\n * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.\n */\n mode?: 'throw' | 'warn' | 'collect';\n}\n\nexport function createInvariantChecker<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const mode = options.mode ?? 'throw';\n const violations: InvariantViolationError[] = [];\n\n function check(prev: State, next: State, action: Action): void {\n for (const rule of rules) {\n if (rule.when && !rule.when(action)) continue;\n\n let ok: boolean | string;\n try {\n ok = rule.check(prev, next, action);\n } catch (err) {\n // A rule that throws internally (e.g. undefined property access) is itself\n // a bug in the rule, but we surface it as a violation so it's not silently\n // swallowed by the store.\n ok = false;\n }\n\n if (ok === false || typeof ok === 'string') {\n const message =\n typeof ok === 'string'\n ? ok\n : rule.message\n ? rule.message(prev, next, action)\n : `Invariant \"${rule.name}\" violated`;\n\n const error = new InvariantViolationError(message, {\n ruleName: rule.name,\n prev,\n next,\n action,\n });\n\n if (mode === 'throw') {\n throw error;\n } else if (mode === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(`[state-invariant] ${message}`, { prev, next, action });\n } else {\n violations.push(error);\n }\n }\n }\n }\n\n check.getViolations = () => violations.slice();\n check.clearViolations = () => {\n violations.length = 0;\n };\n\n return check;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC+BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAMjD,YACE,SACA,SACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAWO,SAAS,uBACd,OACA,UAA0B,CAAC,GAC3B;AA9DF;AA+DE,QAAM,QAAO,aAAQ,SAAR,YAAgB;AAC7B,QAAM,aAAwC,CAAC;AAE/C,WAAS,MAAM,MAAa,MAAa,QAAsB;AAC7D,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAG;AAErC,UAAI;AACJ,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAAA,MACpC,SAAS,KAAK;AAIZ,aAAK;AAAA,MACP;AAEA,UAAI,OAAO,SAAS,OAAO,OAAO,UAAU;AAC1C,cAAM,UACJ,OAAO,OAAO,WACV,KACA,KAAK,UACL,KAAK,QAAQ,MAAM,MAAM,MAAM,IAC/B,cAAc,KAAK,IAAI;AAE7B,cAAM,QAAQ,IAAI,wBAAwB,SAAS;AAAA,UACjD,UAAU,KAAK;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI,SAAS,SAAS;AACpB,gBAAM;AAAA,QACR,WAAW,SAAS,QAAQ;AAE1B,kBAAQ,KAAK,qBAAqB,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,CAAC;AAAA,QACrE,OAAO;AACL,qBAAW,KAAK,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,QAAM,kBAAkB,MAAM;AAC5B,eAAW,SAAS;AAAA,EACtB;AAEA,SAAO;AACT;;;ADxGO,SAAS,2BACd,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,OACL,QACA,aAQoB;AACpB,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,OAAO,MAAM,SAAS,QAAQ,IAAI;AAGxC,UAAM,MAAM,MAAM,MAAM;AAExB,WAAO,MAAM,SAAS,MAAM,IAAI;AAAA,EAClC;AACF;","names":[]}
@@ -0,0 +1,16 @@
1
+ import { InvariantRule, CheckerOptions } from './core';
2
+ /**
3
+ * Orchestrates a backend data transaction (read, compute, write).
4
+ * Ensures that the transition from `prev` to `next` state is validated
5
+ * against the provided invariant rules BEFORE the `write` handler is executed.
6
+ *
7
+ * Perfect for API routes, database operations, and event processors.
8
+ */
9
+ export declare function createInvariantTransaction<State = any, Action = any, Result = any>(rules: InvariantRule<State, Action>[], options?: CheckerOptions): (action: Action, handlers: {
10
+ /** Fetch the current state from your database/source */
11
+ read: () => Promise<State> | State;
12
+ /** Compute the proposed new state based on the current state */
13
+ compute: (prev: State) => Promise<State> | State;
14
+ /** Save the new state. This will NOT be called if the invariant check fails. */
15
+ write: (next: State) => Promise<Result> | Result;
16
+ }) => Promise<Result>;
@@ -0,0 +1,18 @@
1
+ import {
2
+ createInvariantChecker
3
+ } from "./chunk-DRSBKG3Y.js";
4
+
5
+ // src/backend.ts
6
+ function createInvariantTransaction(rules, options = {}) {
7
+ const check = createInvariantChecker(rules, options);
8
+ return async (action, handlers) => {
9
+ const prev = await handlers.read();
10
+ const next = await handlers.compute(prev);
11
+ check(prev, next, action);
12
+ return await handlers.write(next);
13
+ };
14
+ }
15
+ export {
16
+ createInvariantTransaction
17
+ };
18
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backend.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Orchestrates a backend data transaction (read, compute, write).\n * Ensures that the transition from `prev` to `next` state is validated\n * against the provided invariant rules BEFORE the `write` handler is executed.\n * \n * Perfect for API routes, database operations, and event processors.\n */\nexport function createInvariantTransaction<State = any, Action = any, Result = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return async (\n action: Action,\n handlers: {\n /** Fetch the current state from your database/source */\n read: () => Promise<State> | State;\n /** Compute the proposed new state based on the current state */\n compute: (prev: State) => Promise<State> | State;\n /** Save the new state. This will NOT be called if the invariant check fails. */\n write: (next: State) => Promise<Result> | Result;\n }\n ): Promise<Result> => {\n const prev = await handlers.read();\n const next = await handlers.compute(prev);\n \n // Check if the transition is legal before saving\n check(prev, next, action);\n \n return await handlers.write(next);\n };\n}\n"],"mappings":";;;;;AASO,SAAS,2BACd,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,OACL,QACA,aAQoB;AACpB,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,OAAO,MAAM,SAAS,QAAQ,IAAI;AAGxC,UAAM,MAAM,MAAM,MAAM;AAExB,WAAO,MAAM,SAAS,MAAM,IAAI;AAAA,EAClC;AACF;","names":[]}
@@ -0,0 +1,54 @@
1
+ // src/core.ts
2
+ var InvariantViolationError = class extends Error {
3
+ constructor(message, details) {
4
+ super(message);
5
+ this.name = "InvariantViolationError";
6
+ this.ruleName = details.ruleName;
7
+ this.prev = details.prev;
8
+ this.next = details.next;
9
+ this.action = details.action;
10
+ }
11
+ };
12
+ function createInvariantChecker(rules, options = {}) {
13
+ var _a;
14
+ const mode = (_a = options.mode) != null ? _a : "throw";
15
+ const violations = [];
16
+ function check(prev, next, action) {
17
+ for (const rule of rules) {
18
+ if (rule.when && !rule.when(action)) continue;
19
+ let ok;
20
+ try {
21
+ ok = rule.check(prev, next, action);
22
+ } catch (err) {
23
+ ok = false;
24
+ }
25
+ if (ok === false || typeof ok === "string") {
26
+ const message = typeof ok === "string" ? ok : rule.message ? rule.message(prev, next, action) : `Invariant "${rule.name}" violated`;
27
+ const error = new InvariantViolationError(message, {
28
+ ruleName: rule.name,
29
+ prev,
30
+ next,
31
+ action
32
+ });
33
+ if (mode === "throw") {
34
+ throw error;
35
+ } else if (mode === "warn") {
36
+ console.warn(`[state-invariant] ${message}`, { prev, next, action });
37
+ } else {
38
+ violations.push(error);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ check.getViolations = () => violations.slice();
44
+ check.clearViolations = () => {
45
+ violations.length = 0;
46
+ };
47
+ return check;
48
+ }
49
+
50
+ export {
51
+ InvariantViolationError,
52
+ createInvariantChecker
53
+ };
54
+ //# sourceMappingURL=chunk-DRSBKG3Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * state-invariant/core\n *\n * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.\n * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused\n * by some action) was legal. This is different from Zod/Yup: those check the shape\n * of ONE object. This checks the relationship between two states and the action\n * that connected them.\n */\n\nexport interface InvariantRule<State = any, Action = any> {\n /** Human-readable name, shown in error messages and stack traces. */\n name: string;\n /**\n * Optional filter. If provided, the rule only runs when this returns true.\n * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'\n */\n when?: (action: Action) => boolean;\n /**\n * The actual check. Return true if the transition is valid, false if it violates\n * the invariant. Alternatively, return a string to indicate a violation and use\n * that string as the error message.\n */\n check: (prev: State, next: State, action: Action) => boolean | string;\n /**\n * Optional custom error message builder. Falls back to a generic message.\n * Ignored if `check` returns a string.\n */\n message?: (prev: State, next: State, action: Action) => string;\n}\n\nexport class InvariantViolationError extends Error {\n ruleName: string;\n prev: unknown;\n next: unknown;\n action: unknown;\n\n constructor(\n message: string,\n details: { ruleName: string; prev: unknown; next: unknown; action: unknown }\n ) {\n super(message);\n this.name = 'InvariantViolationError';\n this.ruleName = details.ruleName;\n this.prev = details.prev;\n this.next = details.next;\n this.action = details.action;\n }\n}\n\nexport interface CheckerOptions {\n /**\n * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.\n * 'warn' - console.warn and continue. Useful in production if you don't want to crash.\n * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.\n */\n mode?: 'throw' | 'warn' | 'collect';\n}\n\nexport function createInvariantChecker<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const mode = options.mode ?? 'throw';\n const violations: InvariantViolationError[] = [];\n\n function check(prev: State, next: State, action: Action): void {\n for (const rule of rules) {\n if (rule.when && !rule.when(action)) continue;\n\n let ok: boolean | string;\n try {\n ok = rule.check(prev, next, action);\n } catch (err) {\n // A rule that throws internally (e.g. undefined property access) is itself\n // a bug in the rule, but we surface it as a violation so it's not silently\n // swallowed by the store.\n ok = false;\n }\n\n if (ok === false || typeof ok === 'string') {\n const message =\n typeof ok === 'string'\n ? ok\n : rule.message\n ? rule.message(prev, next, action)\n : `Invariant \"${rule.name}\" violated`;\n\n const error = new InvariantViolationError(message, {\n ruleName: rule.name,\n prev,\n next,\n action,\n });\n\n if (mode === 'throw') {\n throw error;\n } else if (mode === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(`[state-invariant] ${message}`, { prev, next, action });\n } else {\n violations.push(error);\n }\n }\n }\n }\n\n check.getViolations = () => violations.slice();\n check.clearViolations = () => {\n violations.length = 0;\n };\n\n return check;\n}\n"],"mappings":";AA+BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAMjD,YACE,SACA,SACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAWO,SAAS,uBACd,OACA,UAA0B,CAAC,GAC3B;AA9DF;AA+DE,QAAM,QAAO,aAAQ,SAAR,YAAgB;AAC7B,QAAM,aAAwC,CAAC;AAE/C,WAAS,MAAM,MAAa,MAAa,QAAsB;AAC7D,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAG;AAErC,UAAI;AACJ,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAAA,MACpC,SAAS,KAAK;AAIZ,aAAK;AAAA,MACP;AAEA,UAAI,OAAO,SAAS,OAAO,OAAO,UAAU;AAC1C,cAAM,UACJ,OAAO,OAAO,WACV,KACA,KAAK,UACL,KAAK,QAAQ,MAAM,MAAM,MAAM,IAC/B,cAAc,KAAK,IAAI;AAE7B,cAAM,QAAQ,IAAI,wBAAwB,SAAS;AAAA,UACjD,UAAU,KAAK;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI,SAAS,SAAS;AACpB,gBAAM;AAAA,QACR,WAAW,SAAS,QAAQ;AAE1B,kBAAQ,KAAK,qBAAqB,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,CAAC;AAAA,QACrE,OAAO;AACL,qBAAW,KAAK,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,QAAM,kBAAkB,MAAM;AAC5B,eAAW,SAAS;AAAA,EACtB;AAEA,SAAO;AACT;","names":[]}
package/dist/core.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * state-invariant/core
3
+ *
4
+ * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.
5
+ * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused
6
+ * by some action) was legal. This is different from Zod/Yup: those check the shape
7
+ * of ONE object. This checks the relationship between two states and the action
8
+ * that connected them.
9
+ */
10
+ export interface InvariantRule<State = any, Action = any> {
11
+ /** Human-readable name, shown in error messages and stack traces. */
12
+ name: string;
13
+ /**
14
+ * Optional filter. If provided, the rule only runs when this returns true.
15
+ * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'
16
+ */
17
+ when?: (action: Action) => boolean;
18
+ /**
19
+ * The actual check. Return true if the transition is valid, false if it violates
20
+ * the invariant. Alternatively, return a string to indicate a violation and use
21
+ * that string as the error message.
22
+ */
23
+ check: (prev: State, next: State, action: Action) => boolean | string;
24
+ /**
25
+ * Optional custom error message builder. Falls back to a generic message.
26
+ * Ignored if `check` returns a string.
27
+ */
28
+ message?: (prev: State, next: State, action: Action) => string;
29
+ }
30
+ export declare class InvariantViolationError extends Error {
31
+ ruleName: string;
32
+ prev: unknown;
33
+ next: unknown;
34
+ action: unknown;
35
+ constructor(message: string, details: {
36
+ ruleName: string;
37
+ prev: unknown;
38
+ next: unknown;
39
+ action: unknown;
40
+ });
41
+ }
42
+ export interface CheckerOptions {
43
+ /**
44
+ * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.
45
+ * 'warn' - console.warn and continue. Useful in production if you don't want to crash.
46
+ * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.
47
+ */
48
+ mode?: 'throw' | 'warn' | 'collect';
49
+ }
50
+ export declare function createInvariantChecker<State = any, Action = any>(rules: InvariantRule<State, Action>[], options?: CheckerOptions): {
51
+ (prev: State, next: State, action: Action): void;
52
+ getViolations(): InvariantViolationError[];
53
+ clearViolations(): void;
54
+ };
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/generic.ts
21
+ var generic_exports = {};
22
+ __export(generic_exports, {
23
+ wrapManual: () => wrapManual,
24
+ wrapReducer: () => wrapReducer
25
+ });
26
+ module.exports = __toCommonJS(generic_exports);
27
+
28
+ // src/core.ts
29
+ var InvariantViolationError = class extends Error {
30
+ constructor(message, details) {
31
+ super(message);
32
+ this.name = "InvariantViolationError";
33
+ this.ruleName = details.ruleName;
34
+ this.prev = details.prev;
35
+ this.next = details.next;
36
+ this.action = details.action;
37
+ }
38
+ };
39
+ function createInvariantChecker(rules, options = {}) {
40
+ var _a;
41
+ const mode = (_a = options.mode) != null ? _a : "throw";
42
+ const violations = [];
43
+ function check(prev, next, action) {
44
+ for (const rule of rules) {
45
+ if (rule.when && !rule.when(action)) continue;
46
+ let ok;
47
+ try {
48
+ ok = rule.check(prev, next, action);
49
+ } catch (err) {
50
+ ok = false;
51
+ }
52
+ if (ok === false || typeof ok === "string") {
53
+ const message = typeof ok === "string" ? ok : rule.message ? rule.message(prev, next, action) : `Invariant "${rule.name}" violated`;
54
+ const error = new InvariantViolationError(message, {
55
+ ruleName: rule.name,
56
+ prev,
57
+ next,
58
+ action
59
+ });
60
+ if (mode === "throw") {
61
+ throw error;
62
+ } else if (mode === "warn") {
63
+ console.warn(`[state-invariant] ${message}`, { prev, next, action });
64
+ } else {
65
+ violations.push(error);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ check.getViolations = () => violations.slice();
71
+ check.clearViolations = () => {
72
+ violations.length = 0;
73
+ };
74
+ return check;
75
+ }
76
+
77
+ // src/generic.ts
78
+ function wrapReducer(reducer, rules, options = {}) {
79
+ const check = createInvariantChecker(rules, options);
80
+ return (state, action) => {
81
+ const nextState = reducer(state, action);
82
+ check(state, nextState, action);
83
+ return nextState;
84
+ };
85
+ }
86
+ function wrapManual(store, rules, options = {}) {
87
+ const check = createInvariantChecker(rules, options);
88
+ let snapshot;
89
+ return {
90
+ before() {
91
+ snapshot = store.getState();
92
+ },
93
+ after(action) {
94
+ if (snapshot === void 0) {
95
+ throw new Error("wrapManual: after() called without before()");
96
+ }
97
+ const next = store.getState();
98
+ check(snapshot, next, action);
99
+ snapshot = void 0;
100
+ }
101
+ };
102
+ }
103
+ // Annotate the CommonJS export names for ESM import in node:
104
+ 0 && (module.exports = {
105
+ wrapManual,
106
+ wrapReducer
107
+ });
108
+ //# sourceMappingURL=generic.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generic.ts","../src/core.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Works with ANY reducer-shaped function: (state, action) => newState.\n * This is the \"plugs into anything\" escape hatch — if a library doesn't have\n * a dedicated adapter, and it exposes a plain reducer somewhere, wrap it with this.\n *\n * Usage:\n * const checkedReducer = wrapReducer(myReducer, [\n * { name: 'total-conserved', check: (p, n) => p.total === n.total }\n * ]);\n */\nexport function wrapReducer<State = any, Action = any>(\n reducer: (state: State, action: Action) => State,\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return (state: State, action: Action): State => {\n const nextState = reducer(state, action);\n check(state, nextState, action);\n return nextState;\n };\n}\n\n/**\n * Works with any object that has getState()/setState()-style access, by letting\n * you manually bracket a mutation. Useful for libraries with no middleware hooks\n * at all (vanilla stores, custom pub-sub state, etc).\n *\n * Usage:\n * const checker = wrapManual(myStore, rules);\n * checker.before();\n * myStore.doSomething();\n * checker.after(actionLabel);\n */\nexport function wrapManual<State = any, Action = any>(\n store: { getState: () => State },\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n let snapshot: State | undefined;\n\n return {\n before() {\n snapshot = store.getState();\n },\n after(action: Action) {\n if (snapshot === undefined) {\n throw new Error('wrapManual: after() called without before()');\n }\n const next = store.getState();\n check(snapshot, next, action);\n snapshot = undefined;\n },\n };\n}\n","/**\n * state-invariant/core\n *\n * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.\n * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused\n * by some action) was legal. This is different from Zod/Yup: those check the shape\n * of ONE object. This checks the relationship between two states and the action\n * that connected them.\n */\n\nexport interface InvariantRule<State = any, Action = any> {\n /** Human-readable name, shown in error messages and stack traces. */\n name: string;\n /**\n * Optional filter. If provided, the rule only runs when this returns true.\n * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'\n */\n when?: (action: Action) => boolean;\n /**\n * The actual check. Return true if the transition is valid, false if it violates\n * the invariant. Alternatively, return a string to indicate a violation and use\n * that string as the error message.\n */\n check: (prev: State, next: State, action: Action) => boolean | string;\n /**\n * Optional custom error message builder. Falls back to a generic message.\n * Ignored if `check` returns a string.\n */\n message?: (prev: State, next: State, action: Action) => string;\n}\n\nexport class InvariantViolationError extends Error {\n ruleName: string;\n prev: unknown;\n next: unknown;\n action: unknown;\n\n constructor(\n message: string,\n details: { ruleName: string; prev: unknown; next: unknown; action: unknown }\n ) {\n super(message);\n this.name = 'InvariantViolationError';\n this.ruleName = details.ruleName;\n this.prev = details.prev;\n this.next = details.next;\n this.action = details.action;\n }\n}\n\nexport interface CheckerOptions {\n /**\n * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.\n * 'warn' - console.warn and continue. Useful in production if you don't want to crash.\n * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.\n */\n mode?: 'throw' | 'warn' | 'collect';\n}\n\nexport function createInvariantChecker<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const mode = options.mode ?? 'throw';\n const violations: InvariantViolationError[] = [];\n\n function check(prev: State, next: State, action: Action): void {\n for (const rule of rules) {\n if (rule.when && !rule.when(action)) continue;\n\n let ok: boolean | string;\n try {\n ok = rule.check(prev, next, action);\n } catch (err) {\n // A rule that throws internally (e.g. undefined property access) is itself\n // a bug in the rule, but we surface it as a violation so it's not silently\n // swallowed by the store.\n ok = false;\n }\n\n if (ok === false || typeof ok === 'string') {\n const message =\n typeof ok === 'string'\n ? ok\n : rule.message\n ? rule.message(prev, next, action)\n : `Invariant \"${rule.name}\" violated`;\n\n const error = new InvariantViolationError(message, {\n ruleName: rule.name,\n prev,\n next,\n action,\n });\n\n if (mode === 'throw') {\n throw error;\n } else if (mode === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(`[state-invariant] ${message}`, { prev, next, action });\n } else {\n violations.push(error);\n }\n }\n }\n }\n\n check.getViolations = () => violations.slice();\n check.clearViolations = () => {\n violations.length = 0;\n };\n\n return check;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC+BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAMjD,YACE,SACA,SACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAWO,SAAS,uBACd,OACA,UAA0B,CAAC,GAC3B;AA9DF;AA+DE,QAAM,QAAO,aAAQ,SAAR,YAAgB;AAC7B,QAAM,aAAwC,CAAC;AAE/C,WAAS,MAAM,MAAa,MAAa,QAAsB;AAC7D,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAG;AAErC,UAAI;AACJ,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAAA,MACpC,SAAS,KAAK;AAIZ,aAAK;AAAA,MACP;AAEA,UAAI,OAAO,SAAS,OAAO,OAAO,UAAU;AAC1C,cAAM,UACJ,OAAO,OAAO,WACV,KACA,KAAK,UACL,KAAK,QAAQ,MAAM,MAAM,MAAM,IAC/B,cAAc,KAAK,IAAI;AAE7B,cAAM,QAAQ,IAAI,wBAAwB,SAAS;AAAA,UACjD,UAAU,KAAK;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI,SAAS,SAAS;AACpB,gBAAM;AAAA,QACR,WAAW,SAAS,QAAQ;AAE1B,kBAAQ,KAAK,qBAAqB,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,CAAC;AAAA,QACrE,OAAO;AACL,qBAAW,KAAK,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,QAAM,kBAAkB,MAAM;AAC5B,eAAW,SAAS;AAAA,EACtB;AAEA,SAAO;AACT;;;ADrGO,SAAS,YACd,SACA,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,CAAC,OAAc,WAA0B;AAC9C,UAAM,YAAY,QAAQ,OAAO,MAAM;AACvC,UAAM,OAAO,WAAW,MAAM;AAC9B,WAAO;AAAA,EACT;AACF;AAaO,SAAS,WACd,OACA,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAClE,MAAI;AAEJ,SAAO;AAAA,IACL,SAAS;AACP,iBAAW,MAAM,SAAS;AAAA,IAC5B;AAAA,IACA,MAAM,QAAgB;AACpB,UAAI,aAAa,QAAW;AAC1B,cAAM,IAAI,MAAM,6CAA6C;AAAA,MAC/D;AACA,YAAM,OAAO,MAAM,SAAS;AAC5B,YAAM,UAAU,MAAM,MAAM;AAC5B,iBAAW;AAAA,IACb;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,29 @@
1
+ import { InvariantRule, CheckerOptions } from './core';
2
+ /**
3
+ * Works with ANY reducer-shaped function: (state, action) => newState.
4
+ * This is the "plugs into anything" escape hatch — if a library doesn't have
5
+ * a dedicated adapter, and it exposes a plain reducer somewhere, wrap it with this.
6
+ *
7
+ * Usage:
8
+ * const checkedReducer = wrapReducer(myReducer, [
9
+ * { name: 'total-conserved', check: (p, n) => p.total === n.total }
10
+ * ]);
11
+ */
12
+ export declare function wrapReducer<State = any, Action = any>(reducer: (state: State, action: Action) => State, rules: InvariantRule<State, Action>[], options?: CheckerOptions): (state: State, action: Action) => State;
13
+ /**
14
+ * Works with any object that has getState()/setState()-style access, by letting
15
+ * you manually bracket a mutation. Useful for libraries with no middleware hooks
16
+ * at all (vanilla stores, custom pub-sub state, etc).
17
+ *
18
+ * Usage:
19
+ * const checker = wrapManual(myStore, rules);
20
+ * checker.before();
21
+ * myStore.doSomething();
22
+ * checker.after(actionLabel);
23
+ */
24
+ export declare function wrapManual<State = any, Action = any>(store: {
25
+ getState: () => State;
26
+ }, rules: InvariantRule<State, Action>[], options?: CheckerOptions): {
27
+ before(): void;
28
+ after(action: Action): void;
29
+ };
@@ -0,0 +1,35 @@
1
+ import {
2
+ createInvariantChecker
3
+ } from "./chunk-DRSBKG3Y.js";
4
+
5
+ // src/generic.ts
6
+ function wrapReducer(reducer, rules, options = {}) {
7
+ const check = createInvariantChecker(rules, options);
8
+ return (state, action) => {
9
+ const nextState = reducer(state, action);
10
+ check(state, nextState, action);
11
+ return nextState;
12
+ };
13
+ }
14
+ function wrapManual(store, rules, options = {}) {
15
+ const check = createInvariantChecker(rules, options);
16
+ let snapshot;
17
+ return {
18
+ before() {
19
+ snapshot = store.getState();
20
+ },
21
+ after(action) {
22
+ if (snapshot === void 0) {
23
+ throw new Error("wrapManual: after() called without before()");
24
+ }
25
+ const next = store.getState();
26
+ check(snapshot, next, action);
27
+ snapshot = void 0;
28
+ }
29
+ };
30
+ }
31
+ export {
32
+ wrapManual,
33
+ wrapReducer
34
+ };
35
+ //# sourceMappingURL=generic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generic.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Works with ANY reducer-shaped function: (state, action) => newState.\n * This is the \"plugs into anything\" escape hatch — if a library doesn't have\n * a dedicated adapter, and it exposes a plain reducer somewhere, wrap it with this.\n *\n * Usage:\n * const checkedReducer = wrapReducer(myReducer, [\n * { name: 'total-conserved', check: (p, n) => p.total === n.total }\n * ]);\n */\nexport function wrapReducer<State = any, Action = any>(\n reducer: (state: State, action: Action) => State,\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return (state: State, action: Action): State => {\n const nextState = reducer(state, action);\n check(state, nextState, action);\n return nextState;\n };\n}\n\n/**\n * Works with any object that has getState()/setState()-style access, by letting\n * you manually bracket a mutation. Useful for libraries with no middleware hooks\n * at all (vanilla stores, custom pub-sub state, etc).\n *\n * Usage:\n * const checker = wrapManual(myStore, rules);\n * checker.before();\n * myStore.doSomething();\n * checker.after(actionLabel);\n */\nexport function wrapManual<State = any, Action = any>(\n store: { getState: () => State },\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n let snapshot: State | undefined;\n\n return {\n before() {\n snapshot = store.getState();\n },\n after(action: Action) {\n if (snapshot === undefined) {\n throw new Error('wrapManual: after() called without before()');\n }\n const next = store.getState();\n check(snapshot, next, action);\n snapshot = undefined;\n },\n };\n}\n"],"mappings":";;;;;AAYO,SAAS,YACd,SACA,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,CAAC,OAAc,WAA0B;AAC9C,UAAM,YAAY,QAAQ,OAAO,MAAM;AACvC,UAAM,OAAO,WAAW,MAAM;AAC9B,WAAO;AAAA,EACT;AACF;AAaO,SAAS,WACd,OACA,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAClE,MAAI;AAEJ,SAAO;AAAA,IACL,SAAS;AACP,iBAAW,MAAM,SAAS;AAAA,IAC5B;AAAA,IACA,MAAM,QAAgB;AACpB,UAAI,aAAa,QAAW;AAC1B,cAAM,IAAI,MAAM,6CAA6C;AAAA,MAC/D;AACA,YAAM,OAAO,MAAM,SAAS;AAC5B,YAAM,UAAU,MAAM,MAAM;AAC5B,iBAAW;AAAA,IACb;AAAA,EACF;AACF;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ InvariantViolationError: () => InvariantViolationError,
24
+ createInvariantChecker: () => createInvariantChecker
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/core.ts
29
+ var InvariantViolationError = class extends Error {
30
+ constructor(message, details) {
31
+ super(message);
32
+ this.name = "InvariantViolationError";
33
+ this.ruleName = details.ruleName;
34
+ this.prev = details.prev;
35
+ this.next = details.next;
36
+ this.action = details.action;
37
+ }
38
+ };
39
+ function createInvariantChecker(rules, options = {}) {
40
+ var _a;
41
+ const mode = (_a = options.mode) != null ? _a : "throw";
42
+ const violations = [];
43
+ function check(prev, next, action) {
44
+ for (const rule of rules) {
45
+ if (rule.when && !rule.when(action)) continue;
46
+ let ok;
47
+ try {
48
+ ok = rule.check(prev, next, action);
49
+ } catch (err) {
50
+ ok = false;
51
+ }
52
+ if (ok === false || typeof ok === "string") {
53
+ const message = typeof ok === "string" ? ok : rule.message ? rule.message(prev, next, action) : `Invariant "${rule.name}" violated`;
54
+ const error = new InvariantViolationError(message, {
55
+ ruleName: rule.name,
56
+ prev,
57
+ next,
58
+ action
59
+ });
60
+ if (mode === "throw") {
61
+ throw error;
62
+ } else if (mode === "warn") {
63
+ console.warn(`[state-invariant] ${message}`, { prev, next, action });
64
+ } else {
65
+ violations.push(error);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ check.getViolations = () => violations.slice();
71
+ check.clearViolations = () => {
72
+ violations.length = 0;
73
+ };
74
+ return check;
75
+ }
76
+ // Annotate the CommonJS export names for ESM import in node:
77
+ 0 && (module.exports = {
78
+ InvariantViolationError,
79
+ createInvariantChecker
80
+ });
81
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core.ts"],"sourcesContent":["export * from './core';\n","/**\n * state-invariant/core\n *\n * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.\n * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused\n * by some action) was legal. This is different from Zod/Yup: those check the shape\n * of ONE object. This checks the relationship between two states and the action\n * that connected them.\n */\n\nexport interface InvariantRule<State = any, Action = any> {\n /** Human-readable name, shown in error messages and stack traces. */\n name: string;\n /**\n * Optional filter. If provided, the rule only runs when this returns true.\n * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'\n */\n when?: (action: Action) => boolean;\n /**\n * The actual check. Return true if the transition is valid, false if it violates\n * the invariant. Alternatively, return a string to indicate a violation and use\n * that string as the error message.\n */\n check: (prev: State, next: State, action: Action) => boolean | string;\n /**\n * Optional custom error message builder. Falls back to a generic message.\n * Ignored if `check` returns a string.\n */\n message?: (prev: State, next: State, action: Action) => string;\n}\n\nexport class InvariantViolationError extends Error {\n ruleName: string;\n prev: unknown;\n next: unknown;\n action: unknown;\n\n constructor(\n message: string,\n details: { ruleName: string; prev: unknown; next: unknown; action: unknown }\n ) {\n super(message);\n this.name = 'InvariantViolationError';\n this.ruleName = details.ruleName;\n this.prev = details.prev;\n this.next = details.next;\n this.action = details.action;\n }\n}\n\nexport interface CheckerOptions {\n /**\n * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.\n * 'warn' - console.warn and continue. Useful in production if you don't want to crash.\n * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.\n */\n mode?: 'throw' | 'warn' | 'collect';\n}\n\nexport function createInvariantChecker<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const mode = options.mode ?? 'throw';\n const violations: InvariantViolationError[] = [];\n\n function check(prev: State, next: State, action: Action): void {\n for (const rule of rules) {\n if (rule.when && !rule.when(action)) continue;\n\n let ok: boolean | string;\n try {\n ok = rule.check(prev, next, action);\n } catch (err) {\n // A rule that throws internally (e.g. undefined property access) is itself\n // a bug in the rule, but we surface it as a violation so it's not silently\n // swallowed by the store.\n ok = false;\n }\n\n if (ok === false || typeof ok === 'string') {\n const message =\n typeof ok === 'string'\n ? ok\n : rule.message\n ? rule.message(prev, next, action)\n : `Invariant \"${rule.name}\" violated`;\n\n const error = new InvariantViolationError(message, {\n ruleName: rule.name,\n prev,\n next,\n action,\n });\n\n if (mode === 'throw') {\n throw error;\n } else if (mode === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(`[state-invariant] ${message}`, { prev, next, action });\n } else {\n violations.push(error);\n }\n }\n }\n }\n\n check.getViolations = () => violations.slice();\n check.clearViolations = () => {\n violations.length = 0;\n };\n\n return check;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC+BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAMjD,YACE,SACA,SACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAWO,SAAS,uBACd,OACA,UAA0B,CAAC,GAC3B;AA9DF;AA+DE,QAAM,QAAO,aAAQ,SAAR,YAAgB;AAC7B,QAAM,aAAwC,CAAC;AAE/C,WAAS,MAAM,MAAa,MAAa,QAAsB;AAC7D,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAG;AAErC,UAAI;AACJ,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAAA,MACpC,SAAS,KAAK;AAIZ,aAAK;AAAA,MACP;AAEA,UAAI,OAAO,SAAS,OAAO,OAAO,UAAU;AAC1C,cAAM,UACJ,OAAO,OAAO,WACV,KACA,KAAK,UACL,KAAK,QAAQ,MAAM,MAAM,MAAM,IAC/B,cAAc,KAAK,IAAI;AAE7B,cAAM,QAAQ,IAAI,wBAAwB,SAAS;AAAA,UACjD,UAAU,KAAK;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI,SAAS,SAAS;AACpB,gBAAM;AAAA,QACR,WAAW,SAAS,QAAQ;AAE1B,kBAAQ,KAAK,qBAAqB,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,CAAC;AAAA,QACrE,OAAO;AACL,qBAAW,KAAK,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,QAAM,kBAAkB,MAAM;AAC5B,eAAW,SAAS;AAAA,EACtB;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1 @@
1
+ export * from './core';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ InvariantViolationError,
3
+ createInvariantChecker
4
+ } from "./chunk-DRSBKG3Y.js";
5
+ export {
6
+ InvariantViolationError,
7
+ createInvariantChecker
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/redux.cjs ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/redux.ts
21
+ var redux_exports = {};
22
+ __export(redux_exports, {
23
+ createInvariantMiddleware: () => createInvariantMiddleware
24
+ });
25
+ module.exports = __toCommonJS(redux_exports);
26
+
27
+ // src/core.ts
28
+ var InvariantViolationError = class extends Error {
29
+ constructor(message, details) {
30
+ super(message);
31
+ this.name = "InvariantViolationError";
32
+ this.ruleName = details.ruleName;
33
+ this.prev = details.prev;
34
+ this.next = details.next;
35
+ this.action = details.action;
36
+ }
37
+ };
38
+ function createInvariantChecker(rules, options = {}) {
39
+ var _a;
40
+ const mode = (_a = options.mode) != null ? _a : "throw";
41
+ const violations = [];
42
+ function check(prev, next, action) {
43
+ for (const rule of rules) {
44
+ if (rule.when && !rule.when(action)) continue;
45
+ let ok;
46
+ try {
47
+ ok = rule.check(prev, next, action);
48
+ } catch (err) {
49
+ ok = false;
50
+ }
51
+ if (ok === false || typeof ok === "string") {
52
+ const message = typeof ok === "string" ? ok : rule.message ? rule.message(prev, next, action) : `Invariant "${rule.name}" violated`;
53
+ const error = new InvariantViolationError(message, {
54
+ ruleName: rule.name,
55
+ prev,
56
+ next,
57
+ action
58
+ });
59
+ if (mode === "throw") {
60
+ throw error;
61
+ } else if (mode === "warn") {
62
+ console.warn(`[state-invariant] ${message}`, { prev, next, action });
63
+ } else {
64
+ violations.push(error);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ check.getViolations = () => violations.slice();
70
+ check.clearViolations = () => {
71
+ violations.length = 0;
72
+ };
73
+ return check;
74
+ }
75
+
76
+ // src/redux.ts
77
+ function createInvariantMiddleware(rules, options = {}) {
78
+ const check = createInvariantChecker(rules, options);
79
+ return (storeApi) => (next) => (action) => {
80
+ const prev = storeApi.getState();
81
+ const result = next(action);
82
+ const nextState = storeApi.getState();
83
+ check(prev, nextState, action);
84
+ return result;
85
+ };
86
+ }
87
+ // Annotate the CommonJS export names for ESM import in node:
88
+ 0 && (module.exports = {
89
+ createInvariantMiddleware
90
+ });
91
+ //# sourceMappingURL=redux.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redux.ts","../src/core.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Redux middleware. Runs your invariant rules after every dispatch, comparing\n * state before and after.\n *\n * Usage:\n * import { createInvariantMiddleware } from 'state-invariant/redux';\n *\n * const middleware = createInvariantMiddleware([\n * {\n * name: 'balance-never-negative',\n * check: (prev, next) => next.balance >= 0,\n * message: (prev, next) => `balance went negative: ${next.balance}`,\n * },\n * ]);\n *\n * const store = configureStore({\n * reducer,\n * middleware: (getDefault) => getDefault().concat(middleware),\n * });\n */\nexport function createInvariantMiddleware<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return (storeApi: { getState: () => State }) =>\n (next: (action: Action) => unknown) =>\n (action: Action) => {\n const prev = storeApi.getState();\n const result = next(action);\n const nextState = storeApi.getState();\n check(prev, nextState, action);\n return result;\n };\n}\n","/**\n * state-invariant/core\n *\n * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.\n * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused\n * by some action) was legal. This is different from Zod/Yup: those check the shape\n * of ONE object. This checks the relationship between two states and the action\n * that connected them.\n */\n\nexport interface InvariantRule<State = any, Action = any> {\n /** Human-readable name, shown in error messages and stack traces. */\n name: string;\n /**\n * Optional filter. If provided, the rule only runs when this returns true.\n * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'\n */\n when?: (action: Action) => boolean;\n /**\n * The actual check. Return true if the transition is valid, false if it violates\n * the invariant. Alternatively, return a string to indicate a violation and use\n * that string as the error message.\n */\n check: (prev: State, next: State, action: Action) => boolean | string;\n /**\n * Optional custom error message builder. Falls back to a generic message.\n * Ignored if `check` returns a string.\n */\n message?: (prev: State, next: State, action: Action) => string;\n}\n\nexport class InvariantViolationError extends Error {\n ruleName: string;\n prev: unknown;\n next: unknown;\n action: unknown;\n\n constructor(\n message: string,\n details: { ruleName: string; prev: unknown; next: unknown; action: unknown }\n ) {\n super(message);\n this.name = 'InvariantViolationError';\n this.ruleName = details.ruleName;\n this.prev = details.prev;\n this.next = details.next;\n this.action = details.action;\n }\n}\n\nexport interface CheckerOptions {\n /**\n * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.\n * 'warn' - console.warn and continue. Useful in production if you don't want to crash.\n * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.\n */\n mode?: 'throw' | 'warn' | 'collect';\n}\n\nexport function createInvariantChecker<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const mode = options.mode ?? 'throw';\n const violations: InvariantViolationError[] = [];\n\n function check(prev: State, next: State, action: Action): void {\n for (const rule of rules) {\n if (rule.when && !rule.when(action)) continue;\n\n let ok: boolean | string;\n try {\n ok = rule.check(prev, next, action);\n } catch (err) {\n // A rule that throws internally (e.g. undefined property access) is itself\n // a bug in the rule, but we surface it as a violation so it's not silently\n // swallowed by the store.\n ok = false;\n }\n\n if (ok === false || typeof ok === 'string') {\n const message =\n typeof ok === 'string'\n ? ok\n : rule.message\n ? rule.message(prev, next, action)\n : `Invariant \"${rule.name}\" violated`;\n\n const error = new InvariantViolationError(message, {\n ruleName: rule.name,\n prev,\n next,\n action,\n });\n\n if (mode === 'throw') {\n throw error;\n } else if (mode === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(`[state-invariant] ${message}`, { prev, next, action });\n } else {\n violations.push(error);\n }\n }\n }\n }\n\n check.getViolations = () => violations.slice();\n check.clearViolations = () => {\n violations.length = 0;\n };\n\n return check;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC+BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAMjD,YACE,SACA,SACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAWO,SAAS,uBACd,OACA,UAA0B,CAAC,GAC3B;AA9DF;AA+DE,QAAM,QAAO,aAAQ,SAAR,YAAgB;AAC7B,QAAM,aAAwC,CAAC;AAE/C,WAAS,MAAM,MAAa,MAAa,QAAsB;AAC7D,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAG;AAErC,UAAI;AACJ,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAAA,MACpC,SAAS,KAAK;AAIZ,aAAK;AAAA,MACP;AAEA,UAAI,OAAO,SAAS,OAAO,OAAO,UAAU;AAC1C,cAAM,UACJ,OAAO,OAAO,WACV,KACA,KAAK,UACL,KAAK,QAAQ,MAAM,MAAM,MAAM,IAC/B,cAAc,KAAK,IAAI;AAE7B,cAAM,QAAQ,IAAI,wBAAwB,SAAS;AAAA,UACjD,UAAU,KAAK;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI,SAAS,SAAS;AACpB,gBAAM;AAAA,QACR,WAAW,SAAS,QAAQ;AAE1B,kBAAQ,KAAK,qBAAqB,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,CAAC;AAAA,QACrE,OAAO;AACL,qBAAW,KAAK,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,QAAM,kBAAkB,MAAM;AAC5B,eAAW,SAAS;AAAA,EACtB;AAEA,SAAO;AACT;;;AD3FO,SAAS,0BACd,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,CAAC,aACN,CAAC,SACD,CAAC,WAAmB;AAClB,UAAM,OAAO,SAAS,SAAS;AAC/B,UAAM,SAAS,KAAK,MAAM;AAC1B,UAAM,YAAY,SAAS,SAAS;AACpC,UAAM,MAAM,WAAW,MAAM;AAC7B,WAAO;AAAA,EACT;AACJ;","names":[]}
@@ -0,0 +1,24 @@
1
+ import { InvariantRule, CheckerOptions } from './core';
2
+ /**
3
+ * Redux middleware. Runs your invariant rules after every dispatch, comparing
4
+ * state before and after.
5
+ *
6
+ * Usage:
7
+ * import { createInvariantMiddleware } from 'state-invariant/redux';
8
+ *
9
+ * const middleware = createInvariantMiddleware([
10
+ * {
11
+ * name: 'balance-never-negative',
12
+ * check: (prev, next) => next.balance >= 0,
13
+ * message: (prev, next) => `balance went negative: ${next.balance}`,
14
+ * },
15
+ * ]);
16
+ *
17
+ * const store = configureStore({
18
+ * reducer,
19
+ * middleware: (getDefault) => getDefault().concat(middleware),
20
+ * });
21
+ */
22
+ export declare function createInvariantMiddleware<State = any, Action = any>(rules: InvariantRule<State, Action>[], options?: CheckerOptions): (storeApi: {
23
+ getState: () => State;
24
+ }) => (next: (action: Action) => unknown) => (action: Action) => unknown;
package/dist/redux.js ADDED
@@ -0,0 +1,19 @@
1
+ import {
2
+ createInvariantChecker
3
+ } from "./chunk-DRSBKG3Y.js";
4
+
5
+ // src/redux.ts
6
+ function createInvariantMiddleware(rules, options = {}) {
7
+ const check = createInvariantChecker(rules, options);
8
+ return (storeApi) => (next) => (action) => {
9
+ const prev = storeApi.getState();
10
+ const result = next(action);
11
+ const nextState = storeApi.getState();
12
+ check(prev, nextState, action);
13
+ return result;
14
+ };
15
+ }
16
+ export {
17
+ createInvariantMiddleware
18
+ };
19
+ //# sourceMappingURL=redux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redux.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Redux middleware. Runs your invariant rules after every dispatch, comparing\n * state before and after.\n *\n * Usage:\n * import { createInvariantMiddleware } from 'state-invariant/redux';\n *\n * const middleware = createInvariantMiddleware([\n * {\n * name: 'balance-never-negative',\n * check: (prev, next) => next.balance >= 0,\n * message: (prev, next) => `balance went negative: ${next.balance}`,\n * },\n * ]);\n *\n * const store = configureStore({\n * reducer,\n * middleware: (getDefault) => getDefault().concat(middleware),\n * });\n */\nexport function createInvariantMiddleware<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return (storeApi: { getState: () => State }) =>\n (next: (action: Action) => unknown) =>\n (action: Action) => {\n const prev = storeApi.getState();\n const result = next(action);\n const nextState = storeApi.getState();\n check(prev, nextState, action);\n return result;\n };\n}\n"],"mappings":";;;;;AAsBO,SAAS,0BACd,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,CAAC,aACN,CAAC,SACD,CAAC,WAAmB;AAClB,UAAM,OAAO,SAAS,SAAS;AAC/B,UAAM,SAAS,KAAK,MAAM;AAC1B,UAAM,YAAY,SAAS,SAAS;AACpC,UAAM,MAAM,WAAW,MAAM;AAC7B,WAAO;AAAA,EACT;AACJ;","names":[]}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/zustand.ts
21
+ var zustand_exports = {};
22
+ __export(zustand_exports, {
23
+ invariant: () => invariant
24
+ });
25
+ module.exports = __toCommonJS(zustand_exports);
26
+
27
+ // src/core.ts
28
+ var InvariantViolationError = class extends Error {
29
+ constructor(message, details) {
30
+ super(message);
31
+ this.name = "InvariantViolationError";
32
+ this.ruleName = details.ruleName;
33
+ this.prev = details.prev;
34
+ this.next = details.next;
35
+ this.action = details.action;
36
+ }
37
+ };
38
+ function createInvariantChecker(rules, options = {}) {
39
+ var _a;
40
+ const mode = (_a = options.mode) != null ? _a : "throw";
41
+ const violations = [];
42
+ function check(prev, next, action) {
43
+ for (const rule of rules) {
44
+ if (rule.when && !rule.when(action)) continue;
45
+ let ok;
46
+ try {
47
+ ok = rule.check(prev, next, action);
48
+ } catch (err) {
49
+ ok = false;
50
+ }
51
+ if (ok === false || typeof ok === "string") {
52
+ const message = typeof ok === "string" ? ok : rule.message ? rule.message(prev, next, action) : `Invariant "${rule.name}" violated`;
53
+ const error = new InvariantViolationError(message, {
54
+ ruleName: rule.name,
55
+ prev,
56
+ next,
57
+ action
58
+ });
59
+ if (mode === "throw") {
60
+ throw error;
61
+ } else if (mode === "warn") {
62
+ console.warn(`[state-invariant] ${message}`, { prev, next, action });
63
+ } else {
64
+ violations.push(error);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ check.getViolations = () => violations.slice();
70
+ check.clearViolations = () => {
71
+ violations.length = 0;
72
+ };
73
+ return check;
74
+ }
75
+
76
+ // src/zustand.ts
77
+ function invariant(rules, options = {}) {
78
+ const check = createInvariantChecker(rules, options);
79
+ return (config) => (set, get, api) => {
80
+ const wrappedSet = (...args) => {
81
+ const prev = get();
82
+ set(...args);
83
+ const next = get();
84
+ const action = args.length > 2 ? args[2] : void 0;
85
+ check(prev, next, action);
86
+ };
87
+ return config(wrappedSet, get, api);
88
+ };
89
+ }
90
+ // Annotate the CommonJS export names for ESM import in node:
91
+ 0 && (module.exports = {
92
+ invariant
93
+ });
94
+ //# sourceMappingURL=zustand.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/zustand.ts","../src/core.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Zustand middleware. Wraps `set` so every state update is checked before\n * and after.\n *\n * Note: plain Zustand doesn't have a first-class \"action\" object like Redux does,\n * so `action` will be undefined unless you're using Zustand's `redux` middleware\n * on top (in which case compose this OUTSIDE that one, closer to `create`).\n *\n * Usage:\n * import { create } from 'zustand';\n * import { invariant } from 'state-invariant/zustand';\n *\n * const useStore = create(\n * invariant([\n * { name: 'count-never-negative', check: (p, n) => n.count >= 0 }\n * ])(\n * (set) => ({ count: 0, decrement: () => set((s) => ({ count: s.count - 1 })) })\n * )\n * );\n */\nexport function invariant<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return (config: (set: any, get: any, api: any) => State) =>\n (set: any, get: any, api: any) => {\n const wrappedSet = (...args: any[]) => {\n const prev = get();\n set(...args);\n const next = get();\n // Zustand `set` signature: set(partial, replace, actionType)\n const action = (args.length > 2 ? args[2] : undefined) as Action;\n check(prev, next, action);\n };\n return config(wrappedSet, get, api);\n };\n}\n","/**\n * state-invariant/core\n *\n * Framework-agnostic engine. Doesn't know about Redux, Zustand, or anyone else.\n * You give it rules; it tells you whether a state TRANSITION (prev -> next, caused\n * by some action) was legal. This is different from Zod/Yup: those check the shape\n * of ONE object. This checks the relationship between two states and the action\n * that connected them.\n */\n\nexport interface InvariantRule<State = any, Action = any> {\n /** Human-readable name, shown in error messages and stack traces. */\n name: string;\n /**\n * Optional filter. If provided, the rule only runs when this returns true.\n * Lets you scope a rule to one action type, e.g. action => action.type === 'WITHDRAW'\n */\n when?: (action: Action) => boolean;\n /**\n * The actual check. Return true if the transition is valid, false if it violates\n * the invariant. Alternatively, return a string to indicate a violation and use\n * that string as the error message.\n */\n check: (prev: State, next: State, action: Action) => boolean | string;\n /**\n * Optional custom error message builder. Falls back to a generic message.\n * Ignored if `check` returns a string.\n */\n message?: (prev: State, next: State, action: Action) => string;\n}\n\nexport class InvariantViolationError extends Error {\n ruleName: string;\n prev: unknown;\n next: unknown;\n action: unknown;\n\n constructor(\n message: string,\n details: { ruleName: string; prev: unknown; next: unknown; action: unknown }\n ) {\n super(message);\n this.name = 'InvariantViolationError';\n this.ruleName = details.ruleName;\n this.prev = details.prev;\n this.next = details.next;\n this.action = details.action;\n }\n}\n\nexport interface CheckerOptions {\n /**\n * 'throw' (default) - throw InvariantViolationError immediately, stopping execution.\n * 'warn' - console.warn and continue. Useful in production if you don't want to crash.\n * 'collect' - don't throw or warn, just gather violations; call .getViolations() yourself.\n */\n mode?: 'throw' | 'warn' | 'collect';\n}\n\nexport function createInvariantChecker<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const mode = options.mode ?? 'throw';\n const violations: InvariantViolationError[] = [];\n\n function check(prev: State, next: State, action: Action): void {\n for (const rule of rules) {\n if (rule.when && !rule.when(action)) continue;\n\n let ok: boolean | string;\n try {\n ok = rule.check(prev, next, action);\n } catch (err) {\n // A rule that throws internally (e.g. undefined property access) is itself\n // a bug in the rule, but we surface it as a violation so it's not silently\n // swallowed by the store.\n ok = false;\n }\n\n if (ok === false || typeof ok === 'string') {\n const message =\n typeof ok === 'string'\n ? ok\n : rule.message\n ? rule.message(prev, next, action)\n : `Invariant \"${rule.name}\" violated`;\n\n const error = new InvariantViolationError(message, {\n ruleName: rule.name,\n prev,\n next,\n action,\n });\n\n if (mode === 'throw') {\n throw error;\n } else if (mode === 'warn') {\n // eslint-disable-next-line no-console\n console.warn(`[state-invariant] ${message}`, { prev, next, action });\n } else {\n violations.push(error);\n }\n }\n }\n }\n\n check.getViolations = () => violations.slice();\n check.clearViolations = () => {\n violations.length = 0;\n };\n\n return check;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC+BO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAMjD,YACE,SACA,SACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,OAAO,QAAQ;AACpB,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AAAA,EACxB;AACF;AAWO,SAAS,uBACd,OACA,UAA0B,CAAC,GAC3B;AA9DF;AA+DE,QAAM,QAAO,aAAQ,SAAR,YAAgB;AAC7B,QAAM,aAAwC,CAAC;AAE/C,WAAS,MAAM,MAAa,MAAa,QAAsB;AAC7D,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,EAAG;AAErC,UAAI;AACJ,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,MAAM,MAAM;AAAA,MACpC,SAAS,KAAK;AAIZ,aAAK;AAAA,MACP;AAEA,UAAI,OAAO,SAAS,OAAO,OAAO,UAAU;AAC1C,cAAM,UACJ,OAAO,OAAO,WACV,KACA,KAAK,UACL,KAAK,QAAQ,MAAM,MAAM,MAAM,IAC/B,cAAc,KAAK,IAAI;AAE7B,cAAM,QAAQ,IAAI,wBAAwB,SAAS;AAAA,UACjD,UAAU,KAAK;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI,SAAS,SAAS;AACpB,gBAAM;AAAA,QACR,WAAW,SAAS,QAAQ;AAE1B,kBAAQ,KAAK,qBAAqB,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,CAAC;AAAA,QACrE,OAAO;AACL,qBAAW,KAAK,KAAK;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,MAAM,WAAW,MAAM;AAC7C,QAAM,kBAAkB,MAAM;AAC5B,eAAW,SAAS;AAAA,EACtB;AAEA,SAAO;AACT;;;AD3FO,SAAS,UACd,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,CAAC,WACN,CAAC,KAAU,KAAU,QAAa;AAChC,UAAM,aAAa,IAAI,SAAgB;AACrC,YAAM,OAAO,IAAI;AACjB,UAAI,GAAG,IAAI;AACX,YAAM,OAAO,IAAI;AAEjB,YAAM,SAAU,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI;AAC5C,YAAM,MAAM,MAAM,MAAM;AAAA,IAC1B;AACA,WAAO,OAAO,YAAY,KAAK,GAAG;AAAA,EACpC;AACJ;","names":[]}
@@ -0,0 +1,22 @@
1
+ import { InvariantRule, CheckerOptions } from './core';
2
+ /**
3
+ * Zustand middleware. Wraps `set` so every state update is checked before
4
+ * and after.
5
+ *
6
+ * Note: plain Zustand doesn't have a first-class "action" object like Redux does,
7
+ * so `action` will be undefined unless you're using Zustand's `redux` middleware
8
+ * on top (in which case compose this OUTSIDE that one, closer to `create`).
9
+ *
10
+ * Usage:
11
+ * import { create } from 'zustand';
12
+ * import { invariant } from 'state-invariant/zustand';
13
+ *
14
+ * const useStore = create(
15
+ * invariant([
16
+ * { name: 'count-never-negative', check: (p, n) => n.count >= 0 }
17
+ * ])(
18
+ * (set) => ({ count: 0, decrement: () => set((s) => ({ count: s.count - 1 })) })
19
+ * )
20
+ * );
21
+ */
22
+ export declare function invariant<State = any, Action = any>(rules: InvariantRule<State, Action>[], options?: CheckerOptions): (config: (set: any, get: any, api: any) => State) => (set: any, get: any, api: any) => State;
@@ -0,0 +1,22 @@
1
+ import {
2
+ createInvariantChecker
3
+ } from "./chunk-DRSBKG3Y.js";
4
+
5
+ // src/zustand.ts
6
+ function invariant(rules, options = {}) {
7
+ const check = createInvariantChecker(rules, options);
8
+ return (config) => (set, get, api) => {
9
+ const wrappedSet = (...args) => {
10
+ const prev = get();
11
+ set(...args);
12
+ const next = get();
13
+ const action = args.length > 2 ? args[2] : void 0;
14
+ check(prev, next, action);
15
+ };
16
+ return config(wrappedSet, get, api);
17
+ };
18
+ }
19
+ export {
20
+ invariant
21
+ };
22
+ //# sourceMappingURL=zustand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/zustand.ts"],"sourcesContent":["import { createInvariantChecker, InvariantRule, CheckerOptions } from './core';\n\n/**\n * Zustand middleware. Wraps `set` so every state update is checked before\n * and after.\n *\n * Note: plain Zustand doesn't have a first-class \"action\" object like Redux does,\n * so `action` will be undefined unless you're using Zustand's `redux` middleware\n * on top (in which case compose this OUTSIDE that one, closer to `create`).\n *\n * Usage:\n * import { create } from 'zustand';\n * import { invariant } from 'state-invariant/zustand';\n *\n * const useStore = create(\n * invariant([\n * { name: 'count-never-negative', check: (p, n) => n.count >= 0 }\n * ])(\n * (set) => ({ count: 0, decrement: () => set((s) => ({ count: s.count - 1 })) })\n * )\n * );\n */\nexport function invariant<State = any, Action = any>(\n rules: InvariantRule<State, Action>[],\n options: CheckerOptions = {}\n) {\n const check = createInvariantChecker<State, Action>(rules, options);\n\n return (config: (set: any, get: any, api: any) => State) =>\n (set: any, get: any, api: any) => {\n const wrappedSet = (...args: any[]) => {\n const prev = get();\n set(...args);\n const next = get();\n // Zustand `set` signature: set(partial, replace, actionType)\n const action = (args.length > 2 ? args[2] : undefined) as Action;\n check(prev, next, action);\n };\n return config(wrappedSet, get, api);\n };\n}\n"],"mappings":";;;;;AAsBO,SAAS,UACd,OACA,UAA0B,CAAC,GAC3B;AACA,QAAM,QAAQ,uBAAsC,OAAO,OAAO;AAElE,SAAO,CAAC,WACN,CAAC,KAAU,KAAU,QAAa;AAChC,UAAM,aAAa,IAAI,SAAgB;AACrC,YAAM,OAAO,IAAI;AACjB,UAAI,GAAG,IAAI;AACX,YAAM,OAAO,IAAI;AAEjB,YAAM,SAAU,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI;AAC5C,YAAM,MAAM,MAAM,MAAM;AAAA,IAC1B;AACA,WAAO,OAAO,YAAY,KAAK,GAAG;AAAA,EACpC;AACJ;","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "state-invariant",
3
+ "version": "0.1.0",
4
+ "description": "Cross-verify state transitions against declared invariants, in any state management library (Redux, Zustand, or plain reducers). Not shape validation like Zod - transition validation: given prev state, next state, and the action that connected them, was it legal?",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./redux": {
16
+ "types": "./dist/redux.d.ts",
17
+ "import": "./dist/redux.js",
18
+ "require": "./dist/redux.cjs"
19
+ },
20
+ "./zustand": {
21
+ "types": "./dist/zustand.d.ts",
22
+ "import": "./dist/zustand.js",
23
+ "require": "./dist/zustand.cjs"
24
+ },
25
+ "./generic": {
26
+ "types": "./dist/generic.d.ts",
27
+ "import": "./dist/generic.js",
28
+ "require": "./dist/generic.cjs"
29
+ },
30
+ "./backend": {
31
+ "types": "./dist/backend.d.ts",
32
+ "import": "./dist/backend.js",
33
+ "require": "./dist/backend.cjs"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup && tsc",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "keywords": [
44
+ "state management",
45
+ "redux",
46
+ "zustand",
47
+ "validation",
48
+ "invariant",
49
+ "runtime checking"
50
+ ],
51
+ "author": "Amal Vijayakumar",
52
+ "license": "MIT",
53
+ "devDependencies": {
54
+ "typescript": "^5.9.3",
55
+ "tsup": "^8.5.0"
56
+ }
57
+ }