├── .eslintignore
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .npmignore
├── .prettierignore
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── package.json
├── rollup.config.js
├── src
├── appContext.ts
├── constant.ts
├── container
│ ├── Injector.ts
│ └── container.ts
├── decorators
│ ├── Injectable.ts
│ ├── effect.ts
│ ├── hook.ts
│ ├── inject.ts
│ ├── memo.ts
│ ├── observable.ts
│ ├── props.ts
│ ├── store.ts
│ ├── storePart.ts
│ └── unobserve.ts
├── hooks
│ └── useStore.ts
├── index.ts
├── proxy
│ ├── adtProxy
│ │ ├── adtProxyBuilder.ts
│ │ ├── array.proxyBuilder.ts
│ │ ├── map.proxyBuilder.ts
│ │ └── object.proxyBuilder.ts
│ ├── deepUnproxy.ts
│ ├── proxyValueAndSaveIt.ts
│ └── storeForConsumerComponentProxy.ts
├── reactStore.ts
├── store
│ ├── StoreProvider.tsx
│ ├── administrator
│ │ ├── getters
│ │ │ ├── memoizedProperty.ts
│ │ │ └── storeGettersManager.ts
│ │ ├── hooksManager.ts
│ │ ├── methods
│ │ │ ├── methodProxyHandler.ts
│ │ │ └── storeMethodsManager.ts
│ │ ├── propertyKeys
│ │ │ ├── observableProperty.ts
│ │ │ ├── storePropertyKeysManager.ts
│ │ │ └── unobservableProperty.ts
│ │ ├── propsManager.ts
│ │ ├── storeAdministrator.ts
│ │ └── storeEffectsManager.ts
│ ├── connect.tsx
│ └── storeFactory.ts
├── types.ts
└── utils
│ ├── decoratorsMetadataStorage.ts
│ ├── getUnProxiedValue.ts
│ ├── isClass.ts
│ ├── isPrimitive.ts
│ ├── toPlainObj.ts
│ ├── useForceUpdate.ts
│ ├── useLazyRef.ts
│ └── useWillMount.ts
├── tests
├── container.spec.ts
├── defineInjectable.spec.ts
├── depsInjection.spec.tsx
├── e2e
│ ├── cypress.config.ts
│ ├── cypress.d.ts
│ ├── cypress
│ │ ├── component
│ │ │ └── methods
│ │ │ │ └── methodExecutionContext.cy.tsx
│ │ └── support
│ │ │ ├── commands.ts
│ │ │ ├── component-index.html
│ │ │ └── component.ts
│ ├── tsconfig.json
│ └── webpack.config.ts
├── effectDecorator.spec.tsx
├── hookDecorator.spec.tsx
├── immutableObjects.spec.tsx
├── memoDecorator.spec.tsx
├── methods.spec.tsx
├── observableDecorator.spec.tsx
├── propertyKeyObservability.spec.tsx
├── propsDecorator.spec.tsx
├── proxyStoreForComponent.spec.tsx
├── pureReactCompatibility.spec.tsx
├── renderPerformance.spec.tsx
├── setupTest.ts
├── storeDecorator.spec.tsx
└── storePartDecorator.spec.tsx
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | coverage
3 | sampleApp/*
4 | tests/*
5 | *.config.js
6 | *.spec.ts
7 | *.spec.tsx
8 | *._spec.tsx
9 | *._spec.ts
10 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 | - name: Run a one-line script
11 | run: |
12 | yarn install
13 | yarn build
14 | yarn lint
15 | yarn test
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | yarn-error.log
4 | .vscode
5 | coverage
6 | videos
7 | screenshots
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 | yarn lint-staged
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | tests
3 | sampleApp
4 | webpack.config.js
5 | tsconfig*
6 | babel*
7 | jest*
8 | .vscode
9 | _config.yml
10 | rollup*
11 | coverage
12 | .husky
13 | .eslintignore
14 | .prettierignore
15 | README.md
16 | LICENSE
17 | .github
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | coverage
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Amir Hossien Qasemi Moqaddam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Store
2 |
3 | 
4 | 
5 | 
6 |
7 | **React Store** is a state management library for React which facilitates to split components into smaller
8 | and maintainable ones then share `States` between them and also let developers to use `class`es to manage
9 | their components logic alongside it's IOC container.
10 |
11 | ## Table of content
12 |
13 | - [Installation](#installation)
14 | - [Usage](#usage)
15 | - [Effects](#effects)
16 | - [Props](#props)
17 | - [Store Part](#store-part)
18 | - [Computed Property](#computed-property)
19 | - [Dependency Injection](#dependency-injection)
20 |
21 | ## Installation
22 |
23 | First install core library:
24 |
25 | `yarn add @react-store/core`
26 |
27 | Then enable **decorators** and **decorators metadata** in typescript:
28 |
29 | ```json
30 | {
31 | "compilerOptions": {
32 | "emitDecoratorMetadata": true,
33 | "experimentalDecorators": true
34 | }
35 | ```
36 |
37 | You can also use other javascript transpilers such as babel.
38 |
39 | > See `example` folder for those how use Create-React-App
40 |
41 | ## Usage
42 |
43 | Now it's ready. First create a `Store`:
44 |
45 | ```ts
46 | // user.store.ts
47 | import { Store } from "@react-store/core";
48 |
49 | @Store()
50 | class UserStore {
51 | name: string;
52 |
53 | onNameChange(e: ChangeEvent) {
54 | this.name = e.target.value;
55 | }
56 | }
57 | ```
58 |
59 | Then connect it to the component **tree** by using `connect` function as component wrapper, call `useStore` and pass it **store class** to access store instance:
60 |
61 | ```tsx
62 | // App.tsx
63 | import { connect, useStore } from "@react-store/core";
64 |
65 | interface Props {
66 | p1: string;
67 | }
68 |
69 | const App = connect((props: Props) => {
70 | const st = useStore(UserStore);
71 | return (
72 |
73 | {st.name}
74 |
75 |
76 | );
77 | }, UserStore);
78 | ```
79 |
80 | And enjoy to use store in child components.
81 |
82 | ```jsx
83 | import { useStore } from "@react-store/core";
84 |
85 | function Input() {
86 | const st = useStore(UserStore);
87 | return (
88 |
89 | Name is:
90 |
91 |
92 | );
93 | }
94 | ```
95 |
96 | ## Store property & method
97 |
98 | - _Property_: Each store property behind the sense is a `[state, setState] = useState(initVal)` it means when you set store property, actually you are doing `setState` and also when you read the property, actually you are reading the `state` but in reading scenario if you have been mutated `state` before reading it you will receive new value even before any rerender.
99 |
100 | - _Method_: Store methods are used for state mutations. store methods are bound to store class instance by default. feel free to use them like below:
101 |
102 | ```tsx
103 | function Input() {
104 | const st = useStore(UserStore);
105 | return ;
106 | }
107 | ```
108 |
109 | ## Effects
110 |
111 | You can manage side effects with `@Effect()` decorator. Like react `useEffect` dependency array you must define an array of dependencies.
112 |
For **clear effect** you can return a function from this method.
113 |
114 | ```ts
115 | @Store()
116 | class UserStore {
117 | name: string;
118 |
119 | @Effect((_: UserStore) => [_.name])
120 | nameChanged() {
121 | console.log("name changed to:", this.name);
122 | return () => console.log("Clear Effect");
123 | }
124 | }
125 | ```
126 |
127 | You also can pass object as dependency item with **deep equal** mode. To do that, pass **true** as second parameters:
128 |
129 | ```ts
130 | @Store()
131 | export class UserStore {
132 | user = { name: "" };
133 |
134 | @Effect((_) => [_.user], true)
135 | usernameChanged() {
136 | console.log("name changed to:", this.name);
137 | }
138 | }
139 | ```
140 |
141 | Instead of passing a function to effect decorator to detect dependencies you can pass an array of paths
142 |
143 | ```ts
144 | @Store()
145 | export class UserStore {
146 | user = { name: "" };
147 |
148 | @Effect(["user.name"])
149 | usernameChanged() {}
150 |
151 | // Only one dependency does not need to be warped by an array
152 | @Effect("user", true)
153 | userChanged() {}
154 | }
155 | ```
156 |
157 | ## Memo
158 |
159 | To memoize a value you can use `@Memo` decorator. Memo decorator parameters is like effect decorator:
160 |
161 | ```ts
162 | @Store()
163 | export class UserStore {
164 | user = { name: "", pass: "" };
165 |
166 | // @Memo(["user.name"])
167 | @Memo("user.name")
168 | get usernameLen() {
169 | return this.user.name.length;
170 | }
171 |
172 | @Memo(["user"], true)
173 | get passLen() {
174 | return this.user.pass;
175 | }
176 | }
177 | ```
178 |
179 | You can manage side effects with `@Effect()` decorator. Like react `useEffect` dependency array you must define an array of dependencies.
180 |
For **clear effect** you can return a function from this method.
181 |
182 | > Methods which decorate with `@Effect()` can be async, but if you want to return `clear effect` function make it sync method
183 |
184 | ## Props
185 |
186 | To have store parent component props (the component directly connected to store by using `connect`) inside store class use `@Props()`:
187 |
188 | ```ts
189 | // user.store.ts
190 | import type { Props as AppProps } from "./App";
191 | import { Props, Store } from "@react-store/core";
192 |
193 | @Store()
194 | export class UserStore {
195 | @Props()
196 | props: AppProps;
197 | }
198 | ```
199 |
200 | ## Store Part
201 |
202 | `Store Part` like store is a class which is decorated with `@StorePart()` and can **only** be connected to a store with `@Wire()` decorator.
203 |
204 | ```ts
205 | @StorePart()
206 | class Validator {
207 | object: Record;
208 |
209 | hasError = false;
210 |
211 | @Effect("object", true)
212 | validate() {
213 | this.hasError = someValidator(object).hasError;
214 | }
215 | }
216 |
217 | @Store()
218 | class UserForm {
219 | user: User;
220 |
221 | @Wire(Validator)
222 | validator: Validator;
223 |
224 | @Effect([])
225 | onMount() {
226 | this.validator.object = this.user;
227 | }
228 |
229 | onUsernameChange(username) {
230 | this.user.username = username;
231 | }
232 | }
233 | ```
234 |
235 | - Store part **can not** be used directly with `useStore` and must be wired to a store.
236 | - Like store, store part can have it's effects, dependency injection.
237 | - Store part is piece of logics and states can be wired to any other store and play a role like React `custom hooks`
238 |
239 | ## Computed Property
240 |
241 | You can define getter in store class and automatically it will be a `computed` value. it means that if any underlying class properties which is used in
242 | getter change, we will recompute getter value and cache it.
243 |
244 | ```ts
245 | @Store()
246 | class BoxStore {
247 | width: number;
248 |
249 | height: number;
250 |
251 | get area() {
252 | return (this.width + this.height) * 2;
253 | }
254 | }
255 | ```
256 |
257 | ## Dependency Injection
258 |
259 | In this library we have also supported dependency injection. To define `Injectable`s, decorate class with `@Injectable()`:
260 |
261 | ```ts
262 | @Injectable()
263 | class UserService {}
264 | ```
265 |
266 | In order to inject dependencies into injectable, use `@Inject(...)`:
267 |
268 | ```ts
269 | @Injectable()
270 | @Inject(AuthService, UserService)
271 | class PostService {
272 | constructor(private authService: AuthService, private userService: UserService) {}
273 | }
274 | ```
275 |
276 | Also you can use `@Inject()` as parameter decorator:
277 |
278 | ```ts
279 | @Injectable()
280 | @Inject(AuthService)
281 | class PostService {
282 | constructor(
283 | private authService: AuthService,
284 | @Inject(UserService) private userService: UserService
285 | ) {}
286 | }
287 | ```
288 |
289 | Injection works fine for **stores**. Injectable can be injected into all stores. Also stores can be injected into other stores but there is one condition. For example, you want to inject `A` store into `B` store so the component which is wrapped with `connect(..., A)` must be higher in `B` store parent component. In other words, it works like React `useContext` rule.
290 |
291 | ```ts
292 | @Injectable()
293 | @Inject(AlertsStore)
294 | class UserStore {
295 | constructor(private alertsStore: AlertsStore) {}
296 | }
297 | ```
298 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"],
3 | plugins: [
4 | "babel-plugin-transform-typescript-metadata",
5 | ["@babel/plugin-proposal-decorators", { legacy: true }],
6 | ["@babel/plugin-proposal-class-properties", { loose: true }],
7 | ["@babel/plugin-proposal-private-methods", { loose: true }],
8 | ["@babel/plugin-proposal-private-property-in-object", { loose: true }],
9 | "@babel/plugin-transform-runtime",
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after `n` failures
9 | // bail: 0,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/s4/bxb34dln0n79bskmg8l_njpw0000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: undefined,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: "coverage",
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: undefined,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: undefined,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // Force coverage collection from ignored files using an array of glob patterns
52 | // forceCoverageMatch: [],
53 |
54 | // A path to a module which exports an async function that is triggered once before all test suites
55 | // globalSetup: undefined,
56 |
57 | // A path to a module which exports an async function that is triggered once after all test suites
58 | // globalTeardown: undefined,
59 |
60 | // A set of global variables that need to be available in all test environments
61 | // globals: {},
62 |
63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
64 | // maxWorkers: "50%",
65 |
66 | // An array of directory names to be searched recursively up from the requiring module's location
67 | // moduleDirectories: [
68 | // "node_modules"
69 | // ],
70 |
71 | // An array of file extensions your modules use
72 | // moduleFileExtensions: [
73 | // "js",
74 | // "json",
75 | // "jsx",
76 | // "ts",
77 | // "tsx",
78 | // "node"
79 | // ],
80 |
81 | // A map from regular expressions to module names that allow to stub out resources with a single module
82 | moduleNameMapper: {
83 | "@react-store/core": "/src",
84 | "src(.*)$": "/src/$1",
85 | },
86 |
87 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
88 | // modulePathIgnorePatterns: [],
89 |
90 | // Activates notifications for test results
91 | // notify: false,
92 |
93 | // An enum that specifies notification mode. Requires { notify: true }
94 | // notifyMode: "failure-change",
95 |
96 | // A preset that is used as a base for Jest's configuration
97 | // preset: undefined,
98 |
99 | // Run tests from one or more projects
100 | // projects: undefined,
101 |
102 | // Use this configuration option to add custom reporters to Jest
103 | // reporters: undefined,
104 |
105 | // Automatically reset mock state between every test
106 | // resetMocks: false,
107 |
108 | // Reset the module registry before running each individual test
109 | // resetModules: false,
110 |
111 | // A path to a custom resolver
112 | // resolver: undefined,
113 |
114 | // Automatically restore mock state between every test
115 | // restoreMocks: false,
116 |
117 | // The root directory that Jest should scan for tests and modules within
118 | // rootDir: undefined,
119 |
120 | // A list of paths to directories that Jest should use to search for files in
121 | // roots: [
122 | // ""
123 | // ],
124 |
125 | // Allows you to use a custom runner instead of Jest's default test runner
126 | // runner: "jest-runner",
127 |
128 | // The paths to modules that run some code to configure or set up the testing environment before each test
129 | // setupFiles: ["/tests/setupTest.ts"],
130 |
131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
132 | setupFilesAfterEnv: ["/tests/setupTest.ts"],
133 |
134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
135 | // snapshotSerializers: [],
136 |
137 | // The test environment that will be used for testing
138 | testEnvironment: "jsdom",
139 |
140 | // Options that will be passed to the testEnvironment
141 | // testEnvironmentOptions: {},
142 |
143 | // Adds a location field to test results
144 | // testLocationInResults: false,
145 |
146 | // The glob patterns Jest uses to detect test files
147 | // testMatch: [
148 | // "**/__tests__/**/*.[jt]s?(x)",
149 | // "**/?(*.)+(spec|test).[tj]s?(x)"
150 | // ],
151 |
152 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
153 | // testPathIgnorePatterns: [
154 | // "/node_modules/"
155 | // ],
156 |
157 | // The regexp pattern or array of patterns that Jest uses to detect test files
158 | // testRegex: [],
159 |
160 | // This option allows the use of a custom results processor
161 | // testResultsProcessor: undefined,
162 |
163 | // This option allows use of a custom test runner
164 | // testRunner: "jasmine2",
165 |
166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
167 | // testURL: "http://localhost",
168 |
169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
170 | // timers: "real",
171 |
172 | // A map from regular expressions to paths to transformers
173 | // transform: undefined,
174 |
175 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
176 | // transformIgnorePatterns: [
177 | // "/node_modules/"
178 | // ],
179 |
180 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
181 | // unmockedModulePathPatterns: undefined,
182 |
183 | // Indicates whether each individual test should be reported during the run
184 | // verbose: undefined,
185 |
186 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
187 | // watchPathIgnorePatterns: [],
188 |
189 | // Whether to use watchman for file crawling
190 | // watchman: true,
191 | };
192 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-store/core",
3 | "version": "0.0.40",
4 | "main": "dist/index.js",
5 | "repository": "https://github.com/amirqasemi74/react-store.git",
6 | "author": "Amir Hossein Qasemi Moqaddam ",
7 | "license": "MIT",
8 | "dependencies": {
9 | "clone-deep": "^4.0.1",
10 | "dequal": "^2.0.3",
11 | "is-promise": "^4.0.0",
12 | "lodash": "^4.17.21",
13 | "reflect-metadata": "^0.1.13"
14 | },
15 | "peerDependencies": {
16 | "react": "^17.0.0 || ^18.0.0",
17 | "react-dom": "^17.0.0 || ^18.0.0"
18 | },
19 | "devDependencies": {
20 | "@babel/plugin-proposal-class-properties": "^7.18.6",
21 | "@babel/plugin-proposal-decorators": "^7.18.6",
22 | "@babel/plugin-transform-runtime": "^7.18.6",
23 | "@babel/preset-env": "^7.18.6",
24 | "@babel/preset-react": "^7.18.6",
25 | "@babel/preset-typescript": "^7.18.6",
26 | "@commitlint/cli": "^17.0.3",
27 | "@commitlint/config-conventional": "^17.0.3",
28 | "@testing-library/jest-dom": "^5.16.4",
29 | "@testing-library/react": "^13.3.0",
30 | "@trivago/prettier-plugin-sort-imports": "^3.2.0",
31 | "@types/clone-deep": "^4.0.1",
32 | "@types/jest": "^28.1.5",
33 | "@types/lodash": "^4.14.182",
34 | "@types/react": "^18.0.15",
35 | "@types/react-dom": "^18.0.6",
36 | "@types/testing-library__jest-dom": "^5.14.5",
37 | "@typescript-eslint/eslint-plugin": "^5.30.6",
38 | "@typescript-eslint/parser": "^5.30.6",
39 | "@zerollup/ts-transform-paths": "^1.7.18",
40 | "babel-jest": "^28.1.3",
41 | "babel-plugin-transform-typescript-metadata": "^0.3.2",
42 | "cypress": "^10.8.0",
43 | "eslint": "^8.19.0",
44 | "eslint-config-prettier": "^8.5.0",
45 | "graphql": "^16.6.0",
46 | "html-webpack-plugin": "^5.5.0",
47 | "husky": "^8.0.1",
48 | "jest": "^28.1.3",
49 | "jest-environment-jsdom": "^28.1.3",
50 | "lint-staged": ">=13",
51 | "prettier": "^2.7.1",
52 | "react": "^18.2.0",
53 | "react-dom": "^18.2.0",
54 | "react-router-dom": "6",
55 | "rollup": "^2.77.0",
56 | "rollup-plugin-typescript2": "^0.32.1",
57 | "ts-loader": "^9.3.1",
58 | "ttypescript": "^1.5.13",
59 | "typescript": "^4.7.4",
60 | "webpack": "^5.73.0",
61 | "webpack-cli": "^4.10.0",
62 | "webpack-dev-server": "^4.9.3",
63 | "websocket-extensions": "^0.1.4"
64 | },
65 | "scripts": {
66 | "start:app": "webpack serve --config tests/browser/webpack.config.js",
67 | "test": "yarn test:jest && yarn test:cypress",
68 | "test:jest": "jest",
69 | "test:cypress": "yarn cy:run",
70 | "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --detectOpenHandles",
71 | "build": "rm -rf dist && rollup -c rollup.config.js",
72 | "prepare": "husky install && yarn build",
73 | "pub": "npm publish --access public",
74 | "format": "yarn prettier --write",
75 | "lint": "yarn eslint 'src/**/*.{ts,tsx}' --max-warnings=0",
76 | "cy:open": "yarn cypress open --project tests/e2e",
77 | "cy:run": "yarn cypress run --component --project tests/e2e"
78 | },
79 | "lint-staged": {
80 | "src/**/*.{js,css,ts,tsx}": [
81 | "yarn format",
82 | "yarn lint"
83 | ]
84 | },
85 | "prettier": {
86 | "printWidth": 85,
87 | "importOrderSeparation": true,
88 | "importOrderSortSpecifiers": true,
89 | "importOrderParserPlugins": [
90 | "typescript",
91 | "jsx",
92 | "classProperties",
93 | "decorators-legacy"
94 | ]
95 | },
96 | "commitlint": {
97 | "extends": [
98 | "@commitlint/config-conventional"
99 | ]
100 | },
101 | "eslintConfig": {
102 | "root": true,
103 | "parser": "@typescript-eslint/parser",
104 | "plugins": [
105 | "@typescript-eslint"
106 | ],
107 | "extends": [
108 | "eslint:recommended",
109 | "plugin:@typescript-eslint/recommended",
110 | "prettier"
111 | ],
112 | "rules": {
113 | "no-console": [
114 | "error",
115 | {
116 | "allow": [
117 | "error",
118 | "warn"
119 | ]
120 | }
121 | ],
122 | "@typescript-eslint/no-explicit-any": [
123 | "error",
124 | {
125 | "ignoreRestArgs": true
126 | }
127 | ],
128 | "@typescript-eslint/ban-ts-comment": "off",
129 | "@typescript-eslint/no-non-null-assertion": "off",
130 | "prefer-const": "off",
131 | "no-empty-pattern": "off",
132 | "no-self-assign": "off"
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // import typescript from "@rollup/plugin-typescript";
2 | import { resolve } from "path";
3 | import typescript from "rollup-plugin-typescript2";
4 | import ttypescript from "ttypescript";
5 |
6 | const tsconfig = resolve(__dirname, "tsconfig.build.json");
7 |
8 | export default {
9 | input: "src/index.ts",
10 | output: {
11 | dir: "dist",
12 | format: "es",
13 | },
14 | plugins: [typescript({ tsconfig, typescript: ttypescript })],
15 | external: [
16 | "dequal",
17 | "react",
18 | "react-dom",
19 | "lodash/get",
20 | "is-promise",
21 | "clone-deep",
22 | "reflect-metadata",
23 | ],
24 | };
25 |
--------------------------------------------------------------------------------
/src/appContext.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from ".";
2 | import { StoreAdministrator } from "./store/administrator/storeAdministrator";
3 | import React from "react";
4 | import { ClassType } from "src/types";
5 |
6 | @Injectable()
7 | export class ReactApplicationContext {
8 | private storeContexts = new Map();
9 |
10 | registerStoreContext(
11 | storeType: ClassType,
12 | context: StoreAdministratorReactContext
13 | ) {
14 | this.storeContexts.set(storeType, context);
15 | }
16 |
17 | getStoreReactContext(storeType: ClassType) {
18 | return this.storeContexts.get(storeType);
19 | }
20 | }
21 |
22 | export type StoreAdministratorReactContext = React.Context<{
23 | id: string;
24 | storeAdmin: StoreAdministrator;
25 | } | null>;
26 |
--------------------------------------------------------------------------------
/src/constant.ts:
--------------------------------------------------------------------------------
1 | export const TARGET = Symbol("TARGET");
2 | export const STORE_ADMINISTRATION = Symbol("STORE_ADMINISTRATION");
3 | export const PROXY_HANDLER_TYPE = Symbol("PROXY_HANDLER_TYPE");
4 |
--------------------------------------------------------------------------------
/src/container/Injector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, ReactStore } from "..";
2 | import { ClassType } from "src/types";
3 |
4 | @Injectable()
5 | export class Injector {
6 | get(token: T) {
7 | return ReactStore.container.resolve(token);
8 | }
9 |
10 | getLazy(token: T) {
11 | return new Promise>((res) =>
12 | setTimeout(() => res(ReactStore.container.resolve(token)), 0)
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/container/container.ts:
--------------------------------------------------------------------------------
1 | import { Scope } from "..";
2 | import { InjectableMetadata } from "src/decorators/Injectable";
3 | import { getClassDependenciesType } from "src/decorators/inject";
4 | import { ClassType, Func } from "src/types";
5 | import { decoratorsMetadataStorage } from "src/utils/decoratorsMetadataStorage";
6 | import { isClass } from "src/utils/isClass";
7 |
8 | class Container {
9 | private readonly instances = new Map();
10 |
11 | resolve(token: InjectableToken): T extends ClassType ? InstanceType : T {
12 | const scope = isClass(token)
13 | ? decoratorsMetadataStorage.get("Injectable", token)[0]
14 | : Scope.SINGLETON;
15 |
16 | if (!scope) {
17 | if (isClass(token)) {
18 | throw new Error(
19 | `\`class ${token.name}\` has not been decorated with @Injectable()`
20 | );
21 | } else {
22 | throw new Error(`\`${token.toString()}\` can't be retrieved from container`);
23 | }
24 | }
25 |
26 | if (isClass(token)) {
27 | switch (scope) {
28 | case Scope.TRANSIENT: {
29 | //eslint-disable-next-line
30 | return new token(...this.resolveDependencies(token)) as any;
31 | }
32 | case Scope.SINGLETON:
33 | default: {
34 | let instance = this.instances.get(token);
35 | if (!instance) {
36 | instance = new token(...this.resolveDependencies(token));
37 | this.instances.set(token, instance);
38 | }
39 | //eslint-disable-next-line
40 | return instance as any;
41 | }
42 | }
43 | } else {
44 | //eslint-disable-next-line
45 | return this.instances.get(token) as any;
46 | }
47 | }
48 |
49 | resolveDependencies(someClass: ClassType) {
50 | // INJECTABLE
51 | return getClassDependenciesType(someClass).map((type) => this.resolve(type));
52 | }
53 |
54 | defineInjectable(
55 | injectable: //eslint-disable-next-line
56 | | { token: InjectableToken; value: any }
57 | | { token: InjectableToken; class: ClassType }
58 | | { token: InjectableToken; factory: Func; inject?: Array }
59 | ) {
60 | const token = injectable.token;
61 | if ("value" in injectable) {
62 | this.instances.set(token, injectable.value);
63 | } else if ("class" in injectable) {
64 | if (isClass(token)) {
65 | this.instances.set(
66 | token,
67 | new injectable.class(...this.resolveDependencies(token))
68 | );
69 | } else {
70 | this.instances.set(token, new injectable.class());
71 | }
72 | } else {
73 | this.instances.set(
74 | token,
75 | injectable.factory(...(injectable.inject || []).map((t) => this.resolve(t)))
76 | );
77 | }
78 | }
79 |
80 | remove(someClass: ClassType) {
81 | this.instances.delete(someClass);
82 | }
83 |
84 | clear() {
85 | this.instances.clear();
86 | }
87 | }
88 |
89 | export const container = new Container();
90 |
91 | //eslint-disable-next-line
92 | type InjectableToken = string | symbol | ClassType;
93 |
--------------------------------------------------------------------------------
/src/decorators/Injectable.ts:
--------------------------------------------------------------------------------
1 | import { Inject } from "./inject";
2 | import "reflect-metadata";
3 | import { ClassType } from "src/types";
4 | import { decoratorsMetadataStorage } from "src/utils/decoratorsMetadataStorage";
5 |
6 | export function Injectable(scope = Scope.SINGLETON): ClassDecorator {
7 | return function (target: ClassType) {
8 | decoratorsMetadataStorage.add("Injectable", target, scope);
9 | Inject()(target);
10 | } as ClassDecorator;
11 | }
12 |
13 | export enum Scope {
14 | SINGLETON = "SINGLETON",
15 | TRANSIENT = "TRANSIENT",
16 | }
17 |
18 | export type InjectableMetadata = Scope;
19 |
--------------------------------------------------------------------------------
/src/decorators/effect.ts:
--------------------------------------------------------------------------------
1 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
2 | import lodashGet from "lodash/get";
3 | import type { ClassType } from "src/types";
4 |
5 | type DepFn = (storeInstance: T) => Array;
6 |
7 | export function Effect(
8 | deps?: DepFn | Array | string,
9 | deepEqual?: boolean
10 | ): MethodDecorator {
11 | return function (target, propertyKey, descriptor) {
12 | let depsFn: DepFn | undefined;
13 | if (typeof deps === "function") {
14 | depsFn = deps;
15 | } else if (Array.isArray(deps)) {
16 | depsFn = (o) => deps.map((d) => lodashGet(o, d));
17 | } else if (typeof deps === "string") {
18 | depsFn = (o) => [lodashGet(o, deps)];
19 | }
20 |
21 | decoratorsMetadataStorage.add(
22 | "Effect",
23 | target.constructor as ClassType,
24 | {
25 | options: {
26 | deps: depsFn,
27 | deepEqual,
28 | } as ManualEffectOptions,
29 | propertyKey,
30 | }
31 | );
32 | return descriptor;
33 | };
34 | }
35 |
36 | export interface ManualEffectOptions {
37 | auto?: false;
38 | deps?: (_: T) => Array;
39 | deepEqual?: boolean;
40 | }
41 |
42 | type EffectOptions = ManualEffectOptions;
43 |
44 | export interface EffectMetaData {
45 | propertyKey: PropertyKey;
46 | options: EffectOptions;
47 | }
48 |
--------------------------------------------------------------------------------
/src/decorators/hook.ts:
--------------------------------------------------------------------------------
1 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
2 | import { ClassType, Func } from "src/types";
3 |
4 | export function Hook(hook: Func): PropertyDecorator {
5 | return function (target, propertyKey) {
6 | decoratorsMetadataStorage.add(
7 | "Hook",
8 | target.constructor as ClassType,
9 | {
10 | propertyKey,
11 | hook,
12 | }
13 | );
14 | };
15 | }
16 |
17 | export interface HookMetadata {
18 | hook: Func;
19 | propertyKey: PropertyKey;
20 | }
21 |
--------------------------------------------------------------------------------
/src/decorators/inject.ts:
--------------------------------------------------------------------------------
1 | import { ClassType } from "src/types";
2 | import { decoratorsMetadataStorage } from "src/utils/decoratorsMetadataStorage";
3 |
4 | export function Inject(...deps: any[]) {
5 | return function (...args: [ClassType] | [ClassType, undefined, number]) {
6 | const [target, , paramIndex] = args;
7 | let type: InjectType;
8 |
9 | // deps len === 0:
10 | // 1. called from @Injectable
11 | // 2. parameter decorator
12 | if (deps.length === 0 && args.length === 1) {
13 | type = "CLASS_METADATA";
14 | deps = Reflect.getOwnMetadata("design:paramtypes", target) || null;
15 | } else {
16 | type = args.length === 1 ? "CLASS" : "PARAMETER";
17 | }
18 |
19 | const hasInjectType = (type: InjectType) =>
20 | decoratorsMetadataStorage
21 | .getOwn("Inject", target)
22 | .some((inj) =>
23 | inj.type === "PARAMETER"
24 | ? inj.type === type
25 | : inj.type === type && inj.deps !== null
26 | );
27 |
28 | if (hasInjectType("PARAMETER") && type === "CLASS") {
29 | throw new Error(
30 | `Dependencies are injecting by @Inject() as parameter and class decorator for \`class ${target.name}\`. Use one of them.`
31 | );
32 | }
33 |
34 | if (type === "PARAMETER") {
35 | decoratorsMetadataStorage.add("Inject", target, {
36 | type,
37 | dep: deps[0],
38 | paramIndex: paramIndex!,
39 | });
40 | } else {
41 | decoratorsMetadataStorage.add("Inject", target, {
42 | deps,
43 | type,
44 | });
45 | }
46 |
47 | if (hasInjectType("CLASS") && hasInjectType("CLASS_METADATA")) {
48 | console.warn(
49 | `Dependencies are automatically detected for \`class ${target.name}\`. Remove @Inject(...)`
50 | );
51 | }
52 | };
53 | }
54 |
55 | export const getClassDependenciesType = (
56 | classType: ClassType | null
57 | ): ClassType[] => {
58 | if (classType) {
59 | let depsType: ClassType[] = [];
60 | const metadata = decoratorsMetadataStorage.getOwn(
61 | "Inject",
62 | classType
63 | );
64 | metadata.forEach((inj) => {
65 | if (inj.type === "CLASS" || inj.type === "CLASS_METADATA") {
66 | depsType = inj.deps || [];
67 | }
68 | });
69 | metadata.forEach((inj) => {
70 | if (inj.type === "PARAMETER") {
71 | depsType[inj.paramIndex] = inj.dep;
72 | }
73 | });
74 |
75 | return depsType.length
76 | ? depsType
77 | : getClassDependenciesType(Reflect.getPrototypeOf(classType) as ClassType);
78 | }
79 |
80 | return [];
81 | };
82 |
83 | export type DecoratedWith = "STORE" | "STORE_PART" | "INJECTABLE";
84 | type InjectType = "CLASS" | "CLASS_METADATA" | "PARAMETER";
85 |
86 | type InjectMetadata =
87 | | {
88 | deps: ClassType[] | null;
89 | type: "CLASS" | "CLASS_METADATA";
90 | }
91 | | {
92 | type: "PARAMETER";
93 | dep: ClassType;
94 | paramIndex: number;
95 | };
96 |
--------------------------------------------------------------------------------
/src/decorators/memo.ts:
--------------------------------------------------------------------------------
1 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
2 | import lodashGet from "lodash/get";
3 | import { ClassType } from "src/types";
4 |
5 | type DepFn = (storeInstance: T) => Array;
6 |
7 | export function Memo(
8 | deps: DepFn | Array | string,
9 | deepEqual?: boolean
10 | ): MethodDecorator {
11 | return function (target, propertyKey, descriptor) {
12 | let depsFn: DepFn | undefined;
13 | if (typeof deps === "function") {
14 | depsFn = deps;
15 | } else if (Array.isArray(deps)) {
16 | depsFn = (o) => deps.map((d) => lodashGet(o, d));
17 | } else if (typeof deps === "string") {
18 | depsFn = (o) => [lodashGet(o, deps)];
19 | }
20 |
21 | decoratorsMetadataStorage.add(
22 | "Memo",
23 | target.constructor as ClassType,
24 | {
25 | options: {
26 | deps: depsFn,
27 | deepEqual,
28 | } as MemoOptions,
29 | propertyKey,
30 | }
31 | );
32 | return descriptor;
33 | };
34 | }
35 |
36 | interface MemoOptions {
37 | deps?: (_: T) => Array;
38 | deepEqual?: boolean;
39 | }
40 |
41 | export interface MemoMetadata {
42 | propertyKey: PropertyKey;
43 | options: MemoOptions;
44 | }
45 |
--------------------------------------------------------------------------------
/src/decorators/observable.ts:
--------------------------------------------------------------------------------
1 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
2 | import { ClassType } from "src/types";
3 |
4 | export function Observable() {
5 | return function (target: ClassType) {
6 | decoratorsMetadataStorage.add("Observable", target, true);
7 | } as ClassDecorator;
8 | }
9 |
10 | export type ObservableMetadata = boolean;
11 |
--------------------------------------------------------------------------------
/src/decorators/props.ts:
--------------------------------------------------------------------------------
1 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
2 | import { ClassType } from "src/types";
3 |
4 | export function Props(): PropertyDecorator {
5 | return function (target, propertyKey) {
6 | decoratorsMetadataStorage.add(
7 | "Props",
8 | target.constructor as ClassType,
9 | propertyKey
10 | );
11 | };
12 | }
13 |
14 | export type PropsMetadata = PropertyKey;
15 |
--------------------------------------------------------------------------------
/src/decorators/store.ts:
--------------------------------------------------------------------------------
1 | import { Inject } from "..";
2 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
3 | import { ClassType } from "src/types";
4 |
5 | export function Store() {
6 | return function (StoreType: ClassType) {
7 | decoratorsMetadataStorage.add("Store", StoreType, true);
8 | Inject()(StoreType);
9 | } as ClassDecorator;
10 | }
11 |
12 | export type StoreMetadata = boolean;
13 |
--------------------------------------------------------------------------------
/src/decorators/storePart.ts:
--------------------------------------------------------------------------------
1 | import { Inject } from "..";
2 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
3 | import { ClassType } from "src/types";
4 |
5 | export function StorePart() {
6 | return function (StorePartType: ClassType) {
7 | decoratorsMetadataStorage.add("StorePart", StorePartType, true);
8 | Inject()(StorePartType);
9 | } as ClassDecorator;
10 | }
11 |
12 | export type StorePartMetadata = boolean;
13 |
--------------------------------------------------------------------------------
/src/decorators/unobserve.ts:
--------------------------------------------------------------------------------
1 | import { decoratorsMetadataStorage } from "../utils/decoratorsMetadataStorage";
2 | import { ClassType } from "src/types";
3 |
4 | export function Unobserve(): PropertyDecorator {
5 | return function (target, propertyKey) {
6 | decoratorsMetadataStorage.add(
7 | "Unobserve",
8 | target.constructor as ClassType,
9 | propertyKey
10 | );
11 | };
12 | }
13 |
14 | export type UnobserveMetadata = PropertyKey;
15 |
--------------------------------------------------------------------------------
/src/hooks/useStore.ts:
--------------------------------------------------------------------------------
1 | import { ReactStore } from "..";
2 | import { ReactApplicationContext } from "../appContext";
3 | import { useContext } from "react";
4 | import { ClassType } from "src/types";
5 |
6 | export const useStore = (storeType: T): InstanceType => {
7 | const StoreContext = ReactStore.container
8 | .resolve(ReactApplicationContext)
9 | .getStoreReactContext(storeType);
10 |
11 | if (!StoreContext) {
12 | throw new Error(
13 | `${storeType.name} haven't been connected to the component tree!`
14 | );
15 | }
16 |
17 | const context = useContext(StoreContext);
18 |
19 | if (!context) {
20 | throw new Error(`\`${storeType.name}\` can't be reached.`);
21 | }
22 |
23 | return context.storeAdmin.instanceForComponents;
24 | };
25 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { ReactStore } from "./reactStore";
2 | export { Injectable, Scope } from "./decorators/Injectable";
3 | export { Injector } from "./container/Injector";
4 | export { useStore } from "./hooks/useStore";
5 | export { connect } from "./store/connect";
6 | export { StoreProvider } from "./store/StoreProvider";
7 | export { Props } from "./decorators/props";
8 | export { Effect } from "./decorators/effect";
9 | export { Store } from "./decorators/store";
10 | export { Inject } from "./decorators/inject";
11 | export { StorePart } from "./decorators/storePart";
12 | export { Observable } from "./decorators/observable";
13 | export { toPlainObj } from "./utils/toPlainObj";
14 | export { Hook } from "./decorators/hook";
15 | export { Memo } from "./decorators/memo";
16 | export { Unobserve } from "./decorators/unobserve";
17 |
--------------------------------------------------------------------------------
/src/proxy/adtProxy/adtProxyBuilder.ts:
--------------------------------------------------------------------------------
1 | import { arrayProxyBuilder } from "./array.proxyBuilder";
2 | import { mapProxyBuilder } from "./map.proxyBuilder";
3 | import { objectProxyBuilder } from "./object.proxyBuilder";
4 | import { ObservableMetadata } from "src/decorators/observable";
5 | import { StoreMetadata } from "src/decorators/store";
6 | import { StorePartMetadata } from "src/decorators/storePart";
7 | import { decoratorsMetadataStorage } from "src/utils/decoratorsMetadataStorage";
8 |
9 | export interface BaseAdtProxyBuilderArgs {
10 | onSet?: () => void;
11 | proxyTypes?: Array<"Array" | "Object" | "Map">;
12 | proxiedValuesStorage: Map;
13 | }
14 |
15 | interface AdtProxyBuilderArgs extends BaseAdtProxyBuilderArgs {
16 | value: unknown;
17 | }
18 |
19 | export const adtProxyBuilder = ({ value, ...restOfArgs }: AdtProxyBuilderArgs) => {
20 | // eslint-disable-next-line
21 | const valType = (value as any)?.constructor;
22 | const { proxyTypes } = restOfArgs;
23 | const doMapProxy = proxyTypes?.includes("Map") ?? true;
24 | const doArrayProxy = proxyTypes?.includes("Array") ?? true;
25 | const doObjectProxy = proxyTypes?.includes("Object") ?? true;
26 |
27 | try {
28 | if (
29 | (valType === Object ||
30 | (value instanceof Object &&
31 | (decoratorsMetadataStorage.get(
32 | "Observable",
33 | valType
34 | )[0] ||
35 | decoratorsMetadataStorage.get("Store", valType).length ||
36 | decoratorsMetadataStorage.get("StorePart", valType)
37 | .length))) &&
38 | doObjectProxy
39 | ) {
40 | return objectProxyBuilder({
41 | object: value as object,
42 | ...restOfArgs,
43 | });
44 | }
45 |
46 | if (valType === Array && doArrayProxy) {
47 | return arrayProxyBuilder({
48 | array: value as unknown[],
49 | ...restOfArgs,
50 | });
51 | }
52 |
53 | if (value instanceof Map && doMapProxy) {
54 | return mapProxyBuilder({
55 | map: value,
56 | ...restOfArgs,
57 | });
58 | }
59 | } catch (error) {
60 | // Nothing to do
61 | }
62 | return value;
63 | };
64 |
--------------------------------------------------------------------------------
/src/proxy/adtProxy/array.proxyBuilder.ts:
--------------------------------------------------------------------------------
1 | import { deepUnproxy } from "../deepUnproxy";
2 | import { proxyValueAndSaveIt } from "../proxyValueAndSaveIt";
3 | import { BaseAdtProxyBuilderArgs } from "./adtProxyBuilder";
4 | import { TARGET } from "src/constant";
5 |
6 | interface ArrayProxyBuilderArgs extends BaseAdtProxyBuilderArgs {
7 | array: unknown[];
8 | }
9 |
10 | export const arrayProxyBuilder = ({
11 | array,
12 | ...restOfArgs
13 | }: ArrayProxyBuilderArgs): unknown[] => {
14 | const { onSet } = restOfArgs;
15 | const isFrozen = Object.isFrozen(array);
16 |
17 | return new Proxy(isFrozen ? [...array] : array, {
18 | get(target: unknown[], propertyKey: PropertyKey, receiver: unknown) {
19 | if (propertyKey === TARGET) {
20 | return target;
21 | }
22 | const value = proxyValueAndSaveIt(
23 | isFrozen ? array : target,
24 | propertyKey,
25 | receiver,
26 | restOfArgs
27 | );
28 |
29 | return value;
30 | },
31 |
32 | set(
33 | target: unknown[],
34 | propertyKey: PropertyKey,
35 | value: unknown,
36 | receiver: unknown
37 | ) {
38 | const res = Reflect.set(target, propertyKey, deepUnproxy(value), receiver);
39 | onSet?.();
40 | return res;
41 | },
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/src/proxy/adtProxy/map.proxyBuilder.ts:
--------------------------------------------------------------------------------
1 | import { deepUnproxy } from "../deepUnproxy";
2 | import { BaseAdtProxyBuilderArgs, adtProxyBuilder } from "./adtProxyBuilder";
3 | import { TARGET } from "src/constant";
4 | import { Func } from "src/types";
5 |
6 | interface MapProxyBuilderArgs extends BaseAdtProxyBuilderArgs {
7 | map: Map;
8 | }
9 |
10 | export const mapProxyBuilder = ({
11 | map,
12 | ...restOfArgs
13 | }: MapProxyBuilderArgs): object => {
14 | const { onSet } = restOfArgs;
15 | return new Proxy(map, {
16 | get(target: Map, propertyKey: PropertyKey) {
17 | if (propertyKey === TARGET) {
18 | return target;
19 | }
20 | const value = target[propertyKey];
21 | switch (propertyKey) {
22 | case "get": {
23 | return function (mapKey: unknown) {
24 | return (value as Func).call(target, mapKey);
25 | };
26 | }
27 |
28 | case "set": {
29 | return function (mapKey: unknown, mapValue: unknown) {
30 | (value as Func).call(
31 | target,
32 | mapKey,
33 | adtProxyBuilder({
34 | onSet,
35 | value: deepUnproxy(mapValue),
36 | ...restOfArgs,
37 | })
38 | );
39 | onSet?.();
40 | };
41 | }
42 |
43 | case "delete": {
44 | return function (mapKey: unknown) {
45 | (value as Func).call(target, mapKey);
46 | onSet?.();
47 | };
48 | }
49 |
50 | case "clear": {
51 | return function () {
52 | (value as Func).call(target);
53 | onSet?.();
54 | };
55 | }
56 | }
57 |
58 | return value instanceof Function ? value.bind(map) : value;
59 | },
60 | });
61 | };
62 |
--------------------------------------------------------------------------------
/src/proxy/adtProxy/object.proxyBuilder.ts:
--------------------------------------------------------------------------------
1 | import { deepUnproxy } from "../deepUnproxy";
2 | import { proxyValueAndSaveIt } from "../proxyValueAndSaveIt";
3 | import { BaseAdtProxyBuilderArgs } from "./adtProxyBuilder";
4 | import React from "react";
5 | import { TARGET } from "src/constant";
6 |
7 | interface ObjectProxyBuilderArgs extends BaseAdtProxyBuilderArgs {
8 | object: object;
9 | }
10 |
11 | export const objectProxyBuilder = ({
12 | object,
13 | ...restOfArgs
14 | }: ObjectProxyBuilderArgs): object => {
15 | if (React.isValidElement(object)) {
16 | return object;
17 | }
18 |
19 | const { onSet } = restOfArgs;
20 | const isFrozen = Object.isFrozen(object);
21 |
22 | return new Proxy(isFrozen ? { ...object } : object, {
23 | get(target: object, propertyKey: PropertyKey, receiver: unknown) {
24 | if (propertyKey === TARGET) {
25 | return target;
26 | }
27 |
28 | const value = proxyValueAndSaveIt(
29 | isFrozen ? object : target,
30 | propertyKey,
31 | receiver,
32 | restOfArgs
33 | );
34 | return value;
35 | },
36 |
37 | set(
38 | target: object,
39 | propertyKey: PropertyKey,
40 | value: unknown,
41 | receiver: unknown
42 | ) {
43 | const res = Reflect.set(target, propertyKey, deepUnproxy(value), receiver);
44 | onSet?.();
45 | return res;
46 | },
47 | });
48 | };
49 |
--------------------------------------------------------------------------------
/src/proxy/deepUnproxy.ts:
--------------------------------------------------------------------------------
1 | import { ObservableMetadata } from "src/decorators/observable";
2 | import { decoratorsMetadataStorage } from "src/utils/decoratorsMetadataStorage";
3 | import { getUnproxiedValue } from "src/utils/getUnProxiedValue";
4 | import { isPrimitive } from "src/utils/isPrimitive";
5 |
6 | export const deepUnproxy = (val: unknown) => {
7 | if (isPrimitive(val)) return val;
8 | const unproxied = getUnproxiedValue(val, true);
9 | // eslint-disable-next-line
10 | const valType = (val as any)?.constructor;
11 | if (Object.isFrozen(unproxied)) return unproxied;
12 |
13 | if (
14 | valType === Array ||
15 | valType === Object ||
16 | (unproxied instanceof Object &&
17 | decoratorsMetadataStorage.get("Observable", valType)[0])
18 | ) {
19 | Object.getOwnPropertyNames(unproxied).forEach((key: PropertyKey) => {
20 | unproxied[key] = deepUnproxy(unproxied[key]);
21 | });
22 | Object.getOwnPropertySymbols(unproxied).forEach((key: PropertyKey) => {
23 | unproxied[key] = deepUnproxy(unproxied[key]);
24 | });
25 | }
26 | return unproxied;
27 | };
28 |
--------------------------------------------------------------------------------
/src/proxy/proxyValueAndSaveIt.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseAdtProxyBuilderArgs,
3 | adtProxyBuilder,
4 | } from "./adtProxy/adtProxyBuilder";
5 | import { getUnproxiedValue } from "src/utils/getUnProxiedValue";
6 | import { isPrimitive } from "src/utils/isPrimitive";
7 |
8 | /**
9 | * Proxy value if need and then proxied value for next usage
10 | * - Array & Object prototype methods and properties will not proxied
11 | * - Object & Array & Function proxied values only will save
12 | */
13 | export function proxyValueAndSaveIt(
14 | target: object,
15 | propertyKey: PropertyKey,
16 | receiver: unknown,
17 | adtProxyBuilderArgs: BaseAdtProxyBuilderArgs
18 | ) {
19 | const storage = adtProxyBuilderArgs.proxiedValuesStorage;
20 | const value = Reflect.get(target, propertyKey, receiver);
21 |
22 | if (isPrimitive(value) || typeof value === "function") {
23 | return value;
24 | }
25 |
26 | if (storage.has(getUnproxiedValue(value))) {
27 | return storage.get(getUnproxiedValue(value));
28 | }
29 |
30 | const proxiedValue = () =>
31 | adtProxyBuilder({
32 | value,
33 | ...adtProxyBuilderArgs,
34 | });
35 |
36 | if (!storage.has(value)) {
37 | storage.set(value, proxiedValue());
38 | }
39 |
40 | return storage.get(value);
41 | }
42 |
--------------------------------------------------------------------------------
/src/proxy/storeForConsumerComponentProxy.ts:
--------------------------------------------------------------------------------
1 | import { StoreAdministrator } from "../store/administrator/storeAdministrator";
2 | import { PROXY_HANDLER_TYPE } from "src/constant";
3 | import { getUnproxiedValue } from "src/utils/getUnProxiedValue";
4 |
5 | export class StoreForConsumerComponentProxy implements ProxyHandler