[
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: 'CodeQL'\n\non:\n  push:\n    branches: ['master']\n  pull_request:\n    branches: ['master']\n  schedule:\n    - cron: '29 3 * * 1'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [javascript]\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v2\n        with:\n          languages: ${{ matrix.language }}\n          queries: +security-and-quality\n\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@v2\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v2\n        with:\n          category: '/language:${{ matrix.language }}'\n"
  },
  {
    "path": ".github/workflows/prerelease.yml",
    "content": "name: Pre Release\n\non:\n  release:\n    types: [prereleased]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          cache: 'pnpm'\n          node-version-file: 'package.json'\n      - run: pnpm install\n      - uses: JS-DevTools/npm-publish@v3\n        with:\n          token: ${{ secrets.NPM_TOKEN }}\n          access: public\n          tag: next\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [released]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          cache: 'pnpm'\n          node-version-file: 'package.json'\n      - run: pnpm install\n      - run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}\n      - run: npm publish\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches:\n      - '*'\n    tags-ignore:\n      - '*'\n  pull_request:\n    branches:\n\njobs:\n  formatting:\n    if: \"!contains(toJson(github.event.commits), '[skip ci]')\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: 'pnpm'\n      - run: pnpm install\n      - run: npx --no-install prettier --cache --verbose --check .\n\n  type-checking:\n    if: \"!contains(toJson(github.event.commits), '[skip ci]')\"\n    strategy:\n      matrix:\n        ts-version:\n          [5.0.x, 5.1.x, 5.2.x, 5.3.x, 5.4.x, 5.5.x, 5.6.x, 5.7.x, 5.8.x]\n        react-version: [18.x, 19.x]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - name: Use Typescript ${{ matrix.ts-version }} & React ${{ matrix.react-version }}\n        uses: actions/setup-node@v4\n        with:\n          cache: 'pnpm'\n          node-version-file: 'package.json'\n      - run: |\n          pnpm install\n          pnpm add -D \\\n            typescript@${{ matrix.ts-version }} \\\n            @types/react@${{ matrix.react-version }} \\\n            @types/react-dom@${{ matrix.react-version }}\n      - run: npx --no-install tsc --noEmit\n\n  test:\n    if: \"!contains(toJson(github.event.commits), '[skip ci]')\"\n    strategy:\n      matrix:\n        node-version: [18.x, 20.x, 22.x]\n        react-version: [18.x, 19.x]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }} & React ${{ matrix.react-version }}\n        uses: actions/setup-node@v4\n        with:\n          cache: 'pnpm'\n          node-version: ${{ matrix.node-version }}\n      - run: |\n          pnpm install\n          pnpm add -D \\\n            react@${{ matrix.react-version }} \\\n            react-dom@${{ matrix.react-version }} \\\n            react-test-renderer@${{ matrix.react-version }}\n      - run: pnpm run test\n      - name: Upload Coverage\n        uses: actions/upload-artifact@v4\n        if: github.ref == 'refs/heads/master' && strategy.job-index == 0\n        with:\n          name: coverage\n          path: coverage\n          if-no-files-found: error\n          retention-days: 1\n\n  coverage:\n    if: github.ref == 'refs/heads/master'\n    needs: test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Download Coverage\n        uses: actions/download-artifact@v4\n        with:\n          name: coverage\n          path: coverage\n      - uses: codecov/codecov-action@v5\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.DS_Store\ndist/\n*.log\nnode_modules/\ncoverage\n.nyc_output\nTODO\n/test.*\n/dist/\n/build/\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "npx --no-install commitlint --edit $1\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx --no-install prettier --cache --check .\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist/\nbuild/\ncoverage/\npnpm-lock.yaml\n_sidebar.md\n"
  },
  {
    "path": ".prettierrc.yml",
    "content": "semi: true\nsingleQuote: true\n# Change when properties in objects are quoted.\n# If at least one property in an object requires quotes, quote all properties.\nquoteProps: consistent\ntabWidth: 2\nprintWidth: 80\nendOfLine: lf\ntrailingComma: all\nbracketSpacing: true\n# Include parentheses around a sole arrow function parameter.\narrowParens: always\nproseWrap: preserve\njsxSingleQuote: false\n# Put > on the last line instead of at a new line.\nbracketSameLine: false\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"esbenp.prettier-vscode\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"typescript.preferences.quoteStyle\": \"single\",\n  \"typescript.suggest.autoImports\": true,\n  \"editor.tabSize\": 2,\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.formatOnSave\": true\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [4.0.1](https://github.com/foca-js/foca/compare/v4.0.0...v4.0.1)&nbsp;&nbsp;(2025-03-03)\n\n- 发布时未生成dist目录\n\n## [4.0.0](https://github.com/foca-js/foca/compare/v3.2.0...v4.0.0)&nbsp;&nbsp;(2025-03-03)\n\n- 升级npm包的目标语法 ES5 -> ES2020\n- 删除废弃的导出变量 `engines.localStorage` 和 `engines.sessionStorage`\n- react-native 的最低版本要求为 0.69\n- 底层包 redux 升级 4 -> 5 （目标语法为esnext）\n- 底层包 react-redux 升级 8 -> 9 （目标语法为esnext）\n\n## [3.2.0](https://github.com/foca-js/foca/compare/v3.1.1...v3.2.0)&nbsp;&nbsp;(2023-10-20)\n\n- 增加局部模型钩子 `useIsolate`\n\n## [3.1.1](https://github.com/foca-js/foca/compare/v3.1.0...v3.1.1)&nbsp;&nbsp;(2023-10-14)\n\n- computed内使用数组状态数据时，更新数组不会触发重新计算\n- 删除无用类型 `ComputedRef`\n\n## [3.1.0](https://github.com/foca-js/foca/compare/v3.0.0...v3.1.0)&nbsp;&nbsp;(2023-10-10)\n\n- 持久化引擎支持`同步`引擎。因此可以直接使用浏览器内置的 localStorage 和 sessionStorage 接口\n\n## [3.0.0](https://github.com/foca-js/foca/compare/v2.0.1...v3.0.0)&nbsp;&nbsp;(2023-10-08)\n\n### 破坏性更新\n\n- 删除hooks函数 `useDefined`\n- 删除模型内`onDestroy`事件钩子\n- 删除持久化`maxAge`配置\n\n```diff\nstore.init({\n  persist: [\n    {\n      key: 'key',\n-     maxAge: 3600,\n      engines: engines.localStorage,\n      models: [],\n    }\n  ]\n})\n```\n\n- 非hooks状态下计算属性使用执行函数的方式获取\n\n```diff\nconst model = defineModel('model', {\n  initialState: { firstName: '', lastName: '' },\n  methods: {\n    myMethod() {\n-     return this.fullName.value;\n+     return this.fullName();\n    }\n  },\n  computed: {\n   fullName() {\n     return this.state.firstName + this.state.lastName;\n   },\n  }\n});\n```\n\n### 新特性\n\n- 开启持久化的模型会立即存储initialState\n- 计算属性支持传递参数\n\n```diff\nconst model = defineModel('model', {\n  initialState: { firstName: '', lastName: '' },\n  methods: {\n    myMethod() {\n+     const profile = this.profile(30, 'addr', false);\n    }\n  },\n  computed: {\n   fullName() {\n     return this.state.firstName + this.state.lastName;\n   },\n+  profile(age: number, address: string, coding: boolean = true) {\n+    return this.fullName() + '-' + age + address + coding;\n+  },\n  }\n});\n\nconst App: FC = () => {\n  const fullName = useComputed(model.fullName);\n+ const profile = useComputed(model.profile, 20, 'my-address');\n}\n```\n\n- 持久化增加 **dump** 和 **load** 两个系列化函数\n\n```diff\nconst model = defineModel('model', {\n  initialState: { firstName: 'tick', lastName: 'tock' },\n  persist: {\n+   dump(state) {\n+     return state.firstName;\n+   },\n+   load(dumpData) {\n+     return { ...this.initialState, firstName: dumpData };\n+   },\n  }\n});\n```\n\n- 持久化新增合并模式 `replace`, `merge`<small>(默认)</small>, `deep-merge`\n\n```diff\nstore.init({\n  persist: [\n    {\n      key: 'item1',\n      version: '1.0',\n+     merge: 'replace',\n      engine: engines.localStorage,\n      models: [],\n    },\n  ]\n})\n```\n\n### 其它\n\n- npm包转译为ES5语法以兼容更早的浏览器 (#41)\n- immer版本降级：10.0.2 -> 9.0.21\n- react-redux版本升级：8.1.2 -> 8.1.3\n\n## [2.0.1](https://github.com/foca-js/foca/compare/v2.0.0...v2.0.1)&nbsp;&nbsp;(2023-08-10)\n\n- react-redux 版本从 8.1.1 升级到 8.1.2 (#40)\n\n## [2.0.0](https://github.com/foca-js/foca/compare/v1.3.1...v2.0.0)&nbsp;&nbsp;(2023-06-26)\n\n- 不再兼容 IE 浏览器\n- 最小 React 版本为 18\n- 最小 TypeScript 版本为 5.0\n- immer 版本从 9.0.21 升级到 10.0.2\n- react-redux 版本从 8.0.5 升级到 8.1.1\n- 删除废弃字段 `actions` 和 `effects`\n\n## [1.3.1](https://github.com/foca-js/foca/compare/v1.3.0...v1.3.1)&nbsp;&nbsp;(2023-04-14)\n\n- 在 onInit 事件内执行的 async method 未储存 loading 状态 (#38)\n- immer 版本从 9.0.16 升级到 9.0.21\n- redux 版本从 4.2.0 升级到 4.2.1\n- 支持 typescript@5\n\n## [1.3.0](https://github.com/foca-js/foca/compare/v1.2.1...v1.3.0)&nbsp;&nbsp;(2022-12-17)\n\n- `setState` 的回调模式支持返回不完整数据\n\n```typescript\ndefineModel('unique_name', {\n  initialState: { a: 'a', b: 'b' },\n  methods: {\n    test() {\n      this.setState(() => {\n        return { a: 'xxx' };\n      });\n      console.log(this.state); // { a: 'xxx', b: 'b' }\n    },\n  },\n});\n```\n\n- 传给 reducer 的 `initialState` 不再执行深拷贝，而是在开发环境下进行冻结处理以防开发者错误操作\n\n```typescript\nconst initialState = { a: 'a', b: 'b' };\n\ndefineModel('unique_name', {\n  initialState: initialState,\n});\n\n// 修改失败，严格模式下会报错\n// TypeError: Cannot assign to read only property 'a' of object '#<Object>'\ninitialState.a = 'xxx';\n```\n\n## [1.2.1](https://github.com/foca-js/foca/compare/v1.2.0...v1.2.1)&nbsp;&nbsp;(2022-11-11)\n\n- 销毁模型时可能触发`onChange`勾子\n\n## [1.2.0](https://github.com/foca-js/foca/compare/v1.1.0...v1.2.0)&nbsp;&nbsp;(2022-11-10)\n\n- 重命名 actions 为 reducers (#29)\n- 重命名 effects 为 methods (#29)\n- exports 导出 package.json 文件\n- 不再支持typescript@4.4\n\n## [1.1.0](https://github.com/foca-js/foca/compare/v1.0.2...v1.1.0)&nbsp;&nbsp;(2022-07-07)\n\n- setState 支持传入非完整数据\n\n```typescript\nconst initialState = { a: 1, b: 'x' };\n\nthis.setState({ a: 2, b: 'y' }); // state === { a: 2, b: 'y' }\nthis.setState({ a: 123 }); // state === { a: 123, b: 'y' }\nthis.setState({ b: 'hello' }); // state === { a: 123, b: 'hello' }\n```\n\n## [1.0.2](https://github.com/foca-js/foca/compare/v1.0.1...v1.0.2)&nbsp;&nbsp;(2022-06-30)\n\n- 修复在 react17 下 action-in-action 中间件可能报错的问题 (#20)\n\n## [1.0.1](https://github.com/foca-js/foca/compare/v1.0.0...v1.0.1)&nbsp;&nbsp;(2022-06-29)\n\n- 完善 action-in-action 的报错信息\n\n## [1.0.0](https://github.com/foca-js/foca/compare/v0.12.3...v1.0.0)&nbsp;&nbsp;(2022-06-17)\n\n### 不兼容更新\n\n- 删除已废弃函数 ~~`useDefinedModel`~~，代替函数：`useDefined`\n- 删除已废弃属性 ~~`hooks`~~，代替属性：`events`\n- 删除已废弃的方法 ~~`assign`~~ ，代替方法：`room`\n- 支持最小 React 版本为 `16.14.0`\n\n### 特性\n\n- 在开发环境下检测 `action in action` 的错误操作并抛出异常\n\n## [0.12.3](https://github.com/foca-js/foca/compare/v0.12.2...v0.12.3)&nbsp;&nbsp;(2022-06-15)\n\n- 废弃函数 `useDefinedModel`，并新增函数 `useDefined` 作为代替\n- 修复计算属性在返回 **原始数组** 或者 **原始对象** 时无法访问的问题\n- 优化 initialState 深拷贝速度\n\n## [0.12.2](https://github.com/foca-js/foca/compare/v0.12.1...v0.12.2)&nbsp;&nbsp;(2022-06-08)\n\n- initialState 现在支持传递 `undefined` 值 (#17)\n\n## [0.12.1](https://github.com/foca-js/foca/compare/v0.12.0...v0.12.1)&nbsp;&nbsp;(2022-05-27)\n\n- 开发模式下局部模型的名称携带组件名称以方便调试\n\n## [0.12.0](https://github.com/foca-js/foca/compare/v0.11.7...v0.12.0)&nbsp;&nbsp;(2022-05-26)\n\n- 增加局部模型接口 `useDefinedModel`，数据跟随组件挂载和释放\n- 提升计算属性的脏检测效率\n\n## [0.11.7](https://github.com/foca-js/foca/compare/v0.11.6...v0.11.7)&nbsp;&nbsp;(2022-05-17)\n\n- 优化 loading 写入性能\n- 修复 react 命名导出在 node ESM 环境中可能报错的风险\n- 打包不再使用 `.mjs` 后缀，设置新的 package.json 同样可以识别成 ESM\n- 不再导出`combine`方法，因为几乎用不上\n\n## [0.11.6](https://github.com/foca-js/foca/compare/v0.11.5...v0.11.6)&nbsp;&nbsp;(2022-05-13)\n\n- 使用`.js`文件以适配旧的打包工具\n\n## [0.11.5](https://github.com/foca-js/foca/compare/v0.11.3...v0.11.5)&nbsp;&nbsp;(2022-05-10)\n\n- 优化持久化逻辑\n- 使用中文提示错误和警告\n- 废弃 effects 中的 `assign` 方法，并新增 `room` 作为代替\n\n```diff\nconst testModel = defineModel('test', {\n  effects: {\n    xyz(id: number) {},\n  },\n});\n\n- testModel.xyz.assign(1).execute(1)\n+ testModel.xyz.room(1).execute(1)\n\n- useLoading(testModel.xyz.assign).find(1)\n+ useLoading(testModel.xyz.room).find(1)\n```\n\n## [0.11.3](https://github.com/foca-js/foca/compare/v0.11.2...v0.11.3)&nbsp;&nbsp;(2022-05-07)\n\n- 修复 setTimeout 类型 (#15)\n\n## [0.11.2](https://github.com/foca-js/foca/compare/v0.11.1...v0.11.2)&nbsp;&nbsp;(2022-05-06)\n\n- 提升 computed 脏检查性能\n\n## [0.11.1](https://github.com/foca-js/foca/compare/v0.11.0...v0.11.1)&nbsp;&nbsp;(2022-04-29)\n\n- 优化 computed in computed 时的缓存对比策略\n- 废弃属性 `hooks` 并推荐使用 `events` 以防止和 react-hooks 在名字上混淆。属性 `hooks` 将在 1.0.0 版本发布时删除。\n\n```diff\nexport const testModel = defineModel('test', {\n  initialState,\n- hooks: {},\n+ events: {},\n});\n```\n\n## [0.11.0](https://github.com/foca-js/foca/compare/v0.10.2...v0.11.0)&nbsp;&nbsp;(2022-04-24)\n\n- 模型新增生命周期 `onChange(prevState, nextState)` 以监听当前模型的状态变化\n- 模型新增 computed 计算属性，并新增 `useComputed` 配合使用\n\n## [0.10.2](https://github.com/foca-js/foca/compare/v0.10.0...v0.10.2)&nbsp;&nbsp;(2022-04-21)\n\n- 使用新的文件打包方案以解决在 node 环境下无法使用 ESM 的问题\n- 使用简单的 JSON.stringify 和 JSON.parse 处理初始值的深度拷贝任务\n\n## [0.10.0](https://github.com/foca-js/foca/compare/v0.9.3...v0.10.0)&nbsp;&nbsp;(2022-04-15)\n\n- 支持 react-18 并发渲染\n\n## [0.9.3](https://github.com/foca-js/foca/compare/v0.9.2...v0.9.3)&nbsp;&nbsp;(2022-04-14)\n\n- 持久化数据有可能被初始值覆盖\n- 模型名称唯一性检测\n\n## [0.9.2](https://github.com/foca-js/foca/compare/v0.9.1...v0.9.2)&nbsp;&nbsp;(2021-12-23)\n\n- 增强初始化时的 compose 类型\n- 设置 sideEffects 以适配 tree-shaking\n- 日志字符串 `redux-devtools` 现在只在非生产环境生效\n\n## [0.9.1](https://github.com/foca-js/foca/compare/v0.9.0...v0.9.1)&nbsp;&nbsp;(2021-12-20)\n\n- 在开发环境下允许多次执行`store.init()`以适应热重载\n- 持久化解析失败时一律抛出异常\n\n## [0.9.0](https://github.com/foca-js/foca/compare/v0.8.1...v0.9.0)&nbsp;&nbsp;(2021-12-17)\n\n- [Breaking] 删除 `useMeta()`, `getMeta()` 接口，移除 meta 概念\n- 修复 IDE 中 React 组件调用的模型方法无法点击跳转回模型的问题\n\n## [0.8.1](https://github.com/foca-js/foca/compare/v0.8.0...v0.8.1)&nbsp;&nbsp;(2021-12-17)\n\n- 私有方法在运行时也不该被导出\n\n## [0.8.0](https://github.com/foca-js/foca/compare/v0.7.1...v0.8.0)&nbsp;&nbsp;(2021-12-17)\n\n- 支持私有方法，在模型外部使用会触发 TS 报错（属性不存在）\n\n## [0.7.1](https://github.com/foca-js/foca/compare/v0.7.0...v0.7.1)&nbsp;&nbsp;(2021-12-13)\n\n- 通过缓存提升 useModel 的性能\n\n## [0.7.0](https://github.com/foca-js/foca/compare/v0.6.0...v0.7.0)&nbsp;&nbsp;(2021-12-12)\n\n- [Breaking] ctx.dispatch 重命名为 ctx.setState\n\n```diff\ndifineModel('name', {\n  effects: {\n    foo() {\n-     this.dispatch({ count: 1 });\n+     this.setState({ count: 1 });\n    }\n  }\n})\n```\n\n- 删除部分继承的 Error 类，直接使用原生 Error\n- 过期的持久化数据不再自动重新生成\n\n## [0.6.0](https://github.com/foca-js/foca/compare/v0.5.0...v0.6.0)&nbsp;&nbsp;(2021-12-10)\n\n- [Breaking] 删除 Map/Set 特性\n- 内置并简化深对比函数\n\n## [0.5.0](https://github.com/foca-js/foca/compare/v0.4.1...v0.5.0)&nbsp;&nbsp;(2021-12-09)\n\n- [Breaking] effect.meta() 重命名为 effect.assign()\n\n```diff\n- model.effect.meta(ID).execute(...);\n+ model.effect.assign(ID).execute(...);\n```\n\n- [Breaking] {get|use}Meta 和 {get|use}Loading 的 pick() 重命名为 find()\n\n```diff\n- useLoading(model.effect, 'pick').pick(ID)\n+ useLoading(model.effect.assign).find(ID)\n\n- useLoading(model.effect, 'pick', ID)\n+ useLoading(model.effect.assign, ID)\n```\n\n- 取消导出部分 redux 模块\n- 增加 metas 和 loadings 在开发环境下的不可变特性\n\n## [0.4.1](https://github.com/foca-js/foca/compare/v0.4.0...v0.4.1)&nbsp;&nbsp;(2021-12-08)\n\n- 修复循环引用问题\n\n## [0.4.0](https://github.com/foca-js/foca/compare/v0.3.6...v0.4.0)&nbsp;&nbsp;(2021-12-08)\n\n- [Breaking] 删除重复且难以理解的 api `useLoadings`, `useMetas`, `getLoadings`, `getMetas`\n\n## [0.3.6](https://github.com/foca-js/foca/compare/v0.3.5...v0.3.6)&nbsp;&nbsp;(2021-12-04)\n\n- 模型增加钩子函数 onInit\n- 修复 getLoadings 和 useLoadings 始终返回新对象的问题\n\n## [0.3.5](https://github.com/foca-js/foca/compare/v0.3.4...v0.3.5)&nbsp;&nbsp;(2021-12-01)\n\n- 使用 Object.assign 代替插件包 object-assign\n- 增加 combine() 函数以覆盖状态库共存时使用 connect() 高阶组件的场景\n\n## [0.3.4](https://github.com/foca-js/foca/compare/v0.3.3...v0.3.4)&nbsp;&nbsp;(2021-11-29)\n\n- 提升 useModel 在传递单个模型时的执行效率\n- useModel 没有传回调函数时，不再提供对比算法参数\n\n## [0.3.3](https://github.com/foca-js/foca/compare/v0.3.2...v0.3.3)&nbsp;&nbsp;(2021-11-28)\n\n- react 最小依赖版本现在为 16.9.0\n- 优化 dispatch 性能\n- 引入 process.env.NODE_ENV 以减少生产环境的体积\n\n## [0.3.2](https://github.com/foca-js/foca/compare/v0.3.1...v0.3.2)&nbsp;&nbsp;(2021-11-27)\n\n- 精简代码\n- 内置插件包 symbol-observable\n\n## [0.3.1](https://github.com/foca-js/foca/compare/v0.3.0...v0.3.1)&nbsp;&nbsp;(2021-11-26)\n\n- 升级 immer 版本\n- 重写 action 和 effect 增强函数\n\n## [0.3.0](https://github.com/foca-js/foca/compare/v0.2.3...v0.3.0)&nbsp;&nbsp;(2021-11-24)\n\n- [Breaking] keepStateFromRefresh 重命名为 skipRefresh\n- 修复 dispatch meta 时未命中拦截条件\n- 重构拦截器\n- 重构 reducer 生成器\n- 完善测试用例\n\n## [0.2.3](https://github.com/foca-js/foca/compare/v0.2.2...v0.2.3)&nbsp;&nbsp;(2021-11-23)\n\n- 对 action 进行拦截以避免无意义的状态更新和组件重渲染\n\n## [0.2.2](https://github.com/foca-js/foca/compare/v0.2.1...v0.2.2)&nbsp;&nbsp;(2021-11-22)\n\n- meta 数据使用新的内部 store 存储\n\n## [0.2.1](https://github.com/foca-js/foca/compare/v0.2.0...v0.2.1)&nbsp;&nbsp;(2021-11-22)\n\n- 异步函数中的`metaId()`重命名为`meta()`\n\n## [0.2.0](https://github.com/foca-js/foca/compare/v0.1.5...v0.2.0)&nbsp;&nbsp;(2021-11-21)\n\n- 增加及时状态方法：`getLoading`, `getLoadings`, `getMeta`, `getMetas`\n- 增加 hooks 方法：`useLoadings`, `useMetas`\n- meta 增加 type 字段，并由此检测 loading 状态\n\n## [0.1.5](https://github.com/foca-js/foca/compare/v0.1.4...v0.1.5)&nbsp;&nbsp;(2021-11-19)\n\n- useModel 可以手动传入对比算法，未传则由框架动态决策\n- 提升异步状态追踪性能\n- 提升数据合并性能\n\n## [0.1.4](https://github.com/foca-js/foca/compare/v0.1.3...v0.1.4)&nbsp;&nbsp;(2021-11-13)\n\n- 删除 tslib 依赖\n- 定义模型时的属性 state 重构为 initialState，防止和 actions 的 state 变量名重叠以及 eslint 规则报错。\n\n## [0.1.3](https://github.com/foca-js/foca/compare/v0.1.2...v0.1.3)&nbsp;&nbsp;(2021-11-02)\n\n- action 的返回类型更新为 AnyAction\n- 内部方法 dispatch 现支持**直接**传入完整的新 state。如果你只想更新 state 的某个值，则仍然使用回调。\n- 修改异步方法报错时 action.type 的文字\n\n## [0.1.2](https://github.com/foca-js/foca/compare/v0.1.1...v0.1.2)&nbsp;&nbsp;(2021-11-01)\n\n- 存储引擎可自定义 keyPrefix 参数\n\n## [0.1.1](https://github.com/foca-js/foca/compare/v0.1.0...v0.1.1)&nbsp;&nbsp;(2021-10-31)\n\n- 存储引擎放回当前库\n\n## [0.1.0](https://github.com/foca-js/foca/compare)&nbsp;&nbsp;(2021-10-31)\n\n- 模块化\n- 持久化\n- 支持类型提示\n- 支持 Map/Set\n- 支持 immer\n- 与其他 redux 库共存，方便迁移\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021-2023 geekact\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# FOCA\n\n流畅的 react 状态管理库，基于[redux](https://github.com/reduxjs/redux)和[react-redux](https://github.com/reduxjs/react-redux)。简洁、极致、高效。\n\n[![npm peer react version](https://img.shields.io/npm/dependency-version/foca/peer/react?logo=react)](https://github.com/facebook/react)\n[![npm peer typescript version](https://img.shields.io/npm/dependency-version/foca/peer/typescript?logo=typescript)](https://github.com/microsoft/TypeScript)\n[![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/foca-js/foca/test.yml?branch=master&label=test&logo=vitest)](https://github.com/foca-js/foca/actions)\n[![Codecov](https://img.shields.io/codecov/c/github/foca-js/foca?logo=codecov)](https://codecov.io/gh/foca-js/foca)\n[![npm](https://img.shields.io/npm/v/foca?logo=npm)](https://www.npmjs.com/package/foca)\n[![npm](https://img.shields.io/npm/dt/foca?logo=codeforces)](https://www.npmjs.com/package/foca)\n[![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/foca?label=bundle+size&cacheSeconds=3600&logo=esbuild)](https://bundlephobia.com/package/foca@latest)\n[![License](https://img.shields.io/github/license/foca-js/foca?logo=open-source-initiative)](https://github.com/foca-js/foca/blob/master/LICENSE)\n[![Code Style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier)](https://github.com/prettier/prettier)\n\n<br>\n\n![mind map](https://raw.githubusercontent.com/foca-js/foca/master/docs/mindMap.svg)\n\n# 特性\n\n- 模块化开发，导出即可使用\n- 专注 TS 极致体验，超强的类型自动推导\n- 内置 [immer](https://github.com/immerjs/immer) 响应式修改数据\n- 支持计算属性，自动收集依赖，可传参数\n- 自动管理异步函数的 loading 状态\n- 可定制的多引擎数据持久化\n- 支持局部模型，用完即扔\n- 支持私有方法\n\n# 使用环境\n\n- Browser\n- React Native\n- Taro\n- Electron\n\n# 安装\n\n```bash\n# npm\nnpm install foca\n# yarn\nyarn add foca\n# pnpm\npnpm add foca\n```\n\n# 初始化\n\n```typescript\nimport { store } from 'foca';\n\nstore.init();\n```\n\n# 创建模型\n\n### reducers 修改数据\n\n```typescript\nimport { defineModel } from 'foca';\n\nconst initialState: { count: number } = { count: 0 };\n\nexport const counterModel = defineModel('counter', {\n  initialState,\n  reducers: {\n    // 支持无限参数\n    plus(state, value: number, times: number = 1) {\n      state.count += value * times;\n    },\n    minus(state, value: number) {\n      return { count: state.count - value };\n    },\n  },\n});\n```\n\n### computed 计算属性\n\n```typescript\nexport const counterModel = defineModel('counter', {\n  initialState,\n  // 自动收集依赖\n  computed: {\n    filled() {\n      return Array(this.state.count)\n        .fill('')\n        .map((_, index) => index)\n        .map((item) => item * 2);\n    },\n  },\n});\n```\n\n### methods 组合逻辑\n\n```typescript\nexport const counterModel = defineModel('counter', {\n  initialState,\n  reducers: {\n    increment(state) {\n      state.count += 1;\n    },\n  },\n  methods: {\n    async incrementAsync() {\n      await this._sleep(100);\n\n      this.increment();\n      // 也可直接修改状态而不通过reducers，仅在内部使用\n      this.setState({ count: this.state.count + 1 });\n      this.setState((state) => {\n        state.count += 1;\n      });\n\n      return 'OK';\n    },\n    // 私有方法，外部使用时不会提示该方法\n    _sleep(duration: number) {\n      return new Promise((resolve) => {\n        setTimeout(resolve, duration);\n      });\n    },\n  },\n});\n```\n\n### events 事件回调\n\n```typescript\nexport const counterModel = defineModel('counter', {\n  initialState,\n  events: {\n    // 模型初始化\n    onInit() {\n      console.log(this.state);\n    },\n    // 模型数据变更\n    onChange(prevState, nextState) {},\n  },\n});\n```\n\n# 使用\n\n### 在 function 组件中使用\n\n```tsx\nimport { FC, useEffect } from 'react';\nimport { useModel, useLoading } from 'foca';\nimport { counterModel } from './counterModel';\n\nconst App: FC = () => {\n  const count = useModel(counterModel, (state) => state.count);\n  const loading = useLoading(counterModel.incrementAsync);\n\n  useEffect(() => {\n    counterModel.incrementAsync();\n  }, []);\n\n  return (\n    <div onClick={() => counterModel.plus(1)}>\n      {count} {loading ? 'Loading...' : null}\n    </div>\n  );\n};\n\nexport default App;\n```\n\n### 在 class 组件中使用\n\n```tsx\nimport { Component } from 'react';\nimport { connect, getLoading } from 'foca';\nimport { counterModel } from './counterModel';\n\ntype Props = ReturnType<typeof mapStateToProps>;\n\nclass App extends Component<Props> {\n  componentDidMount() {\n    counterModel.incrementAsync();\n  }\n\n  render() {\n    const { count, loading } = this.props;\n\n    return (\n      <div onClick={() => counterModel.plus(1)}>\n        {count} {loading ? 'Loading...' : null}\n      </div>\n    );\n  }\n}\n\nconst mapStateToProps = () => {\n  return {\n    count: counterModel.state.count,\n    loading: getLoading(counterModel.incrementAsync),\n  };\n};\n\nexport default connect(mapStateToProps)(App);\n```\n\n# 文档\n\nhttps://foca.js.org\n\n# 例子\n\n沙盒在线试玩：https://codesandbox.io/s/foca-demos-e8rh3\n<br />\nReact 案例仓库：https://github.com/foca-js/foca-demo-web\n<br>\nRN 案例仓库：https://github.com/foca-js/foca-demo-react-native\n<br>\nTaro 案例仓库：https://github.com/foca-js/foca-demo-taro\n\n# 生态\n\n#### 网络请求\n\n| 仓库                                                    | 版本                                                                                            | 描述                          |\n| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |\n| [axios](https://github.com/axios/axios)                 | [![npm](https://img.shields.io/npm/v/axios)](https://www.npmjs.com/package/axios)               | 当下最流行的请求库            |\n| [foca-axios](https://github.com/foca-js/foca-axios)     | [![npm](https://img.shields.io/npm/v/foca-axios)](https://www.npmjs.com/package/foca-axios)     | axios++ 支持 节流、缓存、重试 |\n| [foca-openapi](https://github.com/foca-js/foca-openapi) | [![npm](https://img.shields.io/npm/v/foca-openapi)](https://www.npmjs.com/package/foca-openapi) | 使用openapi文档生成请求服务   |\n\n#### 持久化存储引擎\n\n| 仓库                                                                                      | 版本                                                                                                                                                      | 描述                                       | 平台     |\n| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- |\n| [react-native-async-storage](https://github.com/react-native-async-storage/async-storage) | [![npm](https://img.shields.io/npm/v/@react-native-async-storage/async-storage)](https://www.npmjs.com/package/@react-native-async-storage/async-storage) | React-Native 持久化引擎                    | RN       |\n| [foca-taro-storage](https://github.com/foca-js/foca-taro-storage)                         | [![npm](https://img.shields.io/npm/v/foca-taro-storage)](https://www.npmjs.com/package/foca-taro-storage)                                                 | Taro 持久化引擎                            | Taro     |\n| [localforage](https://github.com/localForage/localForage)                                 | [![npm](https://img.shields.io/npm/v/localforage)](https://www.npmjs.com/package/localforage)                                                             | 浏览器端持久化引擎，支持 IndexedDB, WebSQL | Web      |\n| [foca-electron-storage](https://github.com/foca-js/foca-electron-storage)                 | [![npm](https://img.shields.io/npm/v/foca-electron-storage)](https://www.npmjs.com/package/foca-electron-storage)                                         | Electron 持久化引擎                        | Electron |\n| [foca-mmkv-storage](https://github.com/foca-js/foca-mmkv-storage)                         | [![npm](https://img.shields.io/npm/v/foca-mmkv-storage)](https://www.npmjs.com/package/foca-mmkv-storage)                                                 | 基于 mmkv 的持久化引擎                     | RN       |\n| [foca-cookie-storage](https://github.com/foca-js/foca-cookie-storage)                     | [![npm](https://img.shields.io/npm/v/foca-cookie-storage)](https://www.npmjs.com/package/foca-cookie-storage)                                             | Cookie 持久化引擎                          | Web      |\n\n#### 日志\n\n| 仓库                                                                       | 版本                                                                                                                      | 描述           | 平台          |\n| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- |\n| [@redux-devtools/extension](https://github.com/reduxjs/redux-devtools)     | [![npm](https://img.shields.io/npm/v/@redux-devtools/extension)](https://www.npmjs.com/package/@redux-devtools/extension) | 浏览器日志插件 | Web, RN       |\n| [react-native-debugger](https://github.com/jhen0409/react-native-debugger) | [![npm](https://img.shields.io/npm/v/react-native-debugger)](https://www.npmjs.com/package/react-native-debugger)         | 日志应用程序   | RN            |\n| [redux-logger](https://github.com/LogRocket/redux-logger)                  | [![npm](https://img.shields.io/npm/v/redux-logger)](https://www.npmjs.com/package/redux-logger)                           | 控制台输出日志 | Web, RN, Taro |\n\n# 常见疑问\n\n### 函数里 this 的类型是 any\n\n答：需要在文件 **tsconfig.json** 中开启`\"strict\": true`或者`\"noImplicitThis\": true`\n\n---\n\n更多答案请[查看文档](https://foca.js.org/#/troubleshooting)\n\n# 捐赠\n\n开源不易，升级维护框架和解决各种 issue 需要十分多的精力和时间。希望能得到你的支持，让项目处于良性发展的状态。捐赠地址：[二维码](https://foca.js.org#donate)\n\n<table>\n <tr>\n  <td align=\"center\">\n    <a target=\"_blank\" href=\"https://github.com/arcsin1?donate_money=16.66\">\n      <img src=\"https://avatars.githubusercontent.com/u/13724222\" width=80 height=80 style=\"border-radius: 100%;\" />\n    </a><br>\n    <a target=\"_blank\" href=\"https://github.com/arcsin1?donate_money=16.66\">arcsin1</a>\n  </td>\n  <td align=\"center\">\n    <a target=\"_blank\" href=\"https://github.com/xiongxliu?donate_money=50\">\n      <img src=\"https://avatars.githubusercontent.com/u/17661296\" width=80 height=80 style=\"border-radius: 100%;\" />\n    </a><br>\n    <a target=\"_blank\" href=\"https://github.com/xiongxliu?donate_money=50\">xiongxliu</a>\n  </td>\n </tr>\n</table>\n"
  },
  {
    "path": "docs/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/CNAME",
    "content": "foca.js.org"
  },
  {
    "path": "docs/_sidebar.md",
    "content": "* [关于Foca](/)\n\n- [更新日志](/changelog.md)\n\n* [开始使用](/initialize.md)\n\n- [模型](/model.md)\n\n* [接口](/api.md)\n\n- [事件钩子](/events.md)\n\n* [持久化](/persist.md)\n\n- [进阶用法](/advanced.md)\n\n* [问题解答](/troubleshooting.md)\n\n- [编写测试](/test.md)\n\n* [与Redux-Toolkit对比](/redux-toolkit.md)\n\n- [捐赠](/donate.md)\n"
  },
  {
    "path": "docs/advanced.md",
    "content": "# <!-- {docsify-ignore} -->\n\n# 克隆模型\n\n虽然比较不常用，但有时候为了同一个页面的不同模块能独立使用模型数据，你就得需要复制这个模型，并把名字改掉。其实也不用这么麻烦，foca 给你来个惊喜：\n\n```typescript\nimport { defineModel, cloneModel } from 'foca';\n\n// 你打算用在各个普通页面里。\ncosnt userModel = defineModel('users', { ... });\n\n// 你打算用在通用的用户列表弹窗里。\nconst user1Model = cloneModel('users1', userModel);\n// 你打算用在页头或页脚模块里。\nconst user2Model = cloneModel('users2', userModel);\n```\n\n共享方法但状态是独立的，这是个不错的主意，你只要维护一份代码就行了。\n\n克隆时支持修改 `initialState, events, persist, skipRefresh` 这些属性\n\n```typescript\nconst user3Model = cloneModel('users3', userModel, {\n  initialState: {...},\n  ...\n});\n\nconst user3Model = cloneModel('users3', userModel, (prev) => {\n  return {\n    initialState: {\n      ...prev.initialState,\n      customData: 'xyz',\n    },\n    ...\n  }\n});\n```\n\n# 局部模型\n\n通过`defineModel`和`cloneModel`创建的模型均为全局类别的模型，数据一直保持在内存中，直到应用关闭或者退出才会释放，对于比较大的项目，这可能会有性能问题。所以有时候你其实想要一种`用完就扔`的模型，即在 React 组件初始化时把模型数据扔到 store 中，当 React 组件被销毁时，模型的数据也跟着销毁。现在，局部模型很适合你的需求：\n\n```diff\nimport { useEffect } from 'react';\nimport { defineModel, useIsolate } from 'foca';\n\n// test.model.ts\nexport const testModel = defineModel('test', {\n  initialState: { count: 0 },\n  reducers: {\n    plus(state, value: number) {\n      state.count += value;\n    },\n  },\n});\n\n// App.tsx\nconst App: FC = () => {\n+ const model = useIsolate(testModel);\n  const { count } = useModel(model);\n\n  useEffect(() => {\n    model.plus(1);\n  }, []);\n\n  return <div>{count}</div>;\n};\n```\n\n只需增加一行代码的工作量，利用 `useIsolate` 函数根据全局模型创建一个新的局部模型。局部模型拥有一份独立的状态数据，任何操作都不会影响到原来的全局模型，而且会随着组件一起 `挂载/销毁`，能有效降低内存占用。\n\n另外，别忘了模型上还有两个对应的事件`onInit`和`onDestroy`可以使用\n\n```typescript\nexport const testModel = defineModel('test', {\n  initialState: { count: 0 },\n  events: {\n    onInit() {\n      // 全局模型创建时触发\n      // 局部模型随组件一起挂载时触发\n    },\n    onDestroy() {\n      // 局部模型随组件一起销毁时触发\n    },\n  },\n});\n```\n\n!> 如果不需要持久化，那么它可以完全代替克隆模型\n\n# loadings\n\n默认地，methods 函数只会保存一份执行状态，如果你在同一时间多次执行同一个函数，那么状态就会互相覆盖，产生错乱的数据。如果现在有 10 个按钮，点击每个按钮都会执行`model.methodX(id)`，那么我们如何知道是哪个按钮执行的呢？这时候我们需要为执行状态开辟一个独立的存储空间，让同一个函数拥有多个状态互不干扰。\n\n```tsx\nimport { useLoading } from 'foca';\n\nconst App: FC = () => {\n  const loadings = useLoading(model.myMethod.room);\n\n  const handleClick = (id: number) => {\n    model.myMethod.room(id).execute(id);\n  };\n\n  return (\n    <div>\n      <button onClick={() => handleClick(1)}>\n        A {loadings.find(1) ? 'Loading...' : ''}\n      </button>\n      <button onClick={() => handleClick(2)}>\n        B {loadings.find(2) ? 'Loading...' : ''}\n      </button>\n      <button onClick={() => handleClick(3)}>\n        C {loadings.find(3) ? 'Loading...' : ''}\n      </button>\n    </div>\n  );\n};\n```\n\n这种场景也常出现在一些表格里，每一行通常都带有切换（switch UI）控件，点击后该控件需要被禁用或者出现 loading 图标，提前是你得知道是谁。\n\n如果你能确定 find 的参数，那么也可以直接传递：\n\n```typescript\n// 适用于明确地知道编号的场景，比如是从组件props直接传入\nconst loading = useLoading(model.myMethod.room, 100); // boolean\n\n// 适用于列表，编号只能在for循环中获取的场景\nconst loadings = useLoading(model.myMethod.room);\nlist.forEach(({ id }) => {\n  const loading = loadings.find(id);\n});\n```\n\n# 重置所有数据\n\n当用户退出登录时，你需要清理与用户相关的一些数据，然后把页面切换到`登录页`。清理操作其实是比较麻烦的，首先 model 太多了，然后就是后期也可能再增加其它模型，不可能手动一个个清理。这时候可以用上 store 自带的方法：\n\n```diff\nimport { store } from 'foca';\n\n// onLogout是你的业务方法\nonLogout().then(() => {\n+ store.refresh();\n});\n```\n\n一个方法就能把所有数据都恢复成初始值状态，太方便了吧？\n\n重置时，你也可以保留部分模型的数据不被影响（可能是一些全局的配置数据），在相应的模型下加入关键词`skipRefresh`即可：\n\n```diff\ndefineModel('my-global-model', {\n  initialState: {},\n+ skipRefresh: true,\n});\n```\n\n对了，如果你实在是想无情地删除所有数据（即无视 skipRefresh 参数），那么就用`强制模式`好了：\n\n```typescript\nstore.refresh(true);\n```\n\n# 私有方法\n\n我们总是会想抽出一些逻辑作为独立的方法调用，但又不想暴露给模型外部使用，而且方法一多，调用方法时 TS 会提示长长的一串方法列表，显得十分混乱。是时候声明一些私有方法了，foca 使用约定俗成的`前置下划线(_)`来代表私有方法\n\n```typescript\nconst userModel = defineModel('users', {\n  initialState,\n  reducers: {\n    addUser(state, user: UserItem) {},\n    _deleteUser(state, userId: number) {},\n  },\n  methods: {\n    async retrieve(id: number) {\n      const user = await http.get<UserItem>(`/users/${id}`);\n      this.addUser(user);\n\n      // 私有reducers方法\n      this._deleteUser(15);\n      // 私有methods方法\n      this._myLogic();\n      // 私有computed变量\n      this._fullname.value;\n    },\n    async _myLogic() {},\n  },\n  computed: {\n    _fullname() {},\n  },\n});\n\nuserModel.retrieve; // OK\nuserModel._deleteUser; // 报错了，找不到属性 _deleteUser\nuserModel._myLogic; // 报错了，找不到属性 _myLogic\nuserModel._fullname; // 报错了，找不到属性 _fullname\n```\n\n对外接口变得十分清爽，减少出错概率的同时，也提升了数据的安全性。\n"
  },
  {
    "path": "docs/api.md",
    "content": "# <!-- {docsify-ignore} -->\n\n# useModel\n\n使用频率：:star2::star2::star2::star2::star2:\n\n你绝对想不到在 React 组件中获取一个模型的数据有多简单，试试：\n\n```tsx\n// File: App.tsx\nimport { FC } from 'react';\nimport { useModel } from 'foca';\nimport { userModel } from './userModel';\n\nconst App: FC = () => {\n  const users = useModel(userModel);\n\n  return (\n    <>\n      {users.map((user) => (\n        <div key={user.id}>{user.name}</div>\n      ))}\n    </>\n  );\n};\n\nexport default App;\n```\n\n就这么一小行，朴实无华，你也可以对数据进行改造，返回你当前需要的数据：\n\n```typescript\nconst userIds = useModel(userModel, (state) => state.map((item) => item.id));\n```\n\n不错，你拿到了所有用户的 id 编号，同时 userIds 的类型会自动推断为`number[]`。\n\n!> 只要 useModel() 返回的最终值不变，就不会触发 react 组件刷新。foca 使用 **深对比** 来判断值是否变化。\n\n---\n\n等等，事情还没结束！useModel 还能增加更多模型上去，这样可以少用几个 hooks：\n\n```typescript\nconst { users, agents, teachers } = useModel(\n  userModel,\n  agentModel,\n  teacherModel,\n);\n```\n\n传递超过一个模型作为参数时，返回值将变成对象，而 key 就是模型的名称，value 就是模型的 state。这很酷，而且你仍然不用担心类型的问题。\n\n如果你有一些数据需要多个模型才能计算出来，那么现在就是 useModel 大展身手的时候了：\n\n```typescript\nconst count = useModel(\n  userModel,\n  agentModel,\n  teacherModel,\n  (users, agents, teachers) => users.length + agents.length + teachers.length,\n);\n```\n\n返回的是一个数字，如假包换，TS 也自动推导出来了这是`number`类型\n\n# useLoading\n\n使用频率：:star2::star2::star2::star2::star2:\n\nmethods 函数大部分是异步的，你可能正在函数里执行一个请求 api 的操作。在用户等待期间，你需要为用户渲染`loading...`之类的字样或者图标以缓解用户的焦虑心情。利用 foca 提供的逻辑，你可以轻松地知道某个函数是否正在执行：\n\n```tsx\nimport { useLoading } from 'foca';\n\nconst App: FC = () => {\n  const loading = useLoading(userModel.getUser);\n\n  const handleClick = () => {\n    userModel.getUser(1);\n  };\n\n  return <div onClick={handleClick}>{loading ? 'loading...' : 'OK'}</div>;\n};\n```\n\n每次开始执行`getUser`函数，loading 自动变成`true`，从而触发组件刷新。\n\n在某些编辑表单的场景，很可能会同时有新增和修改两种操作。对于 restful API，你需要写两个异步函数来处理请求。但你的表单保存按钮只有一个，很显然不管是新增还是修改，你都想让保存按钮渲染`保存中...`的字样。\n\n为了减少业务代码，useLoading 允许你传入多个异步函数，只要有任何一个函数在执行，那么最终值就会是 true。\n\n```typescript\nconst loading = useLoading(userModel.create, userModel.update, ...);\n```\n\n# useComputed\n\n使用频率：:star2::star2::star2:\n\n配合 computed 计算属性使用。携带参数的情况下，则从第二个参数开始依次传入\n\n```tsx\nimport { useComputed } from 'foca';\n\n// 假设有这么一个model\nconst userModel = defineModel('user', {\n  initialState: {\n    firstName: 'tick',\n    lastName: 'tock',\n  },\n  computed: {\n    fullName() {\n      return this.state.firstName + '.' + this.state.lastName;\n    },\n    profile(age: number) {\n      return this.fullName() + '-' + age;\n    },\n  },\n});\n\nconst App: FC = () => {\n  // 只有当 firstName 或者 lastName 变化，才会重新刷新该组件\n  const fullName = useComputed(userModel.fullName);\n  // 这里会有TS提示你应该传几个参数，以及参数类型\n  const profile = useComputed(userModel.profile, 24);\n\n  return <div>{fullName}</div>;\n};\n```\n\n# getLoading\n\n使用频率：:star2:\n\n如果想实时获取异步函数的执行状态，则可以通过 `getLoading(...)` 的方式获取。它与 **useLoading** 的唯一区别就是是否hooks。\n\n```typescript\nconst loading = getLoading(userModel.create);\n```\n\n# connect\n\n使用频率：:star2:\n\n如果你写烦了函数式组件，偶尔想写一下 class 组件，那么 foca 已经为你准备好了`connect()`函数。如果不知道这是什么，可以参考[react-redux](https://github.com/reduxjs/react-redux)的文档。事实上，我们内置了这个库并对其做了一些封装。\n\n```typescript\nimport { PureComponent } from 'react';\nimport { connect } from 'foca';\nimport { userModel } from './userModel';\n\ntype Props = ReturnType<typeof mapStateToProps>;\n\nclass App extends PureComponent<Props> {\n  render() {\n    const { users, loading } = this.props;\n\n    if (loading) {\n      return <p>Loading...</p>;\n    }\n\n    return <p>Hello, {users.length} people</p>;\n  }\n}\n\nconst mapStateToProps = () => {\n  return {\n    users: userModel.state,\n    loading: getLoading(userModel.fetchUser),\n  };\n};\n\nexport default connect(mapStateToProps)(App);\n```\n\n没有了 hooks 的帮忙，我们只能从模型或者方法上获取实时的数据。但只要你是在 mapStateToProps 中获取的数据，foca 就会自动为你更新并注入到组件里。\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "# 更新日志 <!-- {docsify-ignore-all} -->\n\n[changelog](https://raw.githubusercontent.com/foca-js/foca/master/CHANGELOG.md ':include')\n"
  },
  {
    "path": "docs/donate.md",
    "content": "# 捐赠 <!-- {docsify-ignore} -->\n\n开源不易，升级维护框架和解决各种 issue 需要十分多的精力和时间。希望能得到你的支持，让项目处于良性发展的状态。\n\n> 捐赠时请备注你的 github 账号，对于每一个捐赠者，我都会放到 README.md 以及当前页表示感谢。\n\n### 二维码\n\n<center>\n<img src=\"./alipay.png\" width=\"40%\" />\n<span style=\"display:inline-block; width: 8%\"></span>\n<img src=\"./wepay.png\" width=\"40%\" />\n</center>\n\n### 鸣谢\n\n<!-- 匿名捐赠共 `10` 元 -->\n\n<table>\n <tr>\n  <td align=\"center\">\n    <a target=\"_blank\" href=\"https://github.com/arcsin1?donate_money=16.66\">\n      <img src=\"https://avatars.githubusercontent.com/u/13724222\" width=80 height=80 style=\"border-radius: 100%;\" />\n    </a><br>\n    <a target=\"_blank\" href=\"https://github.com/arcsin1?donate_money=16.66\">arcsin1</a>\n  </td>\n  <td align=\"center\">\n    <a target=\"_blank\" href=\"https://github.com/xiongxliu?donate_money=50\">\n      <img src=\"https://avatars.githubusercontent.com/u/17661296\" width=80 height=80 style=\"border-radius: 100%;\" />\n    </a><br>\n    <a target=\"_blank\" href=\"https://github.com/xiongxliu?donate_money=50\">xiongxliu</a>\n  </td>\n </tr>\n</table>\n"
  },
  {
    "path": "docs/events.md",
    "content": "每个模型都有针对自身的事件回调，在某些复杂的业务场景下，事件和其它属性的组合将变得十分灵活。\n\n## onInit\n\n当 store 初始化完成 并且持久化（如果有）数据已经恢复时，onInit 就会被自动触发。你可以调用 methods 或者 reducers 做一些额外操作。\n\n```typescript\nimport { defineModel } from 'foca';\n\n// 如果是持久化的模型，则初始值不一定是0\nconst initialState = { count: 0 };\n\nexport const myModel = defineModel('my', {\n  initialState,\n  reducers: {\n    add(state, step: number) {\n      state.count += step;\n    },\n  },\n  methods: {\n    async requestApi() {\n      const result = await http.get('/path/to');\n      // ...\n    },\n  },\n  events: {\n    onInit() {\n      this.add(10);\n      this.requestApi();\n    },\n  },\n});\n```\n\n## onChange\n\n每当 state 有变化时的回调通知。初始化(onInit)执行之前不会触发该回调。如果在 onInit 中做了修改 state 的操作，则会触发该回调。\n\n```typescript\nimport { defineModel } from 'foca';\n\nconst initialState = { count: 0 };\n\nexport const testModel = defineModel('test', {\n  initialState,\n  reducers: {\n    add(state, step: number) {\n      state.count += step;\n    },\n  },\n  methods: {\n    _notify() {\n      // do something\n    },\n  },\n  events: {\n    onChange(prevState, nextState) {\n      if (prevState.count !== nextState.count) {\n        // 达到watch的效果\n        this._notify();\n      }\n    },\n  },\n});\n```\n\n## onDestroy\n\n模型数据从 store 卸载时的回调通知。onDestroy 事件只针对[局部模型](/advanced?id=局部模型)，即通过`useIsolate`这个 hooks api 创建的模型才会触发，因为局部模型是跟随组件一起创建和销毁的。\n\n注意，当触发 onDestroy 回调时，模型已经被卸载了，所以无法再拿到当前数据，而且`this`上下文也被限制使用了。\n\n```typescript\nimport { defineModel } from 'foca';\n\nexport const testModel = defineModel('test', {\n  initialState: { count: 0 },\n  events: {\n    onDestroy(modelName) {\n      console.log('Destroyed', modelName);\n    },\n  },\n});\n```\n"
  },
  {
    "path": "docs/home.md",
    "content": "# FOCA\n\n流畅的 react 状态管理库，基于[redux](https://github.com/reduxjs/redux)和[react-redux](https://github.com/reduxjs/react-redux)。简洁、极致、高效。\n\n[![npm peer react version](https://img.shields.io/npm/dependency-version/foca/peer/react?logo=react)](https://github.com/facebook/react)\n[![npm peer typescript version](https://img.shields.io/npm/dependency-version/foca/peer/typescript?logo=typescript)](https://github.com/microsoft/TypeScript)\n[![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/foca-js/foca/test.yml?branch=master&label=test&logo=vitest)](https://github.com/foca-js/foca/actions)\n[![Codecov](https://img.shields.io/codecov/c/github/foca-js/foca?logo=codecov)](https://codecov.io/gh/foca-js/foca)\n[![npm](https://img.shields.io/npm/v/foca?logo=npm)](https://www.npmjs.com/package/foca)\n[![npm](https://img.shields.io/npm/dt/foca?logo=codeforces)](https://www.npmjs.com/package/foca)\n[![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/foca?label=bundle+size&cacheSeconds=3600&logo=esbuild)](https://bundlephobia.com/package/foca@latest)\n[![License](https://img.shields.io/github/license/foca-js/foca?logo=open-source-initiative)](https://github.com/foca-js/foca/blob/master/LICENSE)\n[![Code Style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier)](https://github.com/prettier/prettier)\n\n<br>\n\n![mind map](./mindMap.svg)\n\n# 使用环境\n\n- Browser\n- React Native\n- Taro\n- Electron\n\n# 特性\n\n#### 模块化开发，导出即可使用\n\n一个模型包含了状态操作的所有方法，基础代码全部剥离，不再像原生 redux 一般拆分成 action/type/reducer 三个文件，既不利于管理，又难以在提示上关联起来。\n\n定义完模型，导出即可在组件中使用，不用再怕忘记注册或者嫌麻烦了。\n\n#### 专注 TS 极致体验，超强的类型自动推导\n\n无 TS 不编程，foca 提供 **100%** 的基础类型提示，强大的自动推导能力让你产生一种提示上瘾的快感，而你只需关注业务中的类型。\n\n#### 内置 [immer](https://github.com/immerjs/immer) 响应式修改数据\n\n可以说加入 immer 是非常有必要的，当 reducer 数据多层嵌套时，你不必再忍受更改里层的数据而不断使用 rest/spread(...)扩展符的烦恼，相反地，直接赋值就好了，其他的交给 immer 搞定。\n\n#### 支持计算属性，自动收集依赖，可传参数\n\n现在，redux 家族不需要再羡慕`vue`或者`mobx`等响应式框架，咱也能支持计算属性并且自动收集依赖，而且是时候把[reselect](https://github.com/reduxjs/reselect)**扔进垃圾桶**了。\n\n#### 自动管理异步函数的 loading 状态\n\n我们总是想知道某个异步方法（或者请求）正在执行，然后在页面上渲染出`loading...`字样，幸运地是框架自动（按需）为你记录了执行状态。\n\n#### 可定制的多引擎数据持久化\n\n某些数据在一个时间段内可能是不变的，比如登录凭证 token。所以你想着先把数据存到本地，下次自动恢复到模型中，这样用户就不需要频繁登录了。\n\n#### 支持局部模型，用完即扔\n\n利用全局模型派生出局部模型，并跟随组件`挂载/卸载`，状态与外界隔离，组件卸载后状态自动删除，严格控制内存使用量\n\n#### 支持私有方法\n\n一个前置下划线(`_`)就能让方法变成私有的，外部使用时 TS 不会提示私有方法和私有变量，简单好记又省心。\n\n# 生态\n\n#### 网络请求\n\n| 仓库                                                    | 版本                                                                                            | 描述                          |\n| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |\n| [axios](https://github.com/axios/axios)                 | [![npm](https://img.shields.io/npm/v/axios)](https://www.npmjs.com/package/axios)               | 当下最流行的请求库            |\n| [foca-axios](https://github.com/foca-js/foca-axios)     | [![npm](https://img.shields.io/npm/v/foca-axios)](https://www.npmjs.com/package/foca-axios)     | axios++ 支持 节流、缓存、重试 |\n| [foca-openapi](https://github.com/foca-js/foca-openapi) | [![npm](https://img.shields.io/npm/v/foca-openapi)](https://www.npmjs.com/package/foca-openapi) | 使用openapi文档生成请求服务   |\n\n#### 持久化存储引擎\n\n| 仓库                                                                                      | 版本                                                                                                                                                      | 描述                                       | 平台     |\n| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- |\n| [react-native-async-storage](https://github.com/react-native-async-storage/async-storage) | [![npm](https://img.shields.io/npm/v/@react-native-async-storage/async-storage)](https://www.npmjs.com/package/@react-native-async-storage/async-storage) | React-Native 持久化引擎                    | RN       |\n| [foca-taro-storage](https://github.com/foca-js/foca-taro-storage)                         | [![npm](https://img.shields.io/npm/v/foca-taro-storage)](https://www.npmjs.com/package/foca-taro-storage)                                                 | Taro 持久化引擎                            | Taro     |\n| [localforage](https://github.com/localForage/localForage)                                 | [![npm](https://img.shields.io/npm/v/localforage)](https://www.npmjs.com/package/localforage)                                                             | 浏览器端持久化引擎，支持 IndexedDB, WebSQL | Web      |\n| [foca-electron-storage](https://github.com/foca-js/foca-electron-storage)                 | [![npm](https://img.shields.io/npm/v/foca-electron-storage)](https://www.npmjs.com/package/foca-electron-storage)                                         | Electron 持久化引擎                        | Electron |\n| [foca-mmkv-storage](https://github.com/foca-js/foca-mmkv-storage)                         | [![npm](https://img.shields.io/npm/v/foca-mmkv-storage)](https://www.npmjs.com/package/foca-mmkv-storage)                                                 | 基于 mmkv 的持久化引擎                     | RN       |\n| [foca-cookie-storage](https://github.com/foca-js/foca-cookie-storage)                     | [![npm](https://img.shields.io/npm/v/foca-cookie-storage)](https://www.npmjs.com/package/foca-cookie-storage)                                             | Cookie 持久化引擎                          | Web      |\n\n| 仓库                                                                       | 版本                                                                                                                      | 描述           | 平台          |\n| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- |\n| [@redux-devtools/extension](https://github.com/reduxjs/redux-devtools)     | [![npm](https://img.shields.io/npm/v/@redux-devtools/extension)](https://www.npmjs.com/package/@redux-devtools/extension) | 浏览器日志插件 | Web, RN       |\n| [react-native-debugger](https://github.com/jhen0409/react-native-debugger) | [![npm](https://img.shields.io/npm/v/react-native-debugger)](https://www.npmjs.com/package/react-native-debugger)         | 日志应用程序   | RN            |\n| [redux-logger](https://github.com/LogRocket/redux-logger)                  | [![npm](https://img.shields.io/npm/v/redux-logger)](https://www.npmjs.com/package/redux-logger)                           | 控制台输出日志 | Web, RN, Taro |\n\n# 缺陷\n\n- [不支持 SSR](/troubleshooting?id=为什么不支持-ssr)\n\n# 例子\n\nReact 案例仓库：https://github.com/foca-js/foca-demo-web\n<br>\nRN 案例仓库：https://github.com/foca-js/foca-demo-react-native\n<br>\nTaro 案例仓库：https://github.com/foca-js/foca-demo-taro\n<br>\n\n# 在线试玩\n\n<iframe src=\"https://codesandbox.io/embed/foca-demos-e8rh3?fontsize=14&hidenavigation=1&theme=dark&view=preview\"\n     style=\"width:100%; height:600px; border:0; border-radius: 4px; overflow:hidden;\"\n     title=\"foca-demos\"\n     allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n     sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n   ></iframe>\n"
  },
  {
    "path": "docs/index.html",
    "content": "<!doctype html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Foca</title>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n    <meta name=\"description\" content=\"现代化react状态管理库\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0\"\n    />\n    <link rel=\"shortcut icon\" href=\"/favicon.ico\" type=\"image/x-icon\" />\n    <link\n      rel=\"stylesheet\"\n      href=\"//fastly.jsdelivr.net/npm/docsify@4/lib/themes/vue.css\"\n    />\n\n    <style type=\"text/css\">\n      h1[id='']:first-of-type {\n        display: none;\n      }\n\n      span.token.deleted {\n        color: #ffdedb;\n        background-color: #cc1421;\n        text-decoration: none;\n        padding-bottom: 2px;\n      }\n\n      span.token.inserted {\n        color: #acf7b6;\n        background-color: #007728;\n        border-bottom: 0;\n        padding-bottom: 2px;\n      }\n\n      img.emoji {\n        vertical-align: text-top;\n      }\n\n      .sidebar {\n        width: 260px;\n      }\n\n      section.content {\n        left: 260px;\n      }\n\n      .markdown-section {\n        max-width: 100%;\n        width: 100%;\n        box-sizing: border-box;\n      }\n\n      .markdown-section tr {\n        background: transparent !important;\n      }\n\n      .markdown-section table pre {\n        padding: 0 0.5em;\n      }\n\n      .markdown-section table pre > code {\n        padding: 1.5em 0;\n      }\n\n      .markdown-section table thead {\n        background-color: #eee;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"app\">加载中...</div>\n    <script>\n      window.$docsify = {\n        name: 'Foca',\n        repo: 'https://github.com/foca-js/foca',\n        loadSidebar: true,\n        subMaxLevel: 2,\n        auto2top: true,\n        homepage: 'home.md',\n        autoHeader: true,\n        topMargin: 30,\n        search: {\n          placeholder: '搜索',\n          noData: '未找到相关信息',\n          hideOtherSidebarContent: true,\n        },\n      };\n    </script>\n    <!-- Docsify v4 -->\n    <script src=\"//fastly.jsdelivr.net/npm/docsify@4\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/docsify@4/lib/plugins/emoji.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-typescript.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-jsx.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-diff.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-tsx.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/prismjs@1/components/prism-json5.min.js\"></script>\n    <script src=\"//fastly.jsdelivr.net/npm/docsify-tabs\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/initialize.md",
    "content": "# <!-- {docsify-ignore} -->\n\n# 安装\n\n```bash\n# npm\nnpm install foca\n# yarn\nyarn add foca\n# pnpm\npnpm add foca\n```\n\n# 激活\n\nfoca 遵循`唯一store`原则，并提供了快速初始化的入口。\n\n```typescript\n// File: store.ts\nimport { store } from 'foca';\n\nstore.init();\n```\n\n好吧，就是这么简单！\n\n# 导入\n\n与原生 react-redux 类似，你需要把 foca 提供的 Provider 组件放置到入口文件，这样才能在业务组件中获取到数据。\n\n<!-- tabs:start -->\n\n#### ** React **\n\n```diff\n+ import './store';\n+ import { FocaProvider } from 'foca';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nconst container = document.getElementById('root');\nconst root = ReactDOM.createRoot(container);\n\nroot.render(\n+ <FocaProvider>\n    <App />\n+ </FocaProvider>\n);\n```\n\n#### ** React-Native **\n\n```diff\n+ import './store';\n+ import { FocaProvider } from 'foca';\nimport { Text, View } from 'react-native';\n\nexport default function App() {\n  return (\n+   <FocaProvider>\n      <View>\n        <Text>Hello World</Text>\n      </View>\n+   </FocaProvider>\n  )\n}\n```\n\n#### ** Taro.js **\n\n```diff\n+ import './store';\n+ import { FocaProvider } from 'foca';\nimport { Component } from 'react';\n\nexport default class App extends Component {\n  render() {\n-   return this.props.children;\n+   return <FocaProvider>{this.props.children}</FocaProvider>;\n  }\n}\n```\n\n<!-- tabs:end -->\n\n# 日志\n\n在开发阶段，如果你想实时查看状态的操作过程以及数据的变化细节，那么开启可视化界面是必不可少的一个环节。\n\n<!-- tabs:start -->\n\n#### ** 全局软件 **\n\n**优势:** 一次安装，所有项目都可以无缝使用。\n\n- 对于 Web 项目，可以安装 Chrome 浏览器的 [redux-devtools](https://github.com/reduxjs/redux-devtools) 扩展，然后打开控制台查看。\n- 对于 React-Native 项目，可以安装并启动软件 [react-native-debugger](https://github.com/jhen0409/react-native-debugger)，然后点击 App 里的按钮 `Debug with Chrome`即可连接软件，其本质也是 Chrome 的控制台\n\n接着，我们在 store 里注入增强函数：\n\n```typescript\nstore.init({\n  // 字符串 redux-devtools 即 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 的缩写\n  // 设置 redux-devtools 在生产环境(process.env.NODE_ENV === 'production')下会自动关闭\n  // 你也可以安装等效的插件包 @redux-devtools/extension 自由控制\n  compose: 'redux-devtools',\n});\n```\n\ncompose 也支持回调形式，目的是为了注入更多插件。\n\n```typescript\nimport { composeWithDevTools as compose } from '@redux-devtools/extension';\n// 或者使用原生的compose\n// import { compose } from 'foca';\n\nstore.init({\n  compose(enhancer) {\n    return compose(enhancer, ...more[]);\n  },\n});\n```\n\n#### ** 项目插件 **\n\n**优势:** 可选配置参数多，且在 Web 和 React-Native 中都能使用。\n\n```bash\n# npm\nnpm install redux-logger @types/redux-logger --save-dev\n# yarn\nyarn add redux-logger @types/redux-logger --dev\n# pnpm\npnpm add redux-logger @types/redux-logger -D\n```\n\n接着我们把这个包注入 store：\n\n```typescript\nimport { store, Middleware } from 'foca';\nimport { createLogger } from 'redux-logger';\n\nconst middleware: Middleware[] = [];\n\nif (process.env.NODE_ENV !== 'production') {\n  middleware.push(\n    createLogger({\n      collapsed: true,\n      diff: true,\n      duration: true,\n      logErrors: true,\n    }),\n  );\n}\n\nstore.init({\n  middleware,\n});\n```\n\n大功告成，下次你对 store 的数据做操作时，控制台就会有相应的通知输出。\n\n<!-- tabs:end -->\n\n# 开发热更\n\n<small>如果是 React-Native，你可以跳过这一节。</small>\n\n因为 store.ts 需要被入口文件引入，而 store.ts 又引入了部分 model（<small>持久化需要这么做</small>），所以如果相应的 model 做了修改操作时，会导致浏览器页面全量刷新而非热更新。如果你正在使用当前流行的打包工具，强烈建议加上`hot.accept`手动处理模块更新。\n\n<!-- tabs:start -->\n\n#### ** Vite **\n\n```typescript\n// File: store.ts\n\nstore.init(...);\n\n// https://cn.vitejs.dev/guide/api-hmr.html#hot-acceptcb\nif (import.meta.hot) {\n  import.meta.hot.accept(() => {\n    console.log('Hot updated: store');\n  });\n}\n```\n\n#### ** Webpack **\n\n```typescript\n// File: store.ts\n\n// ##################################################\n// ######                                     #######\n// ###### yarn add @types/webpack-env --dev   #######\n// ######                                     #######\n// ##################################################\n\nstore.init(...);\n\n// https://webpack.docschina.org/api/hot-module-replacement/\nif (module.hot) {\n  module.hot.accept(() => {\n    console.log('Hot updated: store');\n  });\n}\n```\n\n#### ** Webpack ESM **\n\n```typescript\n// File: store.ts\n\n// ##################################################\n// ######                                     #######\n// ###### yarn add @types/webpack-env --dev   #######\n// ######                                     #######\n// ##################################################\n\nstore.init(...);\n\n// https://webpack.docschina.org/api/hot-module-replacement/\nif (import.meta.webpackHot) {\n  import.meta.webpackHot.accept(() => {\n    console.log('Hot updated: store');\n  });\n}\n```\n\n<!-- tabs:end -->\n"
  },
  {
    "path": "docs/model.md",
    "content": "# <!-- {docsify-ignore} -->\n\n# Model\n\n原生的 redux 由 action/type/reducer 三个部分组成，大多数情况我们会分成 3 个文件分别存储。在实际使用中，这种模板式的书写方式不仅繁琐，而且难以将他们关联起来，类型提示就更麻烦了。\n\n基于此，我们提出了模型概念，以 state 为核心，任何更改 state 的操作都应该放在一起。\n\n```typescript\n// models/user.model.ts\nimport { defineModel } from 'foca';\n\nexport interface UserItem {\n  id: number;\n  name: string;\n  age: number;\n}\n\nconst initialState: UserItem[] = [];\n\nexport const userModel = defineModel('users', {\n  initialState,\n});\n```\n\n你已`defineModel`经定义了一个最基础的模型，其中第一个字符串参数为redux中的`唯一标识`，请确保其它模型不会再使用这个名字。\n\n对了，怎么注册到store？躺着别动！foca 已经自动把模型注册到 store 中心，也让你享受一下 **DRY** <small>(Don't Repeat Yourself)</small> 原则，因此在业务文件内直接导入模型就能使用。\n\n# State\n\nfoca 基于 redux 深度定制，所以 state 必须是个纯对象或者数组。\n\n```typescript\n// 对象\nconst initialState: { [K: string]: string } = {};\nconst objModel = defineModel('model-object', {\n  initialState,\n});\n\n// 数组\nconst initialState: number[] = [];\nconst arrayModel = defineModel('model-array', {\n  initialState,\n});\n```\n\n# Reducers\n\n模型光有 state 也不行，你现在拿到的就是一个空数组([])。加点数据上去吧，这时候就要用到 reducers：\n\n```typescript\nexport const userModel = defineModel('users', {\n  initialState,\n  reducers: {\n    addUser(state, user: UserItem) {\n      state.push(user);\n    },\n    updateName(state, id: number, name: string) {\n      const user = state.find((item) => item.id === id);\n      if (user) {\n        user.name = name;\n      }\n    },\n    removeUser(state, id: number) {\n      const index = state.findIndex((item) => item.id === id);\n      if (index >= 0) {\n        state.splice(index, 1);\n      }\n    },\n    clear() {\n      // 返回初始值\n      return this.initialState;\n    },\n  },\n});\n```\n\n就这么干，你已经赋予了模型生命，你等下可以和它互动了。现在，我们来说说这些 reducers 需要注意的几个点：\n\n- 函数的第一个参数一定是 state ，而且它是能自动识别到类型`State`的，你不用刻意地去指定。\n- 函数是可以带多个参数的，这全凭你自己的喜好。\n- 函数体内可以直接修改 state 对象（数组也属于对象）里的任何内容，这得益于 [immer](https://github.com/immerjs/immer) 的功劳。\n- 函数返回值必须是`State`类型。当然你也可以不返回，这时 foca 会认为你正在直接修改 state。\n- 如果你想使用`this`上下文，比如上面的 **clear()** 函数返回了初始值，那么请~~不要使用箭头函数~~。\n\n# Methods\n\n不可否认，你的数据不可能总是凭空捏造，在真实的业务场景中，数据总是通过接口获得，然后保存到 state 中。foca 贴心地为你准备了组合逻辑的函数，快来试试吧：\n\n```typescript\nconst userModel = defineModel('users', {\n  initialState,\n  methods: {\n    async get() {\n      const users = await http.get<UserItem[]>('/users');\n      this.setState(users);\n      return users;\n    },\n    async retrieve(id: number) {\n      const user = await http.get<UserItem>(`/users/${id}`);\n      this.setState((state) => {\n        state.push(user);\n      });\n    },\n    // 也可以是非异步的普通函数\n    findUser(id: number) {\n      return this.state.users.find((user) => user.id === id);\n    },\n  },\n});\n```\n\n瞧见没，你可以在 methods 里自由地使用 async/await 方案，然后通过上下文`this.setState`快速更新 state。\n\n接下来我们说说`setState`，这其实完全就是 reducers 的快捷方式，你可以直接传入数据或者使用匿名函数来操作，十分方便。这不禁让我们想起了 React Component 里的 setState？咳咳～～读书人的事，那能叫抄吗？\n\n<!-- tabs:start -->\n\n#### ** 直接修改 **\n\n依赖 immer 的能力，你可以直接修改回调函数给的 state 参数，这也是框架最推荐的方式\n\n```typescript\nthis.setState((state) => {\n  state.b = 2;\n});\n\nthis.setState((state) => {\n  state.push('a');\n  state.shift();\n});\n```\n\n#### ** 部分更新 **\n\n是的，你可以返回一部分数据，而且这个特性很简洁高效，框架会使用`Object.assign`帮你把剩余的属性加回去。\n\n!> 只针对 object 类型，而且只有第一级属性可以缺省（参考 React Class Component）\n\n```typescript\nthis.setState({ a: 1 });\n\nthis.setState((state) => {\n  return { a: 1 }; // <==> state.a = 1;\n});\n```\n\n#### ** 全量更新 **\n\n就是重新设置所有数据\n\n```typescript\nthis.setState({ a: 1, b: 2 });\nthis.setState((state) => {\n  return { a: 1, b: 2 };\n});\n\nthis.setState(['a', 'b', 'c']);\nthis.setState((state) => {\n  return ['a', 'b', 'c'];\n});\n\n// 重新设置成初始值\nthis.setState(this.initialState);\n```\n\n<!-- tabs:end -->\n\n嗯？你压根就不想用`setState`，你觉得这样看起来很混乱？Hold on，你突然想起可以使用 reducers 去改变 state 不是吗？\n\n```typescript\nconst userModel = defineModel('users', {\n  initialState,\n  reducers: {\n    addUser(state, user: UserItem) {\n      state.push(user);\n    },\n  },\n  methods: {\n    async retrieve(id: number) {\n      const user = await http.get<UserItem>(`/users/${id}`);\n      // 调用reducers里的函数\n      this.addUser(user);\n    },\n  },\n});\n```\n\n好吧，这样看起来更纯粹一些，代价就是要委屈你多写几行代码了。\n\n# Computed\n\n对于一些数据，其实是需要经过比较冗长的拼接或者复杂的计算才能得出结果，同时你想自动缓存这些结果？来吧，展示：\n\n```typescript\nconst initialState = {\n  firstName: 'tick',\n  lastName: 'tock',\n  country: 0,\n};\n\nconst userModel = defineModel('users', {\n  initialState,\n  computed: {\n    fullName() {\n      return this.state.firstName + '.' + this.state.lastName;\n    },\n    profile(age: number, address?: string) {\n      return this.fullName() + age + (address || 'empty');\n    },\n  },\n});\n```\n\n恕我直言，有点 Methods 的味道了。味道是这个味道，但是本质不一样，当我们多次执行computed函数时，因为存在缓存的概念，所以不会真正地执行该函数。\n\n```typescript\nuserModel.fullName(); // 执行函数，生成缓存\nuserModel.fullName(); // 使用缓存\nuserModel.fullName(); // 使用缓存\n```\n\n带参数的计算属性可以理解为所有参数就是一个key，每个key都会生成一个计算属性实例，互不干扰。\n\n```typescript\nuserModel.profile(20); // 执行函数，生成实例1缓存\nuserModel.profile(20); // 实例1缓存\nuserModel.profile(123); // 执行函数，生成实例2缓存\nuserModel.profile(123); // 实例2缓存\n\nuserModel.profile(20); // 实例1缓存\nuserModel.profile(123); // 实例2缓存\n```\n\n参数尽量使用基本类型，**不建议**使用对象或者数组作为计算属性的实参，因为如果每次都传新建的复合类型，无法起到缓存的效果，执行速度反而变慢，这和`useMemo(callback, deps)`函数的第二个参数（依赖项）是一个原理。如果实在想用复合类型作为参数，不烦考虑一下放到`Methods`里？\n\n---\n\n缓存什么时候才会更新？框架自动收集依赖，只有其中某个依赖更新了，计算属性才会更新。上面的例子中，当`firstName`或者`lastName`有变化时，fullName 将被标记为`dirty`状态，下一次访问则会重新计算结果。而当`country`变化时，不影响 fullName 的结果，下一次访问仍使用缓存作为结果。\n\n!> 可以在 computed 中使用其它 model 的数据。\n"
  },
  {
    "path": "docs/persist.md",
    "content": "# 持久化\n\n持久化是自动把数据通过引擎存储到某个空间的过程。\n\n如果你的某个 api 数据常年不变，那么建议你把它扔到本地做个缓存，这样用户下次再访问你的页面时，可以第一时间看到缓存的内容。如果你不想让用户每次刷新页面就重新登录，那么持久化很适合你。\n\n# 入口\n\n你需要在初始化 store 时开启持久化\n\n```typescript\n// File: store.ts\nimport { store } from 'foca';\nimport { userModel } from './userModel';\nimport { agentModel } from './agentModel';\n\nstore.init({\n  persist: [\n    {\n      key: '$PROJECT_$ENV',\n      version: 1,\n      engine: localStorage,\n      // 模型白名单列表\n      models: [userModel, agentModel],\n    },\n  ],\n});\n```\n\n把需要持久化的模型扔进去，foca 就能自动帮你存取数据。\n\n`key`即为存储路径，最好采用**项目名-环境名**的形式组织。纯前端项目如果和其他前端项目共用一个域名，或者在同一域名下，则有可能使用共同的存储空间，因此需要保证`key`是唯一的值。\n\n# 存储引擎\n\n不同的引擎会把数据存储到不同的位置，使用哪个引擎取决于项目跑在什么环境。引擎操作可以是同步的也可以是异步的。下面列举的第三方库也可以**直接当作**存储引擎：\n\n- window.localStorage - 浏览器自带\n- window.sessionStorage - 浏览器自带\n- [localforage](https://www.npmjs.com/package/localforage) (IndexedDB, WebSQL) - 浏览器专用\n- [@react-native-async-storage/async-storage](https://www.npmjs.com/package/@react-native-async-storage/async-storage) - React-Native专用\n- [foca-mmkv-storage](https://github.com/foca-js/foca-mmkv-storage) - React-Native专用\n- [foca-taro-storage](https://github.com/foca-js/foca-taro-storage) - Taro专用\n- [foca-electron-storage](https://github.com/foca-js/foca-taro-storage) - Electron专用\n- [foca-cookie-storage](https://github.com/foca-js/foca-cookie-storage) - 浏览器专用，存储到cookie\n\n如果有必要，你也可以自己实现一个引擎：\n\n```typescript\n// import { StorageEngine } from 'foca';\n\ninterface StorageEngine {\n  getItem(key: string): string | null | Promise<string | null>;\n  setItem(key: string, value: string): any;\n  removeItem(key: string): any;\n  clear(): any;\n}\n```\n\n如果是在测试环境，也可以尝试使用内置的内存引擎\n\n```typescript\nimport { memoryStorage } from 'foca';\n```\n\n# 设置版本号\n\n当数据结构变化，我们不得不升级版本号来`删除`持久化数据，版本号又分为`全局版本`和`模型版本`两种。当修改模型内版本号时，仅删除该模型的持久化数据，而修改全局版本号时，白名单内所有模型的持久化数据都被删除。\n\n建议优先修改模型内版本号！！\n\n```diff\nconst stockModel = defineModel('stock', {\n  initialState: {},\n  persist: {\n    // 模型版本号，影响当前模型\n+   version: '2.0',\n  },\n});\n\nstore.init({\n  persist: [\n    {\n      key: '$PROJECT_normal_$ENV',\n      // 全局版本号，影响白名单全部模型\n+     version: '3.6',\n      engine: engines.localStorage,\n      models: [musicModel, stockModel],\n    },\n  ],\n});\n```\n\n# 数据合并\n\n> v3.0.0\n\n在项目的推进过程中，难免需要根据产品需求更新模型数据结构，结构变化后，我们可以简单粗暴地通过`版本号+1`的方式来删除持久化的数据。但如果只是新增了某一个字段，我们希望持久化恢复时能自动识别。试试推荐的`合并模式`吧：\n\n```diff\nstore.init({\n  persist: [\n    {\n      key: 'myproject-a-prod',\n      version: 1,\n+     merge: 'merge',\n      engine: engines.localStorage,\n      models: [userModel],\n    },\n  ],\n});\n```\n\n很轻松就设置上了，合并模式目前有3种可选的类型：\n\n- `replace` - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据\n- `merge` - 合并模式（默认）。数据从存储引擎取出后，与初始数据多余部分进行合并，可以理解为 **Object.assign()** 操作\n- `deep-merge` - 二级合并模式。在合并模式的基础上，如果某个key的值为对象，则该对象也会执行合并操作\n\n如果某个模型比较特殊，我们也可以在里面单独设置合并模式。\n\n```diff\nconst userModel = defineModel('user', {\n  initialState: {},\n  persist: {\n+   merge: 'deep-merge',\n  },\n});\n```\n\n接下来看看它的具体表现：\n\n```typescript\nconst persistState = { obj: { test1: 'persist' } };\nconst initialState = { obj: { test2: 'initial' }, foo: 'bar' };\n\n// replace 效果\nconst state = { obj: { test1: 'persist' } };\n// merge 效果\nconst state = { obj: { test1: 'persist' }, foo: 'bar' };\n// deep-merge 效果\nconst state = { obj: { test1: 'persist', test2: 'initial' }, foo: 'bar' };\n```\n\n需要注意的是合并模式对`数组无效`，当持久化数据和初始数据都为数组类型时，会强制使用持久化数据。当持久化数据和初始数据任何一边为数组类型时，会强制使用初始化数据。\n\n```typescript\nconst persistState = [1, 2, 3];  ✅\nconst initialState = [4, 5, 6, 7];  ❌\n// 合并效果\nconst state = [1, 2, 3];\n\n// -------------------------\n\nconst persistState = [1, 2, 3];  ❌\nconst initialState = { foo: 'bar' };  ✅\n// 合并效果\nconst state = { foo: 'bar' };\n\n// -------------------------\n\nconst persistState = { foo: 'bar' };  ❌\nconst initialState = [1, 2, 3];  ✅\n// 合并效果\nconst state = [1, 2, 3];\n```\n\n# 系列化钩子\n\n> v3.0.0\n\n数据在模型与持久化引擎互相转换期间，我们希望对它进行一些额外操作以满足业务需求。比如：\n\n- 只缓存部分字段，避免存储尺寸超过存储空间限制\n- 改变数据结构或者内容\n- 更新时间等动态信息\n\nfoca提供了一对实用的过滤函数`dump`和`load`。**dump** 即 model->persist，**load** 即 persist->model。\n\n```typescript\nconst model = defineModel('model', {\n  initialState: {\n    mode: 'foo', // 本地的设置，需要持久化缓存\n    hugeDate: [], // API请求数据，数据量太大\n  },\n  persist: {\n    // 系列化\n    dump(state) {\n      return state.mode;\n    },\n    // 反系列化\n    load(mode) {\n      return { ...this.initialState, mode };\n    },\n  },\n});\n```\n\n# 分组\n\n我们注意到 persist 其实是个数组，这意味着你可以多填几组配置上去，把不同的模型数据存储到不同的地方。这看起来很酷，但我猜你不一定需要！\n\n```typescript\nimport { store, engines } from 'foca';\n\nstore.init({\n  persist: [\n    {\n      key: 'myproject-a-prod',\n      version: 1,\n      engine: engines.localStorage,\n      models: [userModel],\n    },\n    {\n      key: 'myproject-b-prod',\n      version: 5,\n      engine: engines.sessionStorage,\n      models: [agentModel, teacherModel],\n    },\n    {\n      key: 'myproject-vip-prod',\n      version: 1,\n      engine: engines.localStorage,\n      models: [customModel, otherModel],\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "docs/redux-toolkit.md",
    "content": "<table>\n<thead>\n<tr>\n<th>foca</th>\n<th>toolkit</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<th colspan=\"2\">\n开源时间\n</th>\n</tr>\n<tr>\n<td>2021-10</td>\n<td>2018-03</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n文档地址\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n[foca.js.org](https://foca.js.org)（中文文档）\n\n</td>\n<td>\n\n[redux-toolkit.js.org](https://redux-toolkit.js.org/)（English documentation）\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n安装\n</th>\n</tr>\n<tr>\n<td  valign=\"top\">\n\n```bash\npnpm add foca\n```\n\n</td>\n<td>\n\n```bash\npnpm add @reduxjs/toolkit react-redux\n# 持久化\n# pnpm add redux-persist\n# 计算属性\n# pnpm add reselect\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n初始化\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\n// store.ts\nimport { store } from 'foca';\n\nfoca.init({});\n```\n\n</td>\n<td>\n\n```typescript\n// store.ts\nimport { useDispatch, useSelector } from 'react-redux';\nimport { configureStore } from '@reduxjs/toolkit';\nimport counterReducer from './counterSlice';\n\nexport const store = configureStore({\n  reducer: {\n    // 项目中所有reducer都要import注册到这里（枯燥）\n    counter: counterReducer,\n  },\n});\n\nexport type RootState = ReturnType<typeof store.getState>;\nexport type AppDispatch = typeof store.dispatch;\nexport const useAppDispatch = useDispatch.withTypes<AppDispatch>();\nexport const useAppSelector = useSelector.withTypes<RootState>();\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n注入React\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\nimport './store';\nimport { FocaProvider } from 'foca';\n\nReactDOM.render(\n  <FocaProvider>\n    <App />\n  </FocaProvider>,\n);\n```\n\n</td>\n<td>\n\n```typescript\nimport { store } from './store';\nimport { Provider } from 'react-redux';\n\nReactDOM.render(\n  <Provider store={store}>\n    <App />\n  </Provider>,\n);\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n创建Reducer\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\n// counter.model.ts\nimport { defineModel } from 'foca';\n\nconst initialState: { value: number } = {\n  value: 0,\n};\n\nexport const counterModel = defineModel('counter', {\n  initialState,\n  reducers: {\n    increment(state) {\n      state.value += 1;\n    },\n    decrement(state) {\n      state.value -= 1;\n    },\n    incrementByAmount(state, amount: number) {\n      state.value += amount;\n    },\n  },\n});\n```\n\n</td>\n<td>\n\n```typescript\n// counterSlice.ts\nimport { createSlice } from '@reduxjs/toolkit';\nimport type { PayloadAction } from '@reduxjs/toolkit';\n\nconst initialState: { value: number } = {\n  value: 0,\n};\n\nexport const counterSlice = createSlice({\n  name: 'counter',\n  initialState,\n  reducers: {\n    increment(state) {\n      state.value += 1;\n    },\n    decrement(state) {\n      state.value -= 1;\n    },\n    incrementByAmount(state, action: PayloadAction<number>) {\n      state.value += action.payload;\n    },\n  },\n});\n\nconst { actions, reducer } = counterSlice;\nexport const { increment, decrement, incrementByAmount } = actions;\nexport default reducer;\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n组件中获取数据\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```tsx\nimport { useModel } from 'foca';\nimport { counterModel } from './counter.model';\n\nexport const Counter: FC = () => {\n  const count = useModel(counterModel, (state) => state.value);\n\n  return <div onClick={counterModel.increment}>{count}</div>;\n};\n```\n\n</td>\n<td>\n\n```tsx\nimport { useAppSelector, useAppDispatch } from './store';\nimport { increment } from './counterSlice';\n\nexport const Counter: FC = () => {\n  const count = useAppSelector((state) => state.counter.value);\n  const dispatch = useAppDispatch();\n\n  return <div onClick={() => dispatch(increment())}>{count}</div>;\n};\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n异步请求和loading\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\nimport { defineModel } from 'foca';\n\nexport const todoModel = defineModel('todos', {\n  initialState: { todos: [] },\n  reducers: {},\n  methods: {\n    // 返回Promise时自带loading\n    async fetchTodos() {\n      const response = await http.request('/api');\n      this.setState({ todos: response.data });\n    },\n  },\n});\n```\n\n</td>\n<td>\n\n```typescript\nimport { createSlice, createAsyncThunk } from '@reduxjs/toolkit';\n\nexport const fetchTodosAsync = createAsyncThunk(\n  'todos/fetchTodos',\n  async () => {\n    const response = await http.request('/api');\n    return response.data;\n  },\n);\n\nconst todoSlice = createSlice({\n  name: 'todos',\n  initialState: { todos: [], loading: false },\n  reducers: {},\n  extraReducers(builder) {\n    builder\n      .addCase(fetchTodosAsync.pending, (state) => {\n        state.loading = true;\n      })\n      .addCase(fetchTodosAsync.fulfilled, (state, action) => {\n        state.loading = false;\n        state.todos = action.payload;\n      })\n      .addCase(fetchTodosAsync.rejected, (state, action) => {\n        state.loading = false;\n      });\n  },\n});\n\nexport default todosSlice.reducer;\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n在组件中使用loading状态\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\nimport { useLoading } from 'foca';\nimport { todoModel } from './todo.model';\n\nconst Todo: FC = () => {\n  const loading = useLoading(todoModel.fetchTodos);\n\n  useEffect(() => {\n    todoModel.fetchTodos();\n  }, []);\n\n  return <div />;\n};\n```\n\n</td>\n<td>\n\n```typescript\nimport { useAppSelector, useAppDispatch } from './store';\n\nconst Todo: FC = () => {\n  const dispatch = useAppDispatch();\n  const loading = useAppSelector((s) => s.todos.loading);\n\n  useEffect(() => {\n    dispatch(fetchTodosAsync());\n  }, [dispatch]);\n\n  return <div />;\n};\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n持久化\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\nimport { store } from 'foca';\nimport { counterModel } from './counter.model';\n\nfoca.init({\n  persist: [\n    {\n      key: 'root',\n      engine: localStorage,\n      models: [counterModel],\n    },\n  ],\n});\n```\n\n</td>\n<td>\n\n```bash\npnpm add redux-persist\n```\n\n```typescript\nimport { configureStore } from '@reduxjs/toolkit';\nimport storage from 'redux-persist/lib/storage';\nimport { combineReducers } from 'redux';\nimport { persistReducer } from 'redux-persist';\nimport counterReducer from './counterSlice';\n\nconst reducers = combineReducers({\n  counter: counterReducer,\n});\n\nconst persistConfig = {\n  key: 'root',\n  storage,\n  whitelist: ['counter'],\n};\n\nconst persistedReducer = persistReducer(persistConfig, reducers);\nconst store = configureStore({ reducer: persistedReducer });\n\nexport default store;\n```\n\n```tsx\nimport { store } from './store';\nimport { Provider } from 'react-redux';\nimport { PersistGate } from 'redux-persist/integration/react';\nimport { persistStore } from 'redux-persist';\n\nReactDOM.render(\n  <Provider store={store}>\n    <PersistGate loading={null} persistor={persistStore(store)}>\n      <App />\n    </PersistGate>\n  </Provider>,\n);\n```\n\n</td>\n</tr>\n<tr>\n<th colspan=\"2\">\n计算属性\n</th>\n</tr>\n<tr>\n<td valign=\"top\">\n\n```typescript\nimport { defineModel } from 'foca';\n\nexport const todoModel = defineModel('todos', {\n  initialState: { todos: [] },\n  computed: {\n    todos() {\n      return this.state.todos.filter((todo) => !!todo.completed);\n    },\n  },\n});\n\n// 在组件中使用\nconst memoTodos = useComputed(todoModel.todos);\n```\n\n</td>\n<td>\n\n```bash\npnpm add reselect\n```\n\n```typescript\nimport { createSlice } from '@reduxjs/toolkit';\nimport { createSelector } from 'reselect';\n\nconst todoSlice = createSlice({\n  name: 'todos',\n  initialState: { todos: [] },\n});\n\nconst memoizedSelectCompletedTodos = createSelector(\n  [(state: RootState) => state.todos],\n  (todos) => {\n    return todos.filter((todo) => !!todo.completed);\n  },\n);\n\n// 在组件中使用\nconst memoTodos = memoizedSelectCompletedTodos(state);\n```\n\n</td>\n</tr>\n</tbody>\n</table>\n"
  },
  {
    "path": "docs/test.md",
    "content": "前端的需求变化总是太快导致测试用例跟不上，甚至部分程序员根本就没想过为自己写的代码编写测试，他们心里总是想着`出错了再说`。对于要求拥有高质量体验的项目，测试是必不可少的，它能使得代码更加稳健，并且在新增功能和重构代码时，都无需太担心会破坏原有的逻辑。在多人协作的项目中，充足的测试可以让其他人员对相应的逻辑有更充分的了解。\n\n## 测试框架\n\n- [Vitest](https://cn.vitest.dev/)\n- [Jest](https://jestjs.io/zh-Hans/)\n- [Mocha](https://mochajs.org/)\n- [node:test](https://nodejs.org/dist/latest-v18.x/docs/api/test.html#test-runner) node@18.8.0开始提供\n\n## 准备工作\n\n我们已经知道，foca 是基于 redux 存储数据的，所以在测试模型之前，需要先激活 store，并且在测试完毕后销毁以免影响其他测试。\n\n```typescript\n// test/model.test.ts\nimport { store } from 'foca';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n```\n\n## 单元测试\n\n我们假设你已经写好了一个模型\n\n```typescript\n// src/models/my-custom.model.ts\nimport { defineModel } from 'foca';\n\nexport const myCustomModel = defineModel('my-model', {\n  initialState: { count: 0 },\n  reducers: {\n    plus(state, step: number = 1) {\n      state.count += step;\n    },\n    minus(state, step: number = 1) {\n      state.count -= step;\n    },\n  },\n});\n```\n\n对，它现在很简洁，但是已经满足测试条件了\n\n```typescript\n// test/model.test.ts\nimport { store } from 'foca';\nimport { myCustomModel } from '../src/models/my-custom.model.ts';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('initial state', () => {\n  expect(myCustomModel.state.count).toBe(0);\n});\n\ntest('myCustomModel.plus', () => {\n  myCustomModel.plus();\n  expect(myCustomModel.state.count).toBe(1);\n  myCustomModel.plus(5);\n  expect(myCustomModel.state.count).toBe(6);\n  myCustomModel.plus(100);\n  expect(myCustomModel.state.count).toBe(106);\n});\n\ntest('myCustomModel.minus', () => {\n  myCustomModel.minus();\n  expect(myCustomModel.state.count).toBe(-1);\n  myCustomModel.minus(10);\n  expect(myCustomModel.state.count).toBe(-11);\n  myCustomModel.minus(28);\n  expect(myCustomModel.state.count).toBe(-39);\n});\n```\n\n**只测试**业务上的那部分逻辑，每处逻辑分开测试，这就是 `Unit Test` 和你该做的。\n\n## 覆盖率\n\n对于大一些的项目，你很难保证所有逻辑都已经写进了测试，则建议打开测试框架的覆盖率功能并检查每一行的覆盖情况。一般情况下，覆盖率的报告会放到`coverage`目录，你只需要在浏览器中打开`coverage/index.html`就可以查看了。\n"
  },
  {
    "path": "docs/troubleshooting.md",
    "content": "# <!-- {docsify-ignore} -->\n\n# 函数里 this 的类型是 any\n\n需要在文件 **tsconfig.json** 中开启`\"strict\": true`或者`\"noImplicitThis\": true`。\n\n# 为什么要用 this\n\n1. 可调用额外的内部属性和方法；\n2. 可调用自定义的私有方法；\n3. 方便克隆(cloneModel)，this 作为 context 是可变的。\n\n# 没找到持久化守卫组件\n\n内置在入口组件 `FocaProvider` 里了，初始化 store 的时候如果配置了 persist 属性，守卫会自动开启。\n\n# setState 和 reducers 的区别\n\n互补关系。methods.setState 是专门为网络请求和一些组合业务设置的快捷操作（直接传入 state 或者回调）。相对于一些不需要复用的 reducer 函数，用 setState 反而能让模型对外暴露更少的接口，组件里用起来就会更舒服一些。\n\n# 追踪 methods 的执行状态有性能问题吗\n\n没有。我们已经知道如果想获得状态，就必须通过`useLoading`, `getLoading` 这些 api 获取，但如果你没有显性地通过这些 api 获取某个函数的状态，就不会触发该函数的状态追踪逻辑，即自动忽略。\n\n状态数据使用独立的内部 store 存储，任何变动都不会触发模型数据(useModel, connect)的重新检查。\n\n# 为什么不支持 SSR\n\n因为 foca 是遵循单一 store 存储（单例），它的优点就是 model 创建后无需手动注册，在 CSR(Client-Side-Rendering) 中用起来很流畅。而 SSR(Server-Side-Rendering) 方案中，node 进程常驻于内存，这意味着所有的请求都会共享同一个 store，数据也必然会乱套。所以一些 SSR 框架比如 next.js, remix 都无法使用了。\n\n再者，需要SSR的页面，一般是需要 SEO 的展示页，这种项目也用不上状态管理。并且 SSR 其实不是唯一的 SEO 优化方案，利用 user-agent 配合服务端动态渲染一样可以搞定，参考文章：https://segmentfault.com/a/1190000023481810\n\n# this.initialState 是否多余\n\n大部分情况下你会觉得多余，直到你使用`cloneModel`复制出一个新的模型。我们允许复制模型的同时修改初始值，所以`this.initialState`就和`this.state`一样能明确自己归属于哪个模型。\n\n同时，每次获取`this.initialState`，框架都会返回给你一份全新的数据（deep clone)，这样再也不怕你会改动初始值了。\n\n# 命名有什么建议\n\n模型文件名建议采用 `some-word.model.ts` 这种命名方式，可读性好。<br/>\n模型内容建议采用 `export const someWordModel = defineModel('some-word')` 驼峰的方式来创建，变量名和模型名具有一定的关联性，也不容易与其它模型冲突。\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"foca\",\n  \"version\": \"4.0.1\",\n  \"repository\": \"git@github.com:foca-js/foca.git\",\n  \"homepage\": \"https://foca.js.org\",\n  \"keywords\": [\n    \"redux\",\n    \"redux-model\",\n    \"redux-typescript\",\n    \"react-redux\",\n    \"react-model\",\n    \"redux-toolkit\"\n  ],\n  \"description\": \"流畅的React状态管理库\",\n  \"contributors\": [\n    \"罪 <fanwenhua1990@gmail.com> (https://github.com/geekact)\"\n  ],\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"test\": \"vitest run\",\n    \"prepublishOnly\": \"tsup\",\n    \"docs\": \"docsify serve ./docs\",\n    \"prepare\": \"husky\"\n  },\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/esm/index.js\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"files\": [\n    \"dist\",\n    \"LICENSE\",\n    \"package.json\",\n    \"README.md\",\n    \"CHANGELOG.md\"\n  ],\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  },\n  \"volta\": {\n    \"node\": \"18.16.0\",\n    \"pnpm\": \"9.15.6\"\n  },\n  \"packageManager\": \"pnpm@9.15.6\",\n  \"peerDependencies\": {\n    \"react\": \"^18 || ^19\",\n    \"react-native\": \">=0.69\",\n    \"typescript\": \"^5\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    },\n    \"react-native\": {\n      \"optional\": true\n    }\n  },\n  \"dependencies\": {\n    \"immer\": \"^9.0.21\",\n    \"react-redux\": \"^9.2.0\",\n    \"redux\": \"^5.0.1\",\n    \"topic\": \"^3.0.2\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^19.7.1\",\n    \"@commitlint/config-conventional\": \"^19.7.1\",\n    \"@react-native-async-storage/async-storage\": \"^2.1.2\",\n    \"@redux-devtools/extension\": \"^3.3.0\",\n    \"@testing-library/react\": \"^16.2.0\",\n    \"@types/node\": \"^22.13.8\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"@vitest/coverage-istanbul\": \"^3.0.7\",\n    \"docsify-cli\": \"^4.4.4\",\n    \"fake-indexeddb\": \"^6.0.0\",\n    \"husky\": \"^9.1.7\",\n    \"jsdom\": \"^26.0.0\",\n    \"localforage\": \"^1.10.0\",\n    \"prettier\": \"^3.5.3\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-test-renderer\": \"^19.0.0\",\n    \"rxjs\": \"^7.8.2\",\n    \"sleep-promise\": \"^9.1.0\",\n    \"ts-expect\": \"^1.3.0\",\n    \"tsup\": \"^8.4.0\",\n    \"typescript\": \"^5.8.2\",\n    \"vitest\": \"^3.0.7\"\n  }\n}\n"
  },
  {
    "path": "src/actions/loading.ts",
    "content": "import type { UnknownAction } from 'redux';\n\nexport const TYPE_SET_LOADING = '@@store/loading';\n\nexport const LOADING_CATEGORY = '##' + Math.random();\n\nexport const DESTROY_LOADING = TYPE_SET_LOADING + '/destroy';\n\nexport interface LoadingAction extends UnknownAction {\n  type: typeof TYPE_SET_LOADING;\n  model: string;\n  method: string;\n  payload: {\n    loading: boolean;\n    category: string | number;\n  };\n}\n\nexport const isLoadingAction = (\n  action: UnknownAction | unknown,\n): action is LoadingAction => {\n  const tester = action as LoadingAction;\n  return (\n    tester.type === TYPE_SET_LOADING &&\n    !!tester.model &&\n    !!tester.method &&\n    !!tester.payload\n  );\n};\n\nexport interface DestroyLoadingAction extends UnknownAction {\n  type: typeof DESTROY_LOADING;\n  model: string;\n}\n\nexport const isDestroyLoadingAction = (\n  action: UnknownAction | unknown,\n): action is DestroyLoadingAction => {\n  const tester = action as DestroyLoadingAction;\n  return tester.type === DESTROY_LOADING && !!tester.model;\n};\n"
  },
  {
    "path": "src/actions/model.ts",
    "content": "import type { Action, UnknownAction } from 'redux';\nimport { isFunction } from '../utils/is-type';\n\nexport interface PreModelAction<State extends object = object, Payload = object>\n  extends UnknownAction,\n    Action<string> {\n  model: string;\n  preModel: true;\n  payload: Payload;\n  actionInActionGuard?: () => void;\n  consumer(state: State, action: PreModelAction<State, Payload>): State | void;\n}\n\nexport interface PostModelAction<State = object>\n  extends UnknownAction,\n    Action<string> {\n  model: string;\n  postModel: true;\n  next: State;\n}\n\nexport const isPreModelAction = (\n  action: UnknownAction | unknown,\n): action is PreModelAction => {\n  const test = action as PreModelAction;\n  return test.preModel && !!test.model && isFunction(test.consumer);\n};\n\nexport const isPostModelAction = <State extends object>(\n  action: UnknownAction,\n): action is PostModelAction<State> => {\n  const test = action as PostModelAction<State>;\n  return test.postModel && !!test.next;\n};\n"
  },
  {
    "path": "src/actions/persist.ts",
    "content": "import type { UnknownAction } from 'redux';\n\nconst TYPE_PERSIST_HYDRATE = '@@persist/hydrate';\n\nexport interface PersistHydrateAction extends UnknownAction {\n  type: typeof TYPE_PERSIST_HYDRATE;\n  payload: Record<string, object>;\n}\n\nexport const actionHydrate = (\n  states: Record<string, object>,\n): PersistHydrateAction => {\n  return {\n    type: TYPE_PERSIST_HYDRATE,\n    payload: states,\n  };\n};\n\nexport const isHydrateAction = (\n  action: UnknownAction,\n): action is PersistHydrateAction => {\n  return (action as PersistHydrateAction).type === TYPE_PERSIST_HYDRATE;\n};\n"
  },
  {
    "path": "src/actions/refresh.ts",
    "content": "import type { UnknownAction } from 'redux';\n\nconst TYPE_REFRESH_STORE = '@@store/refresh';\n\nexport interface RefreshAction extends UnknownAction {\n  type: typeof TYPE_REFRESH_STORE;\n  payload: {\n    force: boolean;\n  };\n}\n\nexport const actionRefresh = (force: boolean): RefreshAction => {\n  return {\n    type: TYPE_REFRESH_STORE,\n    payload: {\n      force,\n    },\n  };\n};\n\nexport const isRefreshAction = (\n  action: UnknownAction,\n): action is RefreshAction => {\n  return (action as RefreshAction).type === TYPE_REFRESH_STORE;\n};\n"
  },
  {
    "path": "src/api/get-loading.ts",
    "content": "import { LOADING_CATEGORY } from '../actions/loading';\nimport { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';\nimport { loadingStore, FindLoading } from '../store/loading-store';\nimport { isFunction } from '../utils/is-type';\n\n/**\n * 检测给定的effect方法中是否有正在执行的。支持多个方法同时传入。\n *\n * ```typescript\n * loading = getLoading(effect);\n * loading = getLoading(effect1, effect2, ...);\n * ```\n *\n */\nexport function getLoading(\n  effect: PromiseEffect,\n  ...more: PromiseEffect[]\n): boolean;\n\n/**\n * 检测给定的effect方法是否正在执行。\n *\n * ```typescript\n * loadings = getLoading(effect.room);\n * loading = loadings.find(CATEGORY)\n * ```\n */\nexport function getLoading(effect: PromiseRoomEffect): FindLoading;\n\n/**\n * 检测给定的effect方法是否正在执行。\n *\n * ```typescript\n * loading = getLoading(effect.room, CATEGORY);\n * ```\n */\nexport function getLoading(\n  effect: PromiseRoomEffect,\n  category: string | number,\n): boolean;\n\nexport function getLoading(\n  effect: PromiseEffect | PromiseRoomEffect,\n  category?: string | number | PromiseEffect,\n): boolean | FindLoading {\n  const args = arguments;\n\n  if (effect._.hasRoom && !isFunction(category)) {\n    const loadings = loadingStore.get(effect).loadings;\n    return category === void 0 ? loadings : loadings.find(category);\n  }\n\n  for (let i = args.length; i-- > 0; ) {\n    if (loadingStore.get(args[i]).loadings.find(LOADING_CATEGORY)) {\n      return true;\n    }\n  }\n  return false;\n}\n"
  },
  {
    "path": "src/api/use-computed.ts",
    "content": "import { ComputedFlag } from '../model/types';\nimport { useModelSelector } from '../redux/use-selector';\nimport { toArgs } from '../utils/to-args';\n\nexport interface UseComputedFlag extends ComputedFlag {\n  (...args: any[]): any;\n}\n\n/**\n * 计算属性hooks函数，第二个参数开始传入计算属性的参数（如果有）\n *\n * ```typescript\n *\n * const App: FC = () => {\n *   const fullName = useComputed(model.fullName);\n *   const profile = useComputed(model.profile, 25);\n *   return <p>{profile}</p>;\n * }\n *\n * const model = defineModel('my-model', {\n *   initialState: { firstName: '', lastName: '' },\n *   computed: {\n *     fullName() {\n *       return this.state.firstName + this.state.lastName;\n *     },\n *     profile(age: number, address?: string) {\n *       return this.fullName() + age + address;\n *     }\n *   }\n * });\n *\n * ```\n */\nexport function useComputed<T extends UseComputedFlag>(\n  ref: T,\n  ...args: Parameters<T>\n): T extends (...args: any[]) => infer R ? R : never;\n\nexport function useComputed(ref: UseComputedFlag) {\n  const args = toArgs(arguments, 1);\n  return useModelSelector(() => ref.apply(null, args));\n}\n"
  },
  {
    "path": "src/api/use-isolate.ts",
    "content": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { DestroyLoadingAction, DESTROY_LOADING } from '../actions/loading';\nimport { loadingStore } from '../store/loading-store';\nimport { modelStore } from '../store/model-store';\nimport { cloneModel } from '../model/clone-model';\nimport { Model } from '../model/types';\n\nlet globalCounter = 0;\nconst hotReloadCounter: Record<string, number> = {};\n\n/**\n * 创建局部模型，它的数据变化不会影响到全局模型，而且会随着组件一起销毁\n * ```typescript\n * const testModel = defineModel({\n *   initialState,\n *   events: {\n *     onInit() {},    // 挂载回调\n *     onDestroy() {}, // 销毁回调\n *   }\n * });\n *\n * function App() {\n *   const model = useIsolate(testModel);\n *   const state = useModel(model);\n *\n *   return <p>Hello</p>;\n * }\n * ```\n */\nexport const useIsolate = <\n  State extends object = object,\n  Action extends object = object,\n  Effect extends object = object,\n  Computed extends object = object,\n>(\n  globalModel: Model<string, State, Action, Effect, Computed>,\n): Model<string, State, Action, Effect, Computed> => {\n  const initialCount = useState(() => globalCounter++)[0];\n  const uniqueName =\n    process.env.NODE_ENV === 'production'\n      ? useProdName(globalModel.name, initialCount)\n      : useDevName(globalModel, initialCount, new Error());\n\n  const localModelRef = useRef<typeof globalModel | undefined>(undefined);\n  useEffect(() => {\n    localModelRef.current = isolateModel;\n  });\n\n  // 热更时会重新执行useMemo，因此只能用ref\n  const isolateModel =\n    localModelRef.current?.name === uniqueName\n      ? localModelRef.current\n      : cloneModel(uniqueName, globalModel);\n\n  return isolateModel;\n};\n\nconst useProdName = (modelName: string, count: number) => {\n  const uniqueName = `@isolate:${modelName}#${count}`;\n\n  useEffect(\n    () => () => {\n      setTimeout(unmountModel, 0, uniqueName);\n    },\n    [uniqueName],\n  );\n\n  return uniqueName;\n};\n\n/**\n * 开发模式下，需要Hot Reload。\n * 必须保证数据不会丢，即如果用户一直保持`model.name`不变，就被判定为可以共享热更新之前的数据。\n *\n * 必须严格控制count在组件内的自增次数，否则在第一次修改model的name时，总是会报错：\n * Warning: Cannot update a component (`XXX`) while rendering a different component (`XXX`)\n */\nconst useDevName = (model: Model, count: number, err: Error) => {\n  const componentName = useMemo((): string => {\n    try {\n      const stacks = err.stack!.split('\\n');\n      const innerNamePattern = new RegExp(\n        // vitest测试框架的stack增加了 Module.\n        `at\\\\s(?:Module\\\\.)?${useIsolate.name}\\\\s\\\\(`,\n        'i',\n      );\n      const componentNamePattern = /at\\s(.+?)\\s\\(/i;\n      for (let i = 0; i < stacks.length; ++i) {\n        if (innerNamePattern.test(stacks[i]!)) {\n          return stacks[i + 1]!.match(componentNamePattern)![1]!;\n        }\n      }\n    } catch {}\n    return 'Component';\n  }, [err.stack]);\n\n  /**\n   * 模型文件重新保存时组件会导入新的对象，需根据这个特性重新克隆模型\n   */\n  const globalModelRef = useRef<{ model?: Model; count: number }>({ count: 0 });\n  useEffect(() => {\n    if (globalModelRef.current.model !== model) {\n      globalModelRef.current = { model, count: ++globalModelRef.current.count };\n    }\n  });\n\n  const uniqueName = `@isolate:${model.name}:${componentName}#${count}-${\n    globalModelRef.current.count +\n    Number(globalModelRef.current.model !== model)\n  }`;\n\n  /**\n   * 计算热更次数，如果停止热更，说明组件被卸载\n   */\n  useMemo(() => {\n    hotReloadCounter[uniqueName] ||= 0;\n    ++hotReloadCounter[uniqueName];\n  }, [uniqueName]);\n\n  /**\n   * 热更新时会重新执行一次useEffect\n   * setTimeout可以让其他useEffect有充分的时间使用model\n   *\n   * 需要卸载模型的场景是：\n   * 1. 组件hooks增减或者调换顺序（initialCount会自增）\n   * 2. 组件卸载\n   * 3. model.name变更\n   * 4. model逻辑变更\n   */\n  useEffect(() => {\n    const prev = hotReloadCounter[uniqueName];\n    return () => {\n      setTimeout(() => {\n        const unmounted = prev === hotReloadCounter[uniqueName];\n        unmounted && unmountModel(uniqueName);\n      });\n    };\n  }, [uniqueName]);\n\n  return uniqueName;\n};\n\nconst unmountModel = (modelName: string) => {\n  modelStore['removeReducer'](modelName);\n  loadingStore.dispatch<DestroyLoadingAction>({\n    type: DESTROY_LOADING,\n    model: modelName,\n  });\n};\n"
  },
  {
    "path": "src/api/use-loading.ts",
    "content": "import { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';\nimport { FindLoading } from '../store/loading-store';\nimport { useLoadingSelector } from '../redux/use-selector';\nimport { getLoading } from './get-loading';\n\n/**\n * 检测给定的effect方法中是否有正在执行的。支持多个方法同时传入。\n *\n * ```typescript\n * loading = useLoading(effect);\n * loading = useLoading(effect1, effect2, ...);\n * ```\n *\n */\nexport function useLoading(\n  effect: PromiseEffect,\n  ...more: PromiseEffect[]\n): boolean;\n\n/**\n * 检测给定的effect方法是否正在执行。\n *\n * ```typescript\n * loadings = useLoading(effect.room);\n * loading = loadings.find(CATEGORY);\n * ```\n */\nexport function useLoading(effect: PromiseRoomEffect): FindLoading;\n\n/**\n * 检测给定的effect方法是否正在执行。\n *\n * ```typescript\n * loading = useLoading(effect.room, CATEGORY);\n * ```\n */\nexport function useLoading(\n  effect: PromiseRoomEffect,\n  category: string | number,\n): boolean;\n\nexport function useLoading(): boolean | FindLoading {\n  const args = arguments as unknown as Parameters<typeof getLoading>;\n\n  return useLoadingSelector(() => {\n    return getLoading.apply(null, args);\n  });\n}\n"
  },
  {
    "path": "src/api/use-model.ts",
    "content": "import { shallowEqual } from 'react-redux';\nimport { deepEqual } from '../utils/deep-equal';\nimport type { Model } from '../model/types';\nimport { toArgs } from '../utils/to-args';\nimport { useModelSelector } from '../redux/use-selector';\nimport { isFunction, isString } from '../utils/is-type';\n\n/**\n * hooks新旧数据的对比方式：\n *\n * - `deepEqual`     深度对比，对比所有层级的内容。传递selector时默认使用。\n * - `shallowEqual`  浅对比，只比较对象第一层。传递多个模型但没有selector时默认使用。\n * - `strictEqual`   全等（===）对比。只传一个模型但没有selector时默认使用。\n */\nexport type Algorithm = 'strictEqual' | 'shallowEqual' | 'deepEqual';\n\n/**\n * * 获取模型的状态数据。\n * * 传入一个模型时，将返回该模型的状态。\n * * 传入多个模型时，则返回一个以模型名称为key、状态为value的大对象。\n * * 最后一个参数如果是**函数**，则为状态过滤函数，过滤函数的结果视为最终返回值。\n */\nexport function useModel<State extends object>(\n  model: Model<string, State>,\n): State;\nexport function useModel<State extends object, T>(\n  model: Model<string, State>,\n  selector: (state: State) => T,\n  algorithm?: Algorithm,\n): T;\n\nexport function useModel<\n  Name1 extends string,\n  State1 extends object,\n  Name2 extends string,\n  State2 extends object,\n>(\n  model1: Model<Name1, State1>,\n  model2: Model<Name2, State2>,\n): {\n  [K in Name1]: State1;\n} & {\n  [K in Name2]: State2;\n};\nexport function useModel<State1 extends object, State2 extends object, T>(\n  model1: Model<string, State1>,\n  model2: Model<string, State2>,\n  selector: (state1: State1, state2: State2) => T,\n  algorithm?: Algorithm,\n): T;\n\nexport function useModel<\n  Name1 extends string,\n  State1 extends object,\n  Name2 extends string,\n  State2 extends object,\n  Name3 extends string,\n  State3 extends object,\n>(\n  model1: Model<Name1, State1>,\n  model2: Model<Name2, State2>,\n  model3: Model<Name3, State3>,\n): {\n  [K in Name1]: State1;\n} & {\n  [K in Name2]: State2;\n} & {\n  [K in Name3]: State3;\n};\nexport function useModel<\n  State1 extends object,\n  State2 extends object,\n  State3 extends object,\n  T,\n>(\n  model1: Model<string, State1>,\n  model2: Model<string, State2>,\n  model3: Model<string, State3>,\n  selector: (state1: State1, state2: State2, state3: State3) => T,\n  algorithm?: Algorithm,\n): T;\n\nexport function useModel<\n  Name1 extends string,\n  State1 extends object,\n  Name2 extends string,\n  State2 extends object,\n  Name3 extends string,\n  State3 extends object,\n  Name4 extends string,\n  State4 extends object,\n>(\n  model1: Model<Name1, State1>,\n  model2: Model<Name2, State2>,\n  model3: Model<Name3, State3>,\n  model4: Model<Name4, State4>,\n): {\n  [K in Name1]: State1;\n} & {\n  [K in Name2]: State2;\n} & {\n  [K in Name3]: State3;\n} & {\n  [K in Name4]: State4;\n};\nexport function useModel<\n  State1 extends object,\n  State2 extends object,\n  State3 extends object,\n  State4 extends object,\n  T,\n>(\n  model1: Model<string, State1>,\n  model2: Model<string, State2>,\n  model3: Model<string, State3>,\n  model4: Model<string, State4>,\n  selector: (\n    state1: State1,\n    state2: State2,\n    state3: State3,\n    state4: State4,\n  ) => T,\n  algorithm?: Algorithm,\n): T;\n\nexport function useModel<\n  Name1 extends string,\n  State1 extends object,\n  Name2 extends string,\n  State2 extends object,\n  Name3 extends string,\n  State3 extends object,\n  Name4 extends string,\n  State4 extends object,\n  Name5 extends string,\n  State5 extends object,\n>(\n  model1: Model<Name1, State1>,\n  model2: Model<Name2, State2>,\n  model3: Model<Name3, State3>,\n  model4: Model<Name4, State4>,\n  model5: Model<Name5, State5>,\n): {\n  [K in Name1]: State1;\n} & {\n  [K in Name2]: State2;\n} & {\n  [K in Name3]: State3;\n} & {\n  [K in Name4]: State4;\n} & {\n  [K in Name5]: State5;\n};\nexport function useModel<\n  State1 extends object,\n  State2 extends object,\n  State3 extends object,\n  State4 extends object,\n  State5 extends object,\n  T,\n>(\n  model1: Model<string, State1>,\n  model2: Model<string, State2>,\n  model3: Model<string, State3>,\n  model4: Model<string, State4>,\n  model5: Model<string, State5>,\n  selector: (\n    state1: State1,\n    state2: State2,\n    state3: State3,\n    state4: State4,\n    state5: State5,\n  ) => T,\n  algorithm?: Algorithm,\n): T;\n\nexport function useModel(): any {\n  const args = toArgs(arguments);\n  let algorithm: Algorithm | false =\n    args.length > 1 && isString(args[args.length - 1]) && args.pop();\n  const selector: Function | false =\n    args.length > 1 && isFunction(args[args.length - 1]) && args.pop();\n  const models: Model[] = args;\n  const modelsLength = models.length;\n  const onlyOneModel = modelsLength === 1;\n\n  if (!algorithm) {\n    if (selector) {\n      // 返回子集或者计算过的内容。\n      // 如果只是从模型中获取数据且没有做转换，则大部分时间会降级为shallow或者strict。\n      // 如果对数据做了转换，则肯定需要使用深对比。\n      algorithm = 'deepEqual';\n    } else if (onlyOneModel) {\n      // 一个model属于一个reducer，reducer已经使用了深对比来判断是否变化，\n      algorithm = 'strictEqual';\n    } else {\n      // { key => model } 集合。\n      // 一个model属于一个reducer，reducer已经使用了深对比来判断是否变化，\n      algorithm = 'shallowEqual';\n    }\n  }\n\n  // 储存了结果说明是state状态变化导致的对比计算。\n  // 因为存在闭包，除模型外的所有参数都是旧的，\n  // 所以我们只需要保证用到的模型数据不变即可，这样可以减少无意义的计算。\n  let hasMemo = false,\n    snapshot: any,\n    prevState: Record<string, object>,\n    currentStates: object[],\n    i: number,\n    changed: boolean;\n\n  const reducerNames: string[] = [];\n  for (i = 0; i < modelsLength; ++i) {\n    reducerNames.push(models[i]!.name);\n  }\n\n  return useModelSelector((state: Record<string, object>) => {\n    if (hasMemo) {\n      changed = false;\n      for (i = modelsLength; i-- > 0; ) {\n        const reducerName = reducerNames[i]!;\n        if (state[reducerName] !== prevState[reducerName]) {\n          changed = true;\n          break;\n        }\n      }\n\n      if (!changed) {\n        prevState = state;\n        return snapshot;\n      }\n    }\n\n    prevState = state;\n    hasMemo = true;\n\n    if (onlyOneModel) {\n      const firstState = state[reducerNames[0]!];\n      return (snapshot = selector ? selector(firstState) : firstState);\n    }\n\n    if (selector) {\n      currentStates = [];\n      for (i = modelsLength; i-- > 0; ) {\n        currentStates[i] = state[reducerNames[i]!]!;\n      }\n      return (snapshot = selector.apply(null, currentStates));\n    }\n\n    snapshot = {};\n    for (i = modelsLength; i-- > 0; ) {\n      const reducerName = reducerNames[i]!;\n      snapshot[reducerName] = state[reducerName];\n    }\n    return snapshot;\n  }, compareFn[algorithm]);\n}\n\nconst compareFn: Record<\n  Algorithm,\n  undefined | ((previous: any, next: any) => boolean)\n> = {\n  deepEqual: deepEqual,\n  shallowEqual: shallowEqual,\n  strictEqual: void 0,\n};\n"
  },
  {
    "path": "src/engines/memory.ts",
    "content": "import type { StorageEngine } from './storage-engine';\n\nlet cache: Partial<Record<string, string>> = {};\n\nexport const memoryStorage: StorageEngine = {\n  getItem(key) {\n    return cache[key] === void 0 ? null : cache[key]!;\n  },\n  setItem(key, value) {\n    cache[key] = value;\n  },\n  removeItem(key) {\n    cache[key] = void 0;\n  },\n  clear() {\n    cache = {};\n  },\n};\n"
  },
  {
    "path": "src/engines/storage-engine.ts",
    "content": "export interface StorageEngine {\n  getItem(key: string): string | null | Promise<string | null>;\n  setItem(key: string, value: string): any;\n  removeItem(key: string): any;\n  clear(): any;\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "// 模型中使用\nexport { defineModel } from './model/define-model';\nexport { cloneModel } from './model/clone-model';\n\n// 组件中使用\nexport { useModel } from './api/use-model';\nexport { useLoading } from './api/use-loading';\nexport { getLoading } from './api/get-loading';\nexport { useComputed } from './api/use-computed';\nexport { useIsolate } from './api/use-isolate';\nexport { connect } from './redux/connect';\n\n// 入口使用\nexport { compose } from 'redux';\nexport { modelStore as store } from './store/model-store';\nexport { FocaProvider } from './redux/foca-provider';\nexport { memoryStorage } from './engines/memory';\n\n// 可能用到的TS类型\nexport type {\n  Action,\n  UnknownAction,\n  Dispatch,\n  MiddlewareAPI,\n  Middleware,\n  StoreEnhancer,\n  Unsubscribe,\n} from 'redux';\nexport type { Model } from './model/types';\nexport type { StorageEngine } from './engines/storage-engine';\n"
  },
  {
    "path": "src/middleware/action-in-action.interceptor.ts",
    "content": "import type { Middleware, UnknownAction } from 'redux';\nimport { isPreModelAction } from '../actions/model';\n\n// 开发者有可能在action中执行action，这是十分不规范的操作。\nexport const actionInActionInterceptor: Middleware = () => {\n  let dispatching = false;\n  let prevAction: UnknownAction | null = null;\n\n  return (dispatch) => (action) => {\n    if (!isPreModelAction(action)) {\n      // 非model的action会直接进入redux，redux中已经有dispatch保护机制。\n      return dispatch(action);\n    }\n\n    // model的action如果没有变化则不会进入redux，所以需要在这里额外保护。\n    if (dispatching) {\n      throw new Error(\n        '[dispatch] 派发任务冲突，请检查是否在reducers函数中直接或者间接执行了其他reducers或者methods函数。\\nreducers的唯一职责是更新当前的state，有额外的业务逻辑时请把methods作为执行入口并按需调用reducers。\\n\\n当前冲突的reducer：\\n\\n' +\n          JSON.stringify(action, null, 4) +\n          '\\n\\n上次执行未完成的reducer：\\n\\n' +\n          JSON.stringify(prevAction, null, 4) +\n          '\\n\\n',\n      );\n    }\n\n    try {\n      dispatching = true;\n      prevAction = action;\n      /**\n       * react-redux@8+ 主要服务于react18\n       * 在react17中，有可能出现redux遍历subscriber时立即触发dispatch，然后这边来不及设置dispatching=false\n       * 在react18中，如果使用`ReactDOM.render()`旧入口，则依旧会有这个问题。\n       * @link https://github.com/foca-js/foca/issues/20\n       */\n      action.actionInActionGuard = () => {\n        dispatching = false;\n        prevAction = null;\n      };\n      return dispatch(action);\n    } catch (e) {\n      prevAction = null;\n      dispatching = false;\n      throw e;\n    }\n  };\n};\n"
  },
  {
    "path": "src/middleware/destroy-loading.interceptor.ts",
    "content": "import type { Middleware } from 'redux';\nimport { isDestroyLoadingAction } from '../actions/loading';\n\nexport const destroyLoadingInterceptor: Middleware =\n  (api) => (dispatch) => (action) => {\n    if (\n      !isDestroyLoadingAction(action) ||\n      api.getState().hasOwnProperty(action.model)\n    ) {\n      return dispatch(action);\n    }\n\n    return action;\n  };\n"
  },
  {
    "path": "src/middleware/freeze-state.middleware.ts",
    "content": "import { freeze } from 'immer';\nimport type { Middleware } from 'redux';\n\nexport const freezeStateMiddleware: Middleware = (api) => {\n  freeze(api.getState(), true);\n\n  return (dispatch) => (action) => {\n    try {\n      return dispatch(action);\n    } finally {\n      freeze(api.getState(), true);\n    }\n  };\n};\n"
  },
  {
    "path": "src/middleware/loading.interceptor.ts",
    "content": "import type { Middleware } from 'redux';\nimport type { LoadingStore, LoadingStoreState } from '../store/loading-store';\nimport { isLoadingAction } from '../actions/loading';\n\nexport const loadingInterceptor = (\n  loadingStore: LoadingStore,\n): Middleware<{}, LoadingStoreState> => {\n  return () => (dispatch) => (action) => {\n    if (!isLoadingAction(action)) {\n      return dispatch(action);\n    }\n\n    const {\n      model,\n      method,\n      payload: { category, loading },\n    } = action;\n\n    if (loadingStore.isModelInitializing(model)) {\n      loadingStore.activate(model, method);\n    } else if (!loadingStore.isActive(model, method)) {\n      return;\n    }\n\n    const record = loadingStore.getItem(model, method);\n\n    if (!record || record.loadings.data[category] !== loading) {\n      return dispatch(action);\n    }\n\n    return action;\n  };\n};\n"
  },
  {
    "path": "src/middleware/model.interceptor.ts",
    "content": "import type { Middleware } from 'redux';\nimport { deepEqual } from '../utils/deep-equal';\nimport { isPreModelAction, PostModelAction } from '../actions/model';\nimport { immer } from '../utils/immer';\n\nexport const modelInterceptor: Middleware<{}, Record<string, object>> =\n  (api) => (dispatch) => (action) => {\n    if (!isPreModelAction(action)) {\n      return dispatch(action);\n    }\n\n    const prev = api.getState()[action.model]!;\n    const next = immer.produce(prev, (draft) => {\n      return action.consumer(draft, action);\n    });\n\n    action.actionInActionGuard && action.actionInActionGuard();\n\n    if (deepEqual(prev, next)) return action;\n\n    return dispatch({\n      type: action.type,\n      model: action.model,\n      postModel: true,\n      next: next,\n    } satisfies PostModelAction);\n  };\n"
  },
  {
    "path": "src/model/clone-model.ts",
    "content": "import { isFunction } from '../utils/is-type';\nimport { defineModel } from './define-model';\nimport type { DefineModelOptions, InternalModel, Model } from './types';\n\nconst editableKeys = [\n  'initialState',\n  'events',\n  'persist',\n  'skipRefresh',\n] as const;\n\ntype EditableKeys = (typeof editableKeys)[number];\n\ntype OverrideOptions<\n  State extends object,\n  Action extends object,\n  Effect extends object,\n  Computed extends object,\n  PersistDump,\n> = Pick<\n  DefineModelOptions<State, Action, Effect, Computed, PersistDump>,\n  EditableKeys\n>;\n\nexport const cloneModel = <\n  Name extends string,\n  State extends object,\n  Action extends object,\n  Effect extends object,\n  Computed extends object,\n  PersistDump,\n>(\n  uniqueName: Name,\n  model: Model<string, State, Action, Effect, Computed>,\n  options?:\n    | Partial<OverrideOptions<State, Action, Effect, Computed, PersistDump>>\n    | ((\n        prev: OverrideOptions<State, Action, Effect, Computed, PersistDump>,\n      ) => Partial<\n        OverrideOptions<State, Action, Effect, Computed, PersistDump>\n      >),\n): Model<Name, State, Action, Effect, Computed> => {\n  const realModel = model as unknown as InternalModel<\n    string,\n    State,\n    Action,\n    Effect,\n    Computed\n  >;\n\n  const prevOpts = realModel._$opts;\n  const nextOpts = Object.assign({}, prevOpts);\n\n  if (options) {\n    Object.assign(nextOpts, isFunction(options) ? options(nextOpts) : options);\n\n    /* istanbul ignore else -- @preserve  */\n    if (process.env.NODE_ENV !== 'production') {\n      (Object.keys(nextOpts) as EditableKeys[]).forEach((key) => {\n        if (\n          nextOpts[key] !== prevOpts[key] &&\n          editableKeys.indexOf(key) === -1\n        ) {\n          throw new Error(\n            `[model:${uniqueName}] 复制模型时禁止重写属性'${key}'`,\n          );\n        }\n      });\n    }\n  }\n\n  return defineModel(uniqueName, nextOpts);\n};\n"
  },
  {
    "path": "src/model/define-model.ts",
    "content": "import { parseState, stringifyState } from '../utils/serialize';\nimport { deepEqual } from '../utils/deep-equal';\nimport { EnhancedAction, enhanceAction } from './enhance-action';\nimport { EnhancedEffect, enhanceEffect } from './enhance-effect';\nimport { modelStore } from '../store/model-store';\nimport { createReducer } from '../redux/create-reducer';\nimport { composeGetter, defineGetter } from '../utils/getter';\nimport { getMethodCategory } from '../utils/get-method-category';\nimport { guard } from './guard';\nimport { depsCollector } from '../reactive/deps-collector';\nimport { ObjectDeps } from '../reactive/object-deps';\nimport type {\n  ActionCtx,\n  EffectCtx,\n  DefineModelOptions,\n  GetInitialState,\n  GetState,\n  Model,\n  ComputedCtx,\n  EventCtx,\n  InternalModel,\n  SetStateCallback,\n  ComputedFlag,\n} from './types';\nimport { isFunction } from '../utils/is-type';\nimport { Unsubscribe } from 'redux';\nimport { freeze, original, isDraft } from 'immer';\nimport { isPromise } from '../utils/is-promise';\nimport { enhanceComputed } from './enhance-computed';\n\nexport const defineModel = <\n  Name extends string,\n  State extends object,\n  Action extends object,\n  Effect extends object,\n  Computed extends object,\n  PersistDump,\n>(\n  uniqueName: Name,\n  options: DefineModelOptions<State, Action, Effect, Computed, PersistDump>,\n): Model<Name, State, Action, Effect, Computed> => {\n  guard(uniqueName);\n\n  const { reducers, methods, computed, skipRefresh, events } = options;\n  /**\n   * 防止初始化数据在外面被修改从而影响到store，\n   * 这属于小概率事件，所以仅需要在开发环境处理，\n   * 而且在严格模式下，runtime修改冻结数据会直接报错，可以提醒开发者修正\n   */\n  const initialState =\n    process.env.NODE_ENV !== 'production'\n      ? freeze(options.initialState, true)\n      : options.initialState;\n\n  /* istanbul ignore else -- @preserve  */\n  if (process.env.NODE_ENV !== 'production') {\n    const items = [\n      { name: 'reducers', value: reducers },\n      { name: 'methods', value: methods },\n      { name: 'computed', value: computed },\n    ];\n    const validateUniqueMethod = (index1: number, index2: number) => {\n      const item1 = items[index1]!;\n      const item2 = items[index2]!;\n      if (item1.value && item2.value) {\n        Object.keys(item1.value).forEach((key) => {\n          if (item2.value!.hasOwnProperty(key)) {\n            throw new Error(\n              `[model:${uniqueName}] 属性'${key}'在${item1.name}和${item2.name}中重复使用`,\n            );\n          }\n        });\n      }\n    };\n    validateUniqueMethod(0, 1);\n    validateUniqueMethod(0, 2);\n    validateUniqueMethod(1, 2);\n  }\n\n  /* istanbul ignore else -- @preserve  */\n  if (process.env.NODE_ENV !== 'production') {\n    if (!deepEqual(parseState(stringifyState(initialState)), initialState)) {\n      throw new Error(\n        `[model:${uniqueName}] initialState 包含了不可系列化的数据，允许的类型为：Object, Array, Number, String, Undefined 和 Null`,\n      );\n    }\n  }\n\n  const getState = <T extends object>(obj: T): T & GetState<State> => {\n    return defineGetter(obj, 'state', () => {\n      const state = modelStore.getState()[uniqueName];\n      return depsCollector.active\n        ? new ObjectDeps(modelStore, uniqueName).start(state)\n        : state;\n    });\n  };\n\n  const getInitialState = <T extends object>(\n    obj: T,\n  ): T & GetInitialState<State> => {\n    return defineGetter(obj, 'initialState', () =>\n      parseState(stringifyState(initialState)),\n    );\n  };\n\n  const actionCtx: ActionCtx<State> = composeGetter(\n    {\n      name: uniqueName,\n    },\n    getInitialState,\n  );\n\n  const createEffectCtx = (methodName: string): EffectCtx<State> => {\n    const isArrayState = Array.isArray(initialState);\n    const obj: Pick<EffectCtx<State>, 'setState'> = {\n      // @ts-expect-error\n      setState: enhanceAction(\n        actionCtx,\n        `${methodName}.setState`,\n        <K extends keyof State>(\n          state: State,\n          fn_state: SetStateCallback<State, K> | State | Pick<State, K>,\n        ) => {\n          const nextState = isFunction<SetStateCallback<State, K>>(fn_state)\n            ? fn_state(state)\n            : fn_state;\n\n          if (nextState === void 0) return;\n\n          return isArrayState || isDraft(nextState)\n            ? nextState\n            : Object.assign({}, original(state), nextState);\n        },\n      ),\n    };\n    return composeGetter(\n      Object.assign(obj, { name: uniqueName }),\n      getState,\n      getInitialState,\n    );\n  };\n\n  const enhancedMethods: {\n    [K in ReturnType<typeof getMethodCategory>]: Record<\n      string,\n      EnhancedAction<State> | EnhancedEffect | ComputedFlag\n    >;\n  } = {\n    external: {},\n    internal: {},\n  };\n\n  if (reducers) {\n    const reducerKeys = Object.keys(reducers);\n    for (let i = reducerKeys.length; i-- > 0; ) {\n      const key = reducerKeys[i]!;\n      enhancedMethods[getMethodCategory(key)][key] = enhanceAction(\n        actionCtx,\n        key,\n        reducers[key]!,\n      );\n    }\n  }\n\n  if (computed) {\n    const computedCtx: ComputedCtx<State> & {\n      [K in string]?: ComputedFlag;\n    } = composeGetter({ name: uniqueName }, getState);\n    const computedKeys = Object.keys(computed);\n\n    for (let i = computedKeys.length; i-- > 0; ) {\n      const key = computedKeys[i]!;\n      computedCtx[key] = enhancedMethods[getMethodCategory(key)][key] =\n        enhanceComputed(\n          computedCtx,\n          uniqueName,\n          key,\n          // @ts-expect-error\n          computed[key],\n        );\n    }\n  }\n\n  if (methods) {\n    let ctx: EffectCtx<State>;\n    const ctxs: EffectCtx<State>[] = [(ctx = createEffectCtx(''))];\n    const methodKeys = Object.keys(methods);\n\n    for (let i = methodKeys.length; i-- > 0; ) {\n      const key = methodKeys[i]!;\n      if (process.env.NODE_ENV !== 'production') {\n        ctxs.push((ctx = createEffectCtx(key)));\n      }\n\n      enhancedMethods[getMethodCategory(key)][key] = enhanceEffect(\n        ctx,\n        key,\n        // @ts-expect-error\n        methods[key],\n      );\n    }\n\n    for (let i = ctxs.length; i-- > 0; ) {\n      Object.assign(\n        ctxs[i]!,\n        enhancedMethods.external,\n        enhancedMethods.internal,\n      );\n    }\n  }\n\n  if (events) {\n    const { onInit, onChange, onDestroy } = events;\n    const eventCtx: EventCtx<State> = Object.assign(\n      composeGetter({ name: uniqueName }, getState),\n      enhancedMethods.external,\n      enhancedMethods.internal,\n    );\n\n    modelStore.onInitialized().then(() => {\n      const subscriptions: Unsubscribe[] = [];\n\n      if (onChange) {\n        let prevState = eventCtx.state;\n        subscriptions.push(\n          modelStore.subscribe(() => {\n            const nextState = eventCtx.state;\n            if (\n              modelStore.isReady &&\n              prevState !== nextState &&\n              nextState !== void 0\n            ) {\n              onChange.call(eventCtx, prevState, nextState);\n            }\n            prevState = nextState;\n          }),\n        );\n      }\n\n      if (onDestroy) {\n        subscriptions.push(\n          modelStore.subscribe(() => {\n            if (eventCtx.state === void 0) {\n              for (let i = 0; i < subscriptions.length; ++i) {\n                subscriptions[i]!();\n              }\n              onDestroy.call(null as never, uniqueName);\n            }\n          }),\n        );\n      }\n\n      if (onInit) {\n        /**\n         * 初始化时，用到它的React组件可能还没加载，所以执行async-method时无法判断是否需要保存loading。因此需要一个钩子来处理事件周期\n         * @see https://github.com/foca-js/foca/issues/38\n         */\n        modelStore.topic.publish('modelPreInit', uniqueName);\n        const promiseOrVoid = onInit.call(eventCtx);\n        const postInit = () => {\n          modelStore.topic.publish('modelPostInit', uniqueName);\n        };\n        if (isPromise(promiseOrVoid)) {\n          promiseOrVoid.then(postInit, postInit);\n        } else {\n          postInit();\n        }\n      }\n    });\n  }\n\n  modelStore['appendReducer'](\n    uniqueName,\n    createReducer({\n      name: uniqueName,\n      initialState,\n      allowRefresh: !skipRefresh,\n    }),\n  );\n\n  const model: InternalModel<Name, State, Action, Effect, Computed> =\n    Object.assign(\n      composeGetter(\n        {\n          name: uniqueName,\n          _$opts: options,\n          _$persistCtx: getInitialState({}),\n        },\n        getState,\n      ),\n      enhancedMethods.external,\n    );\n\n  return model as any;\n};\n"
  },
  {
    "path": "src/model/enhance-action.ts",
    "content": "import type { PreModelAction } from '../actions/model';\nimport { modelStore } from '../store/model-store';\nimport { toArgs } from '../utils/to-args';\nimport type { ActionCtx } from './types';\n\nexport interface EnhancedAction<State extends object> {\n  (payload: any): PreModelAction<State>;\n}\n\nexport const enhanceAction = <State extends object>(\n  ctx: ActionCtx<State>,\n  actionName: string,\n  consumer: (state: State, ...args: any[]) => any,\n): EnhancedAction<State> => {\n  const modelName = ctx.name;\n  const actionType = modelName + '.' + actionName;\n\n  const enhancedConsumer: PreModelAction<State, any[]>['consumer'] = (\n    state,\n    action,\n  ) => {\n    return consumer.apply(\n      ctx,\n      [state].concat(action.payload) as [state: State, ...args: any[]],\n    );\n  };\n\n  const fn: EnhancedAction<State> = function () {\n    return modelStore.dispatch<PreModelAction<State, any[]>>({\n      type: actionType,\n      model: modelName,\n      preModel: true,\n      payload: toArgs(arguments),\n      consumer: enhancedConsumer,\n    });\n  };\n\n  return fn;\n};\n"
  },
  {
    "path": "src/model/enhance-computed.ts",
    "content": "import { ComputedValue } from '../reactive/computed-value';\nimport { modelStore } from '../store/model-store';\nimport { toArgs } from '../utils/to-args';\nimport { ComputedCtx, ComputedFlag } from './types';\n\nexport const enhanceComputed = <State extends object>(\n  ctx: ComputedCtx<State>,\n  modelName: string,\n  computedName: string,\n  fn: (...args: any[]) => any,\n): ComputedFlag => {\n  let caches: {\n    deps: any[];\n    skipCount: number;\n    ref: ComputedValue;\n  }[] = [];\n\n  function anonymousFn() {\n    const args = toArgs(arguments);\n    let hitCache: (typeof caches)[number] | undefined;\n\n    searchCache: for (let i = 0; i < caches.length; ++i) {\n      const cache = caches[i]!;\n      if (hitCache) {\n        ++cache.skipCount;\n        continue;\n      }\n      for (let j = 0; j < cache.deps.length; ++j) {\n        if (args[j] !== cache.deps[j]) {\n          ++cache.skipCount;\n          continue searchCache;\n        }\n      }\n      cache.skipCount = 0;\n      hitCache = cache;\n    }\n\n    if (hitCache) return hitCache.ref.value;\n\n    if (caches.length > 10) {\n      caches = caches.filter((cache) => cache.skipCount < 15);\n    }\n\n    hitCache = {\n      deps: args,\n      skipCount: 0,\n      ref: new ComputedValue(modelStore, modelName, computedName, () =>\n        fn.apply(ctx, args),\n      ),\n    };\n    caches.push(hitCache);\n    return hitCache.ref.value;\n  }\n\n  return anonymousFn as any;\n};\n"
  },
  {
    "path": "src/model/enhance-effect.ts",
    "content": "import {\n  LoadingAction,\n  LOADING_CATEGORY,\n  TYPE_SET_LOADING,\n} from '../actions/loading';\nimport type { EffectCtx } from './types';\nimport { isPromise } from '../utils/is-promise';\nimport { toArgs } from '../utils/to-args';\nimport { loadingStore } from '../store/loading-store';\n\ninterface RoomFunc<P extends any[] = any[], R = Promise<any>> {\n  (category: number | string): {\n    execute(...args: P): R;\n  };\n}\n\ninterface AsyncRoomEffect<P extends any[] = any[], R = Promise<any>>\n  extends RoomFunc<P, R> {\n  readonly _: {\n    readonly model: string;\n    readonly method: string;\n    readonly hasRoom: true;\n  };\n}\n\ninterface AsyncEffect<P extends any[] = any[], R = Promise<any>>\n  extends EffectFunc<P, R> {\n  readonly _: {\n    readonly model: string;\n    readonly method: string;\n    readonly hasRoom: '';\n  };\n  /**\n   * 对同一effect函数的执行状态进行分类以实现独立保存。好处有：\n   *\n   * 1. 并发请求同一个请求时不会互相覆盖执行状态。\n   * <br>\n   * 2. 可以精确地判断业务中是哪个控件或者逻辑正在执行。\n   *\n   * ```typescript\n   * model.effect.room(CATEGORY).execute(...);\n   * ```\n   *\n   * @see useLoading(effect.room)\n   * @see getLoading(effect.room)\n   * @since 0.11.4\n   *\n   */\n  readonly room: AsyncRoomEffect<P, R>;\n}\n\nexport type PromiseEffect = AsyncEffect;\nexport type PromiseRoomEffect = AsyncRoomEffect;\n\ninterface EffectFunc<P extends any[] = any[], R = Promise<any>> {\n  (...args: P): R;\n}\n\nexport type EnhancedEffect<P extends any[] = any[], R = Promise<any>> =\n  R extends Promise<any> ? AsyncEffect<P, R> : EffectFunc<P, R>;\n\ntype NonReadonly<T extends object> = {\n  -readonly [K in keyof T]: T[K];\n};\n\nexport const enhanceEffect = <State extends object>(\n  ctx: EffectCtx<State>,\n  methodName: string,\n  effect: (...args: any[]) => any,\n): EnhancedEffect => {\n  const fn: NonReadonly<EnhancedEffect> & EffectFunc = function () {\n    return execute(ctx, methodName, effect, toArgs(arguments));\n  };\n\n  fn._ = {\n    model: ctx.name,\n    method: methodName,\n    hasRoom: '',\n  };\n\n  const room: NonReadonly<AsyncRoomEffect> & RoomFunc = (\n    category: number | string,\n  ) => ({\n    execute() {\n      return execute(ctx, methodName, effect, toArgs(arguments), category);\n    },\n  });\n\n  room._ = Object.assign({}, fn._, {\n    hasRoom: true as const,\n  });\n\n  fn.room = room;\n\n  return fn;\n};\n\nconst dispatchLoading = (\n  modelName: string,\n  methodName: string,\n  loading: boolean,\n  category?: number | string,\n) => {\n  loadingStore.dispatch<LoadingAction>({\n    type: TYPE_SET_LOADING,\n    model: modelName,\n    method: methodName,\n    payload: {\n      category: category === void 0 ? LOADING_CATEGORY : category,\n      loading,\n    },\n  });\n};\n\nconst execute = <State extends object>(\n  ctx: EffectCtx<State>,\n  methodName: string,\n  effect: (...args: any[]) => any,\n  args: any[],\n  category?: number | string,\n) => {\n  const modelName = ctx.name;\n  const resultOrPromise = effect.apply(ctx, args);\n\n  if (!isPromise(resultOrPromise)) return resultOrPromise;\n\n  dispatchLoading(modelName, methodName, true, category);\n\n  return resultOrPromise.then(\n    (result) => {\n      return dispatchLoading(modelName, methodName, false, category), result;\n    },\n    (e: unknown) => {\n      dispatchLoading(modelName, methodName, false, category);\n      throw e;\n    },\n  );\n};\n"
  },
  {
    "path": "src/model/guard.ts",
    "content": "const counter: Record<string, number> = {};\n\nexport const guard = (modelName: string) => {\n  counter[modelName] ||= 0;\n\n  if (process.env.NODE_ENV !== 'production') {\n    setTimeout(() => {\n      --counter[modelName]!;\n    });\n  }\n\n  if (++counter[modelName] > 1) {\n    throw new Error(`模型名称'${modelName}'被重复使用`);\n  }\n};\n"
  },
  {
    "path": "src/model/types.ts",
    "content": "import type { UnknownAction } from 'redux';\nimport type { EnhancedEffect } from './enhance-effect';\nimport type { PersistMergeMode } from '../persist/persist-item';\n\nexport interface ComputedFlag {\n  readonly _computedFlag: never;\n}\n\nexport interface GetName<Name extends string> {\n  /**\n   * 模型名称。请在定义模型时确保是唯一的字符串\n   */\n  readonly name: Name;\n}\n\nexport interface GetState<State extends object> {\n  /**\n   * 模型的实时状态\n   */\n  readonly state: State;\n}\n\nexport interface GetInitialState<State extends object> {\n  /**\n   * 模型的初始状态，每次获取该属性都会执行深拷贝操作\n   */\n  readonly initialState: State;\n}\n\nexport type ModelPersist<State extends object, PersistDump> = {\n  /**\n   * 持久化版本号，数据结构变化后建议立即升级该版本。默认值：`0`\n   */\n  version?: number | string;\n\n  /**\n   * 持久化数据与初始数据的合并方式。默认值以全局配置为准\n   *\n   * - replace - 覆盖模式。直接用持久化数据替换初始数据\n   * - merge - 合并模式。持久化数据与初始数据新增的key进行合并，可理解为`Object.assign`\n   * - deep-merge - 二级合并模式。在合并模式的基础上，如果某个key的值为对象，则该对象也会执行合并操作\n   *\n   * 注意：当数据为数组格式时该配置无效。\n   * @since 3.0.0\n   */\n  merge?: PersistMergeMode;\n} & (\n  | {\n      /**\n       * 模型数据从内存存储到持久化引擎时的过滤函数，允许你只持久化部分数据。\n       * ```typescript\n       *\n       * // state = { firstName: 'tick', lastName: 'tock' }\n       * dump: (state) => state\n       * dump: (state) => state.firstName\n       * dump: (state) => ({ name: state.lastName })\n       * ```\n       *\n       * @since 3.0.0\n       */\n      dump: (state: State) => PersistDump;\n      /**\n       * 持久化数据恢复到模型内存时的过滤函数，参数为`dump`返回的值。\n       * ```typescript\n       * // state = { firstName: 'tick', lastName: 'tock' }\n       * {\n       *   dump(state) {\n       *     return state.firstName\n       *   },\n       *   load(firstName) {\n       *     return { ...this.initialState, firstName: firstName };\n       *   }\n       * }\n       * ```\n       *\n       * @since 3.0.0\n       */\n      load: (this: GetInitialState<State>, dumpData: PersistDump) => State;\n    }\n  | {\n      dump?: never;\n      load?: never;\n    }\n);\n\nexport interface ActionCtx<State extends object>\n  extends GetName<string>,\n    GetInitialState<State> {}\n\nexport interface EffectCtx<State extends object>\n  extends ActionCtx<State>,\n    GetState<State> {\n  /**\n   * 立即更改状态，支持**immer**操作\n   *\n   * ```typescript\n   * this.setState((state) => {\n   *   state.count += 1;\n   * });\n   * ```\n   *\n   * 对于object类型，你可以直接传递 **全部** 或者 **部分** 数据\n   * ```typescript\n   * interface State { id: number; name: string };\n   *\n   * this.setState({}); // 什么也没修改\n   * this.setState({ id: 10 }); // 只修改id\n   * this.setState({ id: 10, name: 'foo' }); // 修改全部\n   *\n   * this.setState((state) => {\n   *   return {}; // 什么也没修改\n   * });\n   * this.setState((state) => {\n   *   return { id: 10 }; // 只修改id\n   * });\n   * this.setState((state) => {\n   *   return { id: 10, name: 'foo' }; // 修改全部\n   * });\n   * ```\n   *\n   * 对于array类型，直接传递数组就行了\n   * ```typescript\n   * this.setState(['a', 'b', 'c']);\n   * ```\n   */\n  readonly setState: State extends any[]\n    ? (state: State | ((state: State) => State | void)) => UnknownAction\n    : <K extends keyof State>(\n        state: SetStateCallback<State, K> | (Pick<State, K> | State),\n      ) => UnknownAction;\n}\n\nexport interface SetStateCallback<State extends object, K extends keyof State> {\n  (state: State): Pick<State, K> | State | void;\n}\n\nexport interface ComputedCtx<State extends object>\n  extends GetName<string>,\n    GetState<State> {}\n\nexport interface BaseModel<Name extends string, State extends object>\n  extends GetState<State>,\n    GetName<Name> {}\n\ntype ModelActionItem<\n  State extends object,\n  Action extends object,\n  K extends keyof Action,\n> = Action[K] extends (state: State, ...args: infer P) => State | void\n  ? (...args: P) => UnknownAction\n  : never;\n\ntype ModelAction<State extends object, Action extends object> = {\n  readonly [K in keyof Action]: ModelActionItem<State, Action, K>;\n};\n\ntype GetPrivateMethodKeys<Method extends object> = {\n  [K in keyof Method]: K extends `_${string}` ? K : never;\n}[keyof Method];\n\ntype ModelEffect<Effect extends object> = {\n  readonly [K in keyof Effect]: Effect[K] extends (...args: infer P) => infer R\n    ? EnhancedEffect<P, R>\n    : never;\n};\n\ntype ModelComputed<Computed extends object> = {\n  readonly [K in keyof Computed]: Computed[K] & ComputedFlag;\n};\n\nexport type Model<\n  Name extends string = string,\n  State extends object = object,\n  Action extends object = object,\n  Effect extends object = object,\n  Computed extends object = object,\n> = BaseModel<Name, State> &\n  // [K in keyof Action as K extends `_${string}` ? never : K]\n  // 上面这种看起来简洁，业务代码提示也正常，但是业务代码那边无法点击跳转进模型了。\n  // 所以需要先转换所有的属性，再把私有属性去除。\n  Omit<ModelAction<State, Action>, GetPrivateMethodKeys<Action>> &\n  Omit<ModelEffect<Effect>, GetPrivateMethodKeys<Effect>> &\n  Omit<ModelComputed<Computed>, GetPrivateMethodKeys<Computed>>;\n\nexport type InternalModel<\n  Name extends string = string,\n  State extends object = object,\n  Action extends object = object,\n  Effect extends object = object,\n  Computed extends object = object,\n> = BaseModel<Name, State> & {\n  readonly _$opts: DefineModelOptions<State, Action, Effect, Computed, any>;\n  readonly _$persistCtx: GetInitialState<State>;\n};\n\nexport type InternalAction<State extends object> = {\n  [key: string]: (state: State, ...args: any[]) => State | void;\n};\n\nexport interface Event<State> {\n  /**\n   * store初始化完成，并且持久化（如果有）的数据也已经恢复。\n   *\n   * 上下文 **this** 可以直接调用actions和effects的函数以及computed计算属性。\n   */\n  onInit?: () => void;\n  /**\n   * 每当state有变化时的回调通知。\n   *\n   * 初始化(onInit)执行之前不会触发该回调。如果在onInit中做了修改state的操作，则会触发该回调。\n   *\n   * 上下文 **this** 可以直接调用actions和effects的函数以及computed计算属性，请谨慎执行修改数据的操作以防止死循环。\n   */\n  onChange?: (prevState: State, nextState: State) => void;\n  /**\n   * 销毁模型时的回调通知，此时模型已经被销毁。\n   * 该事件仅在局部模型生效\n   * @see useIsolate\n   */\n  onDestroy?: (this: never, modelName: string) => void;\n}\n\nexport interface EventCtx<State extends object>\n  extends GetName<string>,\n    GetState<State> {}\n\nexport interface DefineModelOptions<\n  State extends object,\n  Action extends object,\n  Effect extends object,\n  Computed extends object,\n  PersistDump,\n> {\n  /**\n   * 初始状态\n   *\n   * ```typescript\n   * cosnt initialState: {\n   *   count: number;\n   * } = {\n   *   count: 0,\n   * }\n   *\n   * const model = defineModel('model1', {\n   *   initialState\n   * });\n   * ```\n   */\n  initialState: State;\n  /**\n   * 定义修改状态的方法。参数一自动推断为state类型。支持**immer**操作。支持多参数。\n   *\n   * ```typescript\n   * const model = defineModel('model1', {\n   *   initialState,\n   *   reducers: {\n   *     plus(state, step: number) {\n   *       state.count += step;\n   *     },\n   *     minus(state, step: number, scale: 1 | 2) {\n   *       state.count -= step * scale;\n   *     }\n   *   },\n   * });\n   * ```\n   */\n  reducers?: Action & InternalAction<State> & ThisType<ActionCtx<State>>;\n  /**\n   * 定义普通方法，异步方法等。\n   * 调用effect方法时，一般会伴随异步操作（请求数据、耗时任务），框架会自动收集当前方法的调用状态。\n   *\n   * ```typescript\n   * const model = defineModel('model1', {\n   *   initialState,\n   *   methods: {\n   *     async foo(p1: string, p2: number) {\n   *       const result = await Promise.resolve();\n   *       this.setState({ x: result });\n   *       return 'OK';\n   *     }\n   *   },\n   * });\n   *\n   * useLoading(model.foo); // 返回值类型: boolean\n   * ```\n   */\n  methods?: Effect &\n    ThisType<ModelAction<State, Action> & Effect & Computed & EffectCtx<State>>;\n  /**\n   * 定义计算属性。针对需要复杂的计算才能得出结果的场景而设计。如果只是简单的返回，建议使用`methods`\n   *\n   * ```typescript\n   * const initialState = { firstName: 'tick', lastName: 'tock' };\n   *\n   * const model = defineModel('model1', {\n   *   initialState,\n   *   computed: {\n   *     fullname() {\n   *       return this.state.firstName + '.' + this.state.lastName;\n   *     },\n   *     names() {\n   *       return this.fullName.value.split('').map((item) => `[${item}]`);\n   *     }\n   *   },\n   * });\n   * ```\n   *\n   * 可以单独使用：\n   * ```typescript\n   * model.fullname; // ComputedRef<string>;\n   * model.fullname.value; // string;\n   * ```\n   *\n   * 可以配合react hooks使用：\n   *\n   * ```typescript\n   * const fullname = useComputed(model.fullname); // string\n   * ```\n   */\n  computed?: Computed & ThisType<Computed & ComputedCtx<State>>;\n  /**\n   * 是否阻止刷新数据时跳过当前模型，默认即不跳过。\n   *\n   * 如果是强制刷新，则该参数无效。\n   *\n   * @see store.refresh(force: boolean = false)\n   */\n  skipRefresh?: boolean;\n  /**\n   * 定制持久化，请确保已经在初始化store的时候把当前模型加入persist配置，否则当前设置无效\n   *\n   * @see store.init()\n   */\n  persist?: ModelPersist<State, PersistDump> & ThisType<null>;\n  /**\n   * 生命周期\n   * @since 0.11.1\n   */\n  events?: Event<State> &\n    ThisType<ModelAction<State, Action> & Computed & Effect & EventCtx<State>>;\n}\n"
  },
  {
    "path": "src/persist/persist-gate.tsx",
    "content": "import { ReactNode, FC, useState, useEffect } from 'react';\nimport { modelStore } from '../store/model-store';\nimport { isFunction } from '../utils/is-type';\n\nexport interface PersistGateProps {\n  loading?: ReactNode;\n  children?: ReactNode | ((isReady: boolean) => ReactNode);\n}\n\nexport const PersistGate: FC<PersistGateProps> = (props) => {\n  const state = useState(() => modelStore.isReady),\n    isReady = state[0],\n    setIsReady = state[1];\n  const { loading = null, children } = props;\n\n  useEffect(() => {\n    isReady ||\n      modelStore.onInitialized().then(() => {\n        setIsReady(true);\n      });\n  }, []);\n\n  /* istanbul ignore else -- @preserve */\n  if (process.env.NODE_ENV !== 'production') {\n    if (loading && isFunction(children)) {\n      console.error('[PersistGate] 当前children为函数类型，loading属性无效');\n    }\n  }\n\n  return (\n    <>\n      {isFunction(children) ? children(isReady) : isReady ? children : loading}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/persist/persist-item.ts",
    "content": "import type { StorageEngine } from '../engines/storage-engine';\nimport type {\n  GetInitialState,\n  InternalModel,\n  Model,\n  ModelPersist,\n} from '../model/types';\nimport { isObject, isPlainObject, isString } from '../utils/is-type';\nimport { toPromise } from '../utils/to-promise';\nimport { parseState, stringifyState } from '../utils/serialize';\n\nexport interface PersistSchema {\n  /**\n   * 版本\n   */\n  v: number | string;\n  /**\n   * 数据\n   */\n  d: {\n    [key: string]: PersistItemSchema;\n  };\n}\n\nexport interface PersistItemSchema {\n  /**\n   * 版本\n   */\n  v: number | string;\n  /**\n   * 数据\n   */\n  d: string;\n}\n\nexport type PersistMergeMode = 'replace' | 'merge' | 'deep-merge';\n\nexport interface PersistOptions {\n  /**\n   * 存储唯一标识名称\n   */\n  key: string;\n  /**\n   * 存储名称前缀，默认值：`@@foca.persist:`\n   */\n  keyPrefix?: string;\n  /**\n   * 持久化数据与初始数据的合并方式。默认值：`merge`\n   *\n   * - replace - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据\n   * - merge - 合并模式。数据从存储引擎取出后，与初始数据多余部分进行合并，可以理解为`Object.assign()`操作\n   * - deep-merge - 二级合并模式。在合并模式的基础上，如果某个key的值为对象，则该对象也会执行合并操作\n   *\n   * 注意：当数据为数组格式时该配置无效。\n   * @since 3.0.0\n   */\n  merge?: PersistMergeMode;\n  /**\n   * 版本号\n   */\n  version: string | number;\n  /**\n   * 存储引擎\n   */\n  engine: StorageEngine;\n  /**\n   * 允许持久化的模型列表\n   */\n  models: Model[];\n}\n\ntype CustomModelPersistOptions = Required<ModelPersist<object, any>> & {\n  ctx: GetInitialState<object>;\n};\n\nconst defaultDumpOrLoadFn = (value: any) => value;\n\ninterface PersistRecord {\n  model: Model;\n  /**\n   * 模型的persist参数\n   */\n  opts: CustomModelPersistOptions;\n  /**\n   * 已经存储的模型内容，data是字符串。\n   * 存储时，如果各项属性符合条件，则会当作最终值，从而省去了系列化的过程。\n   */\n  schema?: PersistItemSchema;\n  /**\n   * 已经存储的模型内容，data是对象。\n   * 主要用于和store变化后的state对比。\n   */\n  prev?: object;\n}\n\nexport class PersistItem {\n  readonly key: string;\n\n  protected readonly records: Record<string, PersistRecord> = {};\n\n  constructor(protected readonly options: PersistOptions) {\n    const {\n      models,\n      keyPrefix = '@@foca.persist:',\n      key,\n      merge = 'merge' satisfies PersistMergeMode,\n    } = options;\n\n    this.key = keyPrefix + key;\n\n    for (let i = models.length; i-- > 0; ) {\n      const model = models[i]!;\n      const {\n        load = defaultDumpOrLoadFn,\n        dump = defaultDumpOrLoadFn,\n        version: customVersion = 0,\n        merge: customMerge = merge,\n      } = (model as unknown as InternalModel)._$opts.persist || {};\n\n      this.records[model.name] = {\n        model,\n        opts: {\n          version: customVersion,\n          merge: customMerge,\n          load,\n          dump,\n          ctx: (model as unknown as InternalModel)._$persistCtx,\n        },\n      };\n    }\n  }\n\n  init(): Promise<void> {\n    return toPromise(() => this.options.engine.getItem(this.key)).then(\n      (data) => {\n        if (!data) {\n          this.loadMissingState();\n          return this.dump();\n        }\n\n        try {\n          const schema = JSON.parse(data);\n\n          if (!this.validateSchema(schema)) {\n            this.loadMissingState();\n            return this.dump();\n          }\n\n          const schemaKeys = Object.keys(schema.d);\n          for (let i = schemaKeys.length; i-- > 0; ) {\n            const key = schemaKeys[i]!;\n            const record = this.records[key];\n\n            if (record) {\n              const { opts } = record;\n              const itemSchema = schema.d[key]!;\n              if (this.validateItemSchema(itemSchema, opts)) {\n                const dumpData = parseState(itemSchema.d);\n                record.prev = this.merge(\n                  opts.load.call(opts.ctx, dumpData),\n                  opts.ctx.initialState,\n                  opts.merge,\n                );\n                record.schema = itemSchema;\n              }\n            }\n          }\n\n          this.loadMissingState();\n          return this.dump();\n        } catch (e) {\n          this.dump();\n          throw e;\n        }\n      },\n    );\n  }\n\n  loadMissingState() {\n    this.loop((record) => {\n      const { prev, opts, schema } = record;\n      if (!schema || !prev) {\n        const dumpData = opts.dump.call(null, opts.ctx.initialState);\n        record.prev = this.merge(\n          opts.load.call(opts.ctx, dumpData),\n          opts.ctx.initialState,\n          opts.merge,\n        );\n        record.schema = {\n          v: opts.version,\n          d: stringifyState(dumpData),\n        };\n      }\n    });\n  }\n\n  merge(persistState: any, initialState: any, mode: PersistMergeMode) {\n    const isStateArray = Array.isArray(persistState);\n    const isInitialStateArray = Array.isArray(initialState);\n    if (isStateArray && isInitialStateArray) return persistState;\n    if (isStateArray || isInitialStateArray) return initialState;\n\n    if (mode === 'replace') return persistState;\n\n    const state = Object.assign({}, initialState, persistState);\n\n    if (mode === 'deep-merge') {\n      const keys = Object.keys(persistState);\n      for (let i = 0; i < keys.length; ++i) {\n        const key = keys[i]!;\n        if (\n          Object.prototype.hasOwnProperty.call(initialState, key) &&\n          isPlainObject(state[key]) &&\n          isPlainObject(initialState[key])\n        ) {\n          state[key] = Object.assign({}, initialState[key], state[key]);\n        }\n      }\n    }\n\n    return state;\n  }\n\n  collect(): Record<string, object> {\n    const stateMaps: Record<string, object> = {};\n\n    this.loop(({ prev: state }, key) => {\n      state && (stateMaps[key] = state);\n    });\n\n    return stateMaps;\n  }\n\n  update(nextState: Record<string, object>) {\n    let changed = false;\n\n    this.loop((record) => {\n      const { model, prev, opts, schema } = record;\n      const nextStateForKey = nextState[model.name]!;\n\n      // 状态不变的情况下，即使过期了也无所谓，下次初始化时会自动剔除。\n      // 版本号改动的话一定会触发页面刷新。\n      if (nextStateForKey !== prev) {\n        record.prev = nextStateForKey;\n        const nextSchema: PersistItemSchema = {\n          v: opts.version,\n          d: stringifyState(opts.dump.call(null, nextStateForKey)),\n        };\n\n        if (!schema || nextSchema.d !== schema.d) {\n          record.schema = nextSchema;\n          changed ||= true;\n        }\n      }\n    });\n\n    changed && this.dump();\n  }\n\n  protected loop(callback: (record: PersistRecord, key: string) => void) {\n    const records = this.records;\n    const recordKeys = Object.keys(records);\n    for (let i = recordKeys.length; i-- > 0; ) {\n      const key = recordKeys[i]!;\n      callback(records[key]!, key);\n    }\n  }\n\n  protected dump() {\n    this.options.engine.setItem(this.key, JSON.stringify(this.toJSON()));\n  }\n\n  protected validateSchema(schema: any): schema is PersistSchema {\n    return (\n      isObject<PersistSchema>(schema) &&\n      isObject<PersistSchema['d']>(schema.d) &&\n      schema.v === this.options.version\n    );\n  }\n\n  protected validateItemSchema(\n    schema: PersistItemSchema | undefined,\n    options: CustomModelPersistOptions,\n  ) {\n    return schema && schema.v === options.version && isString(schema.d);\n  }\n\n  protected toJSON(): PersistSchema {\n    const states: PersistSchema['d'] = {};\n\n    this.loop(({ schema }, key) => {\n      schema && (states[key] = schema);\n    });\n\n    return { v: this.options.version, d: states };\n  }\n}\n"
  },
  {
    "path": "src/persist/persist-manager.ts",
    "content": "import type { Reducer, Store, Unsubscribe } from 'redux';\nimport { actionHydrate, isHydrateAction } from '../actions/persist';\nimport { PersistItem, PersistOptions } from './persist-item';\n\nexport class PersistManager {\n  protected initialized: boolean = false;\n  protected readonly list: PersistItem[];\n  protected timer?: ReturnType<typeof setTimeout>;\n  protected unsubscribeStore!: Unsubscribe;\n\n  constructor(options: PersistOptions[]) {\n    this.list = options.map((option) => new PersistItem(option));\n  }\n\n  init(store: Store, hydrate: boolean) {\n    this.unsubscribeStore = store.subscribe(() => {\n      this.initialized && this.update(store);\n    });\n\n    return Promise.all(this.list.map((item) => item.init())).then(() => {\n      hydrate && store.dispatch(actionHydrate(this.collect()));\n      this.initialized = true;\n    });\n  }\n\n  destroy() {\n    this.unsubscribeStore();\n    this.initialized = false;\n  }\n\n  collect(): Record<string, object> {\n    return this.list.reduce<Record<string, object>>((stateMaps, item) => {\n      return Object.assign(stateMaps, item.collect());\n    }, {});\n  }\n\n  combineReducer(original: Reducer): Reducer<Record<string, object>> {\n    return (state, action) => {\n      if (state === void 0) state = {};\n\n      if (isHydrateAction(action)) {\n        return Object.assign({}, state, action.payload);\n      }\n\n      return original(state, action);\n    };\n  }\n\n  protected update(store: Store) {\n    this.timer ||= setTimeout(() => {\n      const nextState = store.getState();\n      this.timer = void 0;\n      for (let i = this.list.length; i-- > 0; ) {\n        this.list[i]!.update(nextState);\n      }\n    }, 50);\n  }\n}\n"
  },
  {
    "path": "src/reactive/computed-value.ts",
    "content": "import type { Store } from 'redux';\nimport { depsCollector } from './deps-collector';\nimport { createComputedDeps } from './create-computed-deps';\nimport type { Deps } from './object-deps';\n\nexport class ComputedValue<T = any> {\n  public deps: Deps[] = [];\n  public snapshot: any;\n\n  protected active?: boolean;\n  protected root?: any;\n\n  constructor(\n    protected readonly store: Pick<Store<Record<string, any>>, 'getState'>,\n    public readonly model: string,\n    public readonly property: string,\n    protected readonly fn: () => any,\n  ) {}\n\n  public get value(): T {\n    if (this.active) {\n      throw new Error(\n        `[model:${this.model}] 计算属性\"${this.property}\"正在被循环引用`,\n      );\n    }\n\n    this.active = true;\n    this.isDirty() && this.updateSnapshot();\n    this.active = false;\n\n    if (depsCollector.active) {\n      // 作为其他computed的依赖\n      depsCollector.prepend(createComputedDeps(this));\n    }\n\n    return this.snapshot;\n  }\n\n  isDirty(): boolean {\n    if (!this.root) return true;\n\n    const rootState = this.store.getState();\n\n    if (this.root !== rootState) {\n      const deps = this.deps;\n      // 前置的元素是createComputedDeps()生成的对象，执行isDirty()会触发ref.value\n      // 后置的元素是state，所以需要从后往前判断\n      for (let i = deps.length; i-- > 0; ) {\n        if (deps[i]!.isDirty()) return true;\n      }\n    }\n\n    this.root = rootState;\n    return false;\n  }\n\n  protected updateSnapshot() {\n    this.deps = depsCollector.produce(() => {\n      this.snapshot = this.fn();\n      this.root = this.store.getState();\n    });\n  }\n}\n"
  },
  {
    "path": "src/reactive/create-computed-deps.ts",
    "content": "import { shallowEqual } from 'react-redux';\nimport type { ComputedValue } from './computed-value';\nimport type { Deps } from './object-deps';\n\nexport const createComputedDeps = (body: ComputedValue): Deps => {\n  let snapshot: any;\n\n  return {\n    id: `c-${body.model}-${body.property}`,\n    end(): void {\n      snapshot = body.snapshot;\n    },\n    isDirty(): boolean {\n      return !shallowEqual(snapshot, body.value);\n    },\n  };\n};\n"
  },
  {
    "path": "src/reactive/deps-collector.ts",
    "content": "import type { Deps } from './object-deps';\n\nconst deps: Deps[][] = [];\nlet level = -1;\n\nexport const depsCollector = {\n  get active(): boolean {\n    return level >= 0;\n  },\n  produce(callback: Function): Deps[] {\n    const current: Deps[] = (deps[++level] = []);\n    callback();\n    deps.length = level--;\n\n    const uniqueDeps: Deps[] = [];\n    const uniqueID: string[] = [];\n\n    for (let i = 0; i < current.length; ++i) {\n      const dep = current[i]!;\n      const id = dep.id;\n      if (uniqueID.indexOf(id) === -1) {\n        uniqueID.push(id);\n        uniqueDeps.push(dep);\n        dep.end();\n      }\n    }\n\n    return uniqueDeps;\n  },\n  append(dep: Deps) {\n    deps[level]!.push(dep);\n  },\n  prepend(dep: Deps) {\n    deps[level]!.unshift(dep);\n  },\n};\n"
  },
  {
    "path": "src/reactive/object-deps.ts",
    "content": "import type { Store } from 'redux';\nimport { isObject } from '../utils/is-type';\nimport { depsCollector } from './deps-collector';\n\nexport interface Deps {\n  id: string;\n  end(): void;\n  isDirty(): boolean;\n}\n\nexport class ObjectDeps<T = any> implements Deps {\n  protected active: boolean = true;\n  protected snapshot: any;\n  protected root: any;\n\n  constructor(\n    protected readonly store: Pick<Store<Record<string, any>>, 'getState'>,\n    protected readonly model: string,\n    protected readonly deps: string[] = [],\n  ) {\n    this.root = this.getState();\n  }\n\n  isDirty(): boolean {\n    const rootState = this.getState();\n    if (this.root === rootState) return false;\n    const { pathChanged, snapshot: nextSnapshot } = this.getSnapshot(rootState);\n    if (pathChanged || this.snapshot !== nextSnapshot) return true;\n    this.root = rootState;\n    return false;\n  }\n\n  get id(): string {\n    return this.model + '.' + this.deps.join('.');\n  }\n\n  start<T extends Record<string, any>>(startState: T): T {\n    depsCollector.append(this);\n    return this.proxy(startState);\n  }\n\n  end(): void {\n    this.active = false;\n  }\n\n  protected getState(): T {\n    return this.store.getState()[this.model];\n  }\n\n  protected getSnapshot(state: any): { pathChanged: boolean; snapshot: any } {\n    const deps = this.deps;\n    let snapshot = state;\n    for (let i = 0; i < deps.length; ++i) {\n      if (!isObject<Record<string, any>>(snapshot)) {\n        return { pathChanged: true, snapshot };\n      }\n      snapshot = snapshot[deps[i]!];\n    }\n\n    return { pathChanged: false, snapshot };\n  }\n\n  protected proxy(currentState: Record<string, any>): any {\n    if (\n      currentState === null ||\n      !isObject<Record<string, any>>(currentState) ||\n      Array.isArray(currentState)\n    ) {\n      return currentState;\n    }\n\n    const nextState = {};\n    const keys = Object.keys(currentState);\n    const currentDeps = this.deps.slice();\n    let visited = false;\n\n    for (let i = keys.length; i-- > 0; ) {\n      const key = keys[i]!;\n\n      Object.defineProperty(nextState, key, {\n        enumerable: true,\n        get: () => {\n          if (!this.active) return currentState[key];\n\n          if (visited) {\n            return new ObjectDeps(\n              this.store,\n              this.model,\n              currentDeps.slice(),\n            ).start(currentState)[key];\n          }\n\n          visited = true;\n          this.deps.push(key);\n          return this.proxy((this.snapshot = currentState[key]));\n        },\n      });\n    }\n\n    /* istanbul ignore else -- @preserve */\n    if (process.env.NODE_ENV !== 'production') {\n      Object.freeze(nextState);\n    }\n\n    return nextState;\n  }\n}\n"
  },
  {
    "path": "src/redux/connect.ts",
    "content": "import { Connect, connect as originalConnect } from 'react-redux';\nimport { ProxyContext } from './contexts';\nimport { toArgs } from '../utils/to-args';\n\nexport const connect: Connect = function () {\n  const args = toArgs<Parameters<Connect>>(arguments);\n  (args[3] ||= {}).context = ProxyContext;\n\n  return originalConnect.apply(null, args);\n};\n"
  },
  {
    "path": "src/redux/contexts.ts",
    "content": "import { createContext } from 'react';\nimport type { ReactReduxContextValue } from 'react-redux';\n\nexport const ModelContext = createContext<ReactReduxContextValue | null>(null);\n\nexport const LoadingContext = createContext<ReactReduxContextValue | null>(\n  null,\n);\n\nexport const ProxyContext = createContext<ReactReduxContextValue | null>(null);\n"
  },
  {
    "path": "src/redux/create-reducer.ts",
    "content": "import type { Reducer } from 'redux';\nimport { isPostModelAction } from '../actions/model';\nimport { isRefreshAction } from '../actions/refresh';\n\ninterface Options<State extends object> {\n  readonly name: string;\n  readonly initialState: State;\n  readonly allowRefresh: boolean;\n}\n\nexport const createReducer = <State extends object>(\n  options: Options<State>,\n): Reducer<State> => {\n  const allowRefresh = options.allowRefresh;\n  const reducerName = options.name;\n  const initialState = options.initialState;\n\n  return function reducer(state, action) {\n    if (state === void 0) return initialState;\n\n    if (isPostModelAction<State>(action) && action.model === reducerName) {\n      return action.next;\n    }\n\n    if (isRefreshAction(action) && (allowRefresh || action.payload.force)) {\n      return initialState;\n    }\n\n    return state;\n  };\n};\n"
  },
  {
    "path": "src/redux/foca-provider.tsx",
    "content": "import { FC } from 'react';\nimport { Provider } from 'react-redux';\nimport { ProxyContext, ModelContext, LoadingContext } from './contexts';\nimport { modelStore } from '../store/model-store';\nimport { PersistGate, PersistGateProps } from '../persist/persist-gate';\nimport { proxyStore } from '../store/proxy-store';\nimport { loadingStore } from '../store/loading-store';\nimport { isFunction } from '../utils/is-type';\n\ninterface OwnProps extends PersistGateProps {}\n\n/**\n * 状态上下文组件，请挂载到入口文件。\n * 请确保您已经初始化了store仓库。\n *\n * @see store.init()\n *\n * ```typescript\n * ReactDOM.render(\n *   <FocaProvider>\n *     <App />\n *   </FocaProvider>\n * );\n * ```\n */\nexport const FocaProvider: FC<OwnProps> = ({ children, loading }) => {\n  return (\n    <Provider context={ProxyContext} store={proxyStore}>\n      <Provider context={LoadingContext} store={loadingStore}>\n        <Provider context={ModelContext} store={modelStore}>\n          {modelStore['persister'] ? (\n            <PersistGate loading={loading} children={children} />\n          ) : isFunction(children) ? (\n            children(true)\n          ) : (\n            children\n          )}\n        </Provider>\n      </Provider>\n    </Provider>\n  );\n};\n"
  },
  {
    "path": "src/redux/use-selector.ts",
    "content": "import { createSelectorHook } from 'react-redux';\nimport { ModelContext, LoadingContext } from './contexts';\n\nexport const useModelSelector = createSelectorHook(ModelContext);\n\nexport const useLoadingSelector = createSelectorHook(LoadingContext);\n"
  },
  {
    "path": "src/store/loading-store.ts",
    "content": "import {\n  UnknownAction,\n  applyMiddleware,\n  legacy_createStore as createStore,\n  Middleware,\n} from 'redux';\nimport type { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';\nimport { loadingInterceptor } from '../middleware/loading.interceptor';\nimport { isDestroyLoadingAction, isLoadingAction } from '../actions/loading';\nimport { actionRefresh, isRefreshAction } from '../actions/refresh';\nimport { combine } from './proxy-store';\nimport { destroyLoadingInterceptor } from '../middleware/destroy-loading.interceptor';\nimport { immer } from '../utils/immer';\nimport { StoreBasic } from './store-basic';\nimport { modelStore } from './model-store';\nimport { freeze } from 'immer';\nimport { freezeStateMiddleware } from '../middleware/freeze-state.middleware';\n\nexport interface FindLoading {\n  find(category: number | string): boolean;\n}\n\ninterface LoadingState extends FindLoading {\n  data: {\n    [category: string]: boolean;\n  };\n}\n\ninterface LoadingStoreStateItem {\n  loadings: LoadingState;\n}\n\nexport type LoadingStoreState = Partial<{\n  [model: string]: Partial<{\n    [method: string]: LoadingStoreStateItem;\n  }>;\n}>;\n\nconst findLoading: FindLoading['find'] = function (\n  this: LoadingState,\n  category,\n) {\n  return !!this.data[category];\n};\n\nconst createDefaultRecord = (): LoadingStoreStateItem => {\n  return {\n    loadings: {\n      find: findLoading,\n      data: {},\n    },\n  };\n};\n\nexport class LoadingStore extends StoreBasic<LoadingStoreState> {\n  protected initializingModels: string[] = [];\n  protected status: Partial<{\n    [model: string]: Partial<{\n      [method: string]: boolean;\n    }>;\n  }> = {};\n  protected defaultRecord: LoadingStoreStateItem = freeze(\n    createDefaultRecord(),\n    true,\n  );\n\n  constructor() {\n    super();\n    const topic = modelStore.topic;\n    topic.subscribe('init', this.init.bind(this));\n    topic.subscribe('refresh', this.refresh.bind(this));\n    topic.subscribe('unmount', this.unmount.bind(this));\n    topic.subscribe('modelPreInit', (modelName) => {\n      this.initializingModels.push(modelName);\n    });\n    topic.subscribe('modelPostInit', (modelName) => {\n      this.initializingModels = this.initializingModels.filter(\n        (item) => item !== modelName,\n      );\n    });\n  }\n\n  init() {\n    const middleware: Middleware[] = [\n      loadingInterceptor(this),\n      destroyLoadingInterceptor,\n    ];\n\n    /* istanbul ignore else -- @preserve */\n    if (process.env.NODE_ENV !== 'production') {\n      middleware.push(freezeStateMiddleware);\n    }\n\n    this.origin = createStore(\n      this.reducer.bind(this),\n      applyMiddleware.apply(null, middleware),\n    );\n\n    combine(this.store);\n  }\n\n  unmount(): void {\n    this.origin = null;\n  }\n\n  reducer(\n    state: LoadingStoreState | undefined,\n    action: UnknownAction,\n  ): LoadingStoreState {\n    if (state === void 0) {\n      state = {};\n    }\n\n    if (isLoadingAction(action)) {\n      const {\n        model,\n        method,\n        payload: { category, loading },\n      } = action;\n      const next = immer.produce(state, (draft) => {\n        draft[model] ||= {};\n        const { loadings } = (draft[model]![method] ||= createDefaultRecord());\n        loadings.data[category] = loading;\n      });\n\n      return next;\n    }\n\n    if (isDestroyLoadingAction(action)) {\n      const next = Object.assign({}, state);\n      delete next[action.model];\n      delete this.status[action.model];\n      return next;\n    }\n\n    if (isRefreshAction(action)) return {};\n\n    return state;\n  }\n\n  get(effect: PromiseEffect | PromiseRoomEffect): LoadingStoreStateItem {\n    const {\n      _: { model, method },\n    } = effect;\n    let record: LoadingStoreStateItem | undefined;\n\n    if (this.isActive(model, method)) {\n      record = this.getItem(model, method);\n    } else {\n      this.activate(model, method);\n    }\n\n    return record || this.defaultRecord;\n  }\n\n  getItem(model: string, method: string): LoadingStoreStateItem | undefined {\n    const level1 = this.getState()[model];\n    return level1 && level1[method];\n  }\n\n  isModelInitializing(model: string): boolean {\n    return (\n      this.initializingModels.length > 0 &&\n      this.initializingModels.includes(model)\n    );\n  }\n\n  isActive(model: string, method: string): boolean {\n    const level1 = this.status[model];\n    return level1 !== void 0 && level1[method] === true;\n  }\n\n  activate(model: string, method: string) {\n    (this.status[model] ||= {})[method] = true;\n  }\n\n  inactivate(model: string, method: string) {\n    (this.status[model] ||= {})[method] = false;\n  }\n\n  refresh() {\n    return this.dispatch(actionRefresh(true));\n  }\n}\n\nexport const loadingStore = new LoadingStore();\n"
  },
  {
    "path": "src/store/model-store.ts",
    "content": "import {\n  applyMiddleware,\n  compose,\n  legacy_createStore as createStore,\n  Middleware,\n  Reducer,\n  Store,\n  StoreEnhancer,\n} from 'redux';\nimport { Topic } from 'topic';\nimport { actionRefresh, RefreshAction } from '../actions/refresh';\nimport { modelInterceptor } from '../middleware/model.interceptor';\nimport type { PersistOptions } from '../persist/persist-item';\nimport { PersistManager } from '../persist/persist-manager';\nimport { combine } from './proxy-store';\nimport { OBJECT } from '../utils/is-type';\nimport { StoreBasic } from './store-basic';\nimport { actionInActionInterceptor } from '../middleware/action-in-action.interceptor';\nimport { freezeStateMiddleware } from '../middleware/freeze-state.middleware';\n\ntype Compose =\n  | typeof compose\n  | ((...funcs: StoreEnhancer<any>[]) => StoreEnhancer<any>);\n\ninterface CreateStoreOptions {\n  preloadedState?: Record<string, any>;\n  compose?: 'redux-devtools' | Compose;\n  middleware?: Middleware[];\n  persist?: PersistOptions[];\n}\n\nexport class ModelStore extends StoreBasic<Record<string, any>> {\n  public topic: Topic<{\n    init: [];\n    ready: [];\n    refresh: [];\n    unmount: [];\n    modelPreInit: [modelName: string];\n    modelPostInit: [modelName: string];\n  }> = new Topic();\n  protected _isReady: boolean = false;\n  protected consumers: Record<string, Reducer> = {};\n  protected reducerKeys: string[] = [];\n  protected persister: PersistManager | null = null;\n\n  protected reducer!: Reducer;\n\n  constructor() {\n    super();\n    this.topic.keep('ready', () => this._isReady);\n  }\n\n  get isReady(): boolean {\n    return this._isReady;\n  }\n\n  init(options: CreateStoreOptions = {}) {\n    const prevStore = this.origin;\n    const firstInitialize = !prevStore;\n\n    if (!firstInitialize) {\n      if (process.env.NODE_ENV === 'production') {\n        throw new Error(`[store] 请勿多次执行'store.init()'`);\n      }\n    }\n\n    this._isReady = false;\n    this.reducer = this.combineReducers();\n\n    const persistOptions = options.persist;\n    let persister = this.persister;\n    persister && persister.destroy();\n    if (persistOptions && persistOptions.length) {\n      persister = this.persister = new PersistManager(persistOptions);\n      this.reducer = persister.combineReducer(this.reducer);\n    } else {\n      persister = this.persister = null;\n    }\n\n    let store: Store;\n\n    if (firstInitialize) {\n      const middleware = (options.middleware || []).concat(modelInterceptor);\n      /* istanbul ignore else -- @preserve */\n      if (process.env.NODE_ENV !== 'production') {\n        middleware.unshift(actionInActionInterceptor);\n        middleware.push(freezeStateMiddleware);\n      }\n\n      const enhancer = applyMiddleware.apply(null, middleware);\n\n      store = this.origin = createStore(\n        this.reducer,\n        options.preloadedState,\n        this.getCompose(options.compose)(enhancer),\n      );\n      this.topic.publish('init');\n\n      combine(store);\n    } else {\n      // 重新创建store会导致组件里的subscription都失效\n      store = prevStore;\n      store.replaceReducer(this.reducer);\n    }\n\n    if (persister) {\n      persister.init(store, firstInitialize).then(() => {\n        this.ready();\n      });\n    } else {\n      this.ready();\n    }\n\n    return this;\n  }\n\n  refresh(force: boolean = false): RefreshAction {\n    const action = this.dispatch(actionRefresh(force));\n    this.topic.publish('refresh');\n    return action;\n  }\n\n  unmount() {\n    this.origin = null;\n    this._isReady = false;\n    this.topic.publish('unmount');\n  }\n\n  onInitialized(maybeSync?: () => void): Promise<void> {\n    return new Promise((resolve) => {\n      if (this._isReady) {\n        maybeSync && maybeSync();\n        resolve();\n      } else {\n        this.topic.subscribeOnce('ready', () => {\n          maybeSync && maybeSync();\n          resolve();\n        });\n      }\n    });\n  }\n\n  protected ready() {\n    this._isReady = true;\n    this.topic.publish('ready');\n  }\n\n  protected getCompose(customCompose: CreateStoreOptions['compose']): Compose {\n    if (customCompose === 'redux-devtools') {\n      /* istanbul ignore if -- @preserve */\n      if (process.env.NODE_ENV !== 'production') {\n        return (\n          /** @ts-expect-error */\n          (typeof window === OBJECT\n            ? window\n            : typeof global === OBJECT\n              ? global\n              : {})['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose\n        );\n      }\n\n      return compose;\n    }\n\n    return customCompose || compose;\n  }\n\n  protected combineReducers(): Reducer<Record<string, object>> {\n    return (state, action) => {\n      if (state === void 0) {\n        state = {};\n      }\n\n      const reducerKeys = this.reducerKeys;\n      const consumers = this.consumers;\n      const keyLength = reducerKeys.length;\n      const nextState: Record<string, any> = {};\n      let hasChanged = false;\n      let i = keyLength;\n\n      while (i-- > 0) {\n        const key = reducerKeys[i]!;\n        const prevForKey = state[key];\n        const nextForKey = (nextState[key] = consumers[key]!(\n          prevForKey,\n          action,\n        ));\n        hasChanged ||= nextForKey !== prevForKey;\n      }\n\n      return hasChanged || keyLength !== Object.keys(state).length\n        ? nextState\n        : state;\n    };\n  }\n\n  protected appendReducer(key: string, consumer: Reducer): void {\n    const store = this.origin;\n    const consumers = this.consumers;\n    const exists = store && consumers.hasOwnProperty(key);\n\n    consumers[key] = consumer;\n    this.reducerKeys = Object.keys(consumers);\n    store && !exists && store.replaceReducer(this.reducer);\n  }\n\n  protected removeReducer(key: string): void {\n    const store = this.origin;\n    const consumers = this.consumers;\n\n    if (consumers.hasOwnProperty(key)) {\n      delete consumers[key];\n      this.reducerKeys = Object.keys(consumers);\n      store && store.replaceReducer(this.reducer);\n    }\n  }\n}\n\nexport const modelStore = new ModelStore();\n"
  },
  {
    "path": "src/store/proxy-store.ts",
    "content": "import { legacy_createStore as createStore, Store } from 'redux';\n\nexport const proxyStore = createStore(() => ({}));\n\nconst dispatch = () => {\n  proxyStore.dispatch({\n    type: '-',\n  });\n};\n\n/**\n * 为了触发connect()，需要将实体store都注册到代理的store。\n */\nexport const combine = (otherStore: Store) => {\n  otherStore.subscribe(dispatch);\n};\n"
  },
  {
    "path": "src/store/store-basic.ts",
    "content": "import { Store } from 'redux';\nimport { $$observable } from '../utils/symbol-observable';\n\nexport abstract class StoreBasic<T> implements Store<T> {\n  protected origin: Store<T> | null = null;\n\n  /**\n   * @deprecated 请勿使用该方法，因为它其实没有被实现\n   */\n  declare replaceReducer: Store<T>['replaceReducer'];\n\n  dispatch: Store<T>['dispatch'] = (action) => {\n    return this.store.dispatch(action);\n  };\n\n  getState: Store<T>['getState'] = () => {\n    return this.store.getState();\n  };\n\n  subscribe: Store<T>['subscribe'] = (listener) => {\n    return this.store.subscribe(listener);\n  };\n\n  [$$observable]: Store<T>[typeof $$observable] = () => {\n    return this.store[$$observable]();\n  };\n\n  protected get store(): Store<T> {\n    if (!this.origin) {\n      throw new Error(`[store] 当前无实例，忘记执行'store.init()'了吗？`);\n    }\n    return this.origin;\n  }\n\n  abstract init(): void;\n  abstract unmount(): void;\n}\n"
  },
  {
    "path": "src/utils/deep-equal.ts",
    "content": "import { OBJECT } from './is-type';\n\nexport const deepEqual = (a: any, b: any): boolean => {\n  if (a === b) return true;\n\n  if (a && b && typeof a == OBJECT && typeof b == OBJECT) {\n    if (a.constructor !== b.constructor) return false;\n\n    let i: number;\n    let len: number;\n    let key: string;\n\n    if (Array.isArray(a)) {\n      len = a.length;\n\n      if (len != b.length) return false;\n\n      for (i = len; i-- > 0; ) {\n        if (!deepEqual(a[i], b[i])) return false;\n      }\n\n      return true;\n    }\n\n    const keys = Object.keys(a);\n    len = keys.length;\n\n    if (len !== Object.keys(b).length) return false;\n\n    for (i = len; i-- > 0; ) {\n      if (!hasOwn.call(b, keys[i]!)) return false;\n    }\n\n    for (i = len; i-- > 0; ) {\n      key = keys[i]!;\n      if (!deepEqual(a[key], b[key])) return false;\n    }\n\n    return true;\n  }\n\n  return a !== a && b !== b;\n};\n\nconst hasOwn = Object.prototype.hasOwnProperty;\n"
  },
  {
    "path": "src/utils/get-method-category.ts",
    "content": "export const getMethodCategory = (methodName: string) =>\n  methodName.indexOf('_') === 0 ? 'internal' : 'external';\n"
  },
  {
    "path": "src/utils/getter.ts",
    "content": "import { toArgs } from './to-args';\n\nexport function composeGetter<\n  T extends object,\n  U1 extends (...args: any[]) => any,\n>(obj: T, getter1: U1): T & ReturnType<U1>;\n\nexport function composeGetter<\n  T extends object,\n  U1 extends (...args: any[]) => any,\n  U2 extends (...args: any[]) => any,\n>(obj: T, getter1: U1, getter2: U2): T & ReturnType<U1> & ReturnType<U2>;\n\nexport function composeGetter<\n  T extends object,\n  U1 extends (...args: any[]) => any,\n  U2 extends (...args: any[]) => any,\n  U3 extends (...args: any[]) => any,\n>(\n  obj: T,\n  getter1: U1,\n  getter2: U2,\n  getter3: U3,\n): T & ReturnType<U1> & ReturnType<U2> & ReturnType<U3>;\n\nexport function composeGetter<\n  T extends object,\n  U1 extends (...args: any[]) => any,\n  U2 extends (...args: any[]) => any,\n  U3 extends (...args: any[]) => any,\n  U4 extends (...args: any[]) => any,\n>(\n  obj: T,\n  getter1: U1,\n  getter2: U2,\n  getter3: U3,\n  getter4: U4,\n): T & ReturnType<U1> & ReturnType<U2> & ReturnType<U3> & ReturnType<U4>;\n\nexport function composeGetter() {\n  const args = toArgs<Function[]>(arguments);\n\n  return args.reduce((carry, getter) => getter(carry), args.shift() as object);\n}\n\nexport const defineGetter = (obj: object, key: string, get: () => any): any => {\n  Object.defineProperty(obj, key, {\n    get,\n  });\n  return obj;\n};\n"
  },
  {
    "path": "src/utils/immer.ts",
    "content": "import { Immer, enableES5 } from 'immer';\n\n/**\n * 支持ES5，毕竟Proxy无法polyfill。有些用户手机可以10年不换！！\n * @link https://immerjs.github.io/immer/docs/installation#pick-your-immer-version\n * @since immer@6.0\n */\nenableES5();\n\nexport const immer = new Immer({\n  autoFreeze: false,\n});\n"
  },
  {
    "path": "src/utils/is-promise.ts",
    "content": "import { FUNCTION, isFunction, isObject } from './is-type';\n\nconst hasPromise = typeof Promise === FUNCTION;\n\nexport const isPromise = <T>(value: any): value is Promise<T> => {\n  return (\n    (hasPromise && value instanceof Promise) ||\n    ((isObject(value) || isFunction(value)) && isFunction(value.then))\n  );\n};\n"
  },
  {
    "path": "src/utils/is-type.ts",
    "content": "export const OBJECT = 'object';\nexport const FUNCTION = 'function';\n\nexport const isFunction = <T extends Function>(value: any): value is T =>\n  !!value && typeof value === FUNCTION;\n\nexport const isObject = <T extends object>(value: any): value is T =>\n  !!value && typeof value === OBJECT;\n\nexport const isPlainObject = <T extends object>(value: any): value is T =>\n  !!value && Object.prototype.toString.call(value) === '[object Object]';\n\nexport const isString = <T extends string>(value: any): value is T =>\n  typeof value === 'string';\n"
  },
  {
    "path": "src/utils/serialize.ts",
    "content": "import { isObject } from './is-type';\n\nconst JSON_UNDEFINED = '__JSON_UNDEFINED__';\n\nconst replacer = (_key: string, value: any) => {\n  return value === void 0 ? JSON_UNDEFINED : value;\n};\n\nconst reviver = (_key: string, value: any) => {\n  if (isObject<Record<string, any>>(value)) {\n    const keys = Object.keys(value);\n    for (let i = keys.length; i-- > 0; ) {\n      const key = keys[i]!;\n      if (value[key] === JSON_UNDEFINED) {\n        value[key] = void 0;\n      }\n    }\n  }\n\n  return value;\n};\n\nexport const stringifyState = (value: any) => {\n  return JSON.stringify(value, replacer);\n};\n\nexport const parseState = (value: string) => {\n  return JSON.parse(\n    value,\n    value.indexOf(JSON_UNDEFINED) >= 0 ? reviver : void 0,\n  );\n};\n"
  },
  {
    "path": "src/utils/symbol-observable.ts",
    "content": "import { FUNCTION } from './is-type';\n\n/**\n * Inlined version of the `symbol-observable` polyfill\n * @link https://github.com/reduxjs/redux/blob/master/src/utils/symbol-observable.ts\n */\nexport const $$observable: typeof Symbol.observable =\n  (typeof Symbol === FUNCTION && Symbol.observable) ||\n  ('@@observable' as unknown as typeof Symbol.observable);\n"
  },
  {
    "path": "src/utils/to-args.ts",
    "content": "const slice = Array.prototype.slice;\n\nexport const toArgs = <T = any[]>(args: IArguments, start?: number): T =>\n  slice.call(args, start) as unknown as T;\n"
  },
  {
    "path": "src/utils/to-promise.ts",
    "content": "export const toPromise = <T>(fn: () => T | Promise<T>): Promise<T> => {\n  return Promise.resolve().then(fn);\n};\n"
  },
  {
    "path": "test/__snapshots__/serialize.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`can clone null and undefined 1`] = `\"{\"x\":[\"__JSON_UNDEFINED__\",1,{\"test\":\"__JSON_UNDEFINED__\",\"test1\":\"hello\"},null,\"__JSON_UNDEFINED__\"],\"y\":null,\"z\":\"__JSON_UNDEFINED__\"}\"`;\n\nexports[`can clone null and undefined 2`] = `\n{\n  \"x\": [\n    undefined,\n    1,\n    {\n      \"test\": undefined,\n      \"test1\": \"hello\",\n    },\n    null,\n    undefined,\n  ],\n  \"y\": null,\n  \"z\": undefined,\n}\n`;\n"
  },
  {
    "path": "test/action-in-action.test.tsx",
    "content": "import { act, render } from '@testing-library/react';\nimport { FC, useEffect, version } from 'react';\nimport sleep from 'sleep-promise';\nimport { defineModel, FocaProvider, store, useModel } from '../src';\n\nconst model = defineModel('aia' + Math.random(), {\n  initialState: {\n    open: false,\n    count: 1,\n  },\n  reducers: {\n    plus(state) {\n      state.count += 1;\n    },\n    toggle(state) {\n      state.open = !state.open;\n    },\n  },\n});\n\nconst OtherComponent: FC = () => {\n  useEffect(() => {\n    model.plus();\n  }, []);\n  return null;\n};\n\nconst App: FC = () => {\n  const { open } = useModel(model);\n  return <>{open && <OtherComponent />}</>;\n};\n\ntest.runIf(version.split('.')[0] === '18').each([true, false])(\n  `[legacy: %s] forceUpdate should not cause action in action error`,\n  async (legacy) => {\n    store.init();\n\n    render(\n      <FocaProvider>\n        <App />\n      </FocaProvider>,\n      {\n        legacyRoot: legacy,\n      },\n    );\n\n    // console.error\n    // Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.\n    const spy = vitest.spyOn(console, 'error').mockImplementation(() => {});\n\n    await expect(\n      Promise.all(\n        Array(3)\n          .fill('')\n          .map((_, i) =>\n            act(async () => {\n              await sleep(i * 2);\n              model.toggle();\n            }),\n          ),\n      ),\n    ).resolves.toStrictEqual(Array(3).fill(void 0));\n\n    // 等待useEffect\n    await sleep(1000);\n    store.unmount();\n    spy.mockRestore();\n  },\n);\n"
  },
  {
    "path": "test/build.test.ts",
    "content": "import { writeFileSync } from 'fs';\nimport { execSync, exec } from 'child_process';\n\nfunction testFile(filename: string, expectCode: number) {\n  return new Promise((resolve) => {\n    const child = exec(`node ${filename}`);\n    child.on('exit', (code) => {\n      try {\n        expect(code).toBe(expectCode);\n      } finally {\n        resolve(code);\n      }\n    });\n  });\n}\n\nbeforeEach(() => {\n  execSync('npx tsup');\n}, 10000);\n\ntest('ESM with type=module', async () => {\n  await testFile('dist/esm/index.js', 0);\n});\n\ntest('ESM with type=commonjs', async () => {\n  writeFileSync('dist/esm/package.json', '{\"type\": \"commonjs\"}');\n  await testFile('dist/esm/index.js', 1);\n});\n\ntest('pure commonjs', async () => {\n  await testFile('dist/index.js', 0);\n});\n"
  },
  {
    "path": "test/clone.test.ts",
    "content": "import { defineModel, cloneModel, store } from '../src';\nimport type { InternalModel } from '../src/model/types';\nimport { basicModel } from './models/basic.model';\n\nlet modelIndex = 0;\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('Model can be cloned', async () => {\n  const model = cloneModel('model' + ++modelIndex, basicModel);\n\n  expect(model.state.hello).toBe('world');\n  expect(model.state.count).toBe(0);\n\n  model.plus(11);\n  expect(model.state.count).toBe(11);\n\n  await expect(model.foo('earth', 5)).resolves.toBe('OK');\n  expect(model.state.hello).toBe('earth');\n  expect(model.state.count).toBe(16);\n});\n\ntest('Cloned model name', () => {\n  const model = cloneModel('model' + ++modelIndex, basicModel);\n  expect(model.name).toBe('model' + modelIndex);\n});\n\ntest('Clone model with same name is invalid', () => {\n  const model = defineModel(Date.now().toString(), { initialState: {} });\n  expect(() => cloneModel(model.name, model)).toThrowError();\n});\n\ntest('Override state', () => {\n  const model = cloneModel('model' + ++modelIndex, basicModel, {\n    initialState: {\n      count: 20,\n      hello: 'cat',\n    },\n  }) as unknown as InternalModel;\n\n  expect(model._$opts.initialState).toStrictEqual({\n    count: 20,\n    hello: 'cat',\n  });\n});\n\ntest('Override persist', () => {\n  const model1 = defineModel('model' + ++modelIndex, {\n    initialState: {},\n    persist: {\n      dump: (state) => state,\n      load: (state) => state,\n    },\n    methods: {\n      cc() {\n        return 3;\n      },\n    },\n  });\n\n  const model2 = cloneModel('model' + ++modelIndex, model1, {\n    persist: {},\n  }) as unknown as InternalModel;\n\n  expect(model2._$opts.persist).not.toHaveProperty('maxAge');\n\n  const model3 = cloneModel('model' + ++modelIndex, model1, (prev) => {\n    return {\n      persist: {\n        ...prev.persist,\n        maxAge: 30,\n      },\n    };\n  }) as unknown as InternalModel;\n\n  expect(model3._$opts.persist).toHaveProperty('maxAge');\n  expect(model3._$opts.persist).toHaveProperty('dump');\n  expect(model3._$opts.persist).toHaveProperty('load');\n});\n\ntest('override methods or unknown option can cause error', () => {\n  const model = defineModel('model' + ++modelIndex, { initialState: {} });\n\n  expect(() =>\n    cloneModel('a', model, {\n      // @ts-expect-error\n      reducers: {},\n    }),\n  ).toThrowError();\n\n  expect(() =>\n    cloneModel('b', model, {\n      // @ts-expect-error\n      methods: {},\n    }),\n  ).toThrowError();\n\n  expect(() =>\n    cloneModel('c', model, {\n      // @ts-expect-error\n      computed: {},\n    }),\n  ).toThrowError();\n\n  expect(() =>\n    cloneModel('d', model, {\n      // @ts-expect-error\n      whateverblabla: {},\n    }),\n  ).toThrowError();\n});\n"
  },
  {
    "path": "test/computed.test.ts",
    "content": "import { defineModel, store } from '../src';\nimport { ComputedValue } from '../src/reactive/computed-value';\nimport { depsCollector } from '../src/reactive/deps-collector';\nimport { ObjectDeps } from '../src/reactive/object-deps';\nimport { computedModel } from './models/computed.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('Get computed value', () => {\n  expect(computedModel.fullName()).toBe('ticktock');\n  computedModel.changeFirstName('hello');\n  expect(computedModel.fullName()).toBe('hellotock');\n  computedModel.changeLastName('world');\n  expect(computedModel.fullName()).toBe('helloworld');\n});\n\ntest('Get computed value in computed function', () => {\n  expect(computedModel.testDependentOtherComputed()).toBe('ticktock [online]');\n});\n\ntest('Can return correct result from Object.keys', () => {\n  expect(computedModel.testObjectKeys()).toMatchObject(\n    expect.arrayContaining(['online', 'offline']),\n  );\n});\n\ntest('can enum all items for find method', () => {\n  expect(computedModel.testFind()).toBe('offline');\n});\n\ntest('can visit array item', () => {\n  expect(computedModel.testVisitArray()[0]).toBe('online');\n});\n\ntest('can return correct length from array', () => {\n  expect(computedModel.testArrayLength()).toBe(2);\n});\n\ntest('Can throw error with circularly reference', () => {\n  const model = defineModel('computed-cycle-usage', {\n    initialState: {},\n    computed: {\n      a() {\n        this.b();\n      },\n      b() {\n        this.c();\n      },\n      c() {\n        this.a();\n      },\n    },\n  });\n\n  expect(() => model.a()).toThrowError('循环引用');\n  expect(() => model.b()).toThrowError('循环引用');\n  expect(() => model.c()).toThrowError('循环引用');\n});\n\ntest('Can visit compute value from methods', () => {\n  expect(computedModel.effectsGetFullName()).toBe('ticktock');\n});\n\ntest('Split the deps for same getters', () => {\n  let mockState = { a: { b: { c: 'd' } } };\n  const mockStore = {\n    getState() {\n      return {\n        x: mockState,\n      };\n    },\n  };\n\n  let deps = depsCollector.produce(() => {\n    const proxy = new ObjectDeps(mockStore, 'x');\n    const proxyState = proxy.start(mockState);\n    proxyState.a.b;\n    proxyState.a.b.c;\n  });\n  expect(deps).toHaveLength(2);\n\n  deps = depsCollector.produce(() => {\n    const proxy = new ObjectDeps(mockStore, 'x');\n    const proxyState = proxy.start(mockState);\n    proxyState.a.b;\n  });\n  expect(deps).toHaveLength(1);\n});\n\ntest('Dirty deps never turn to clean', () => {\n  let mockState = { a: { b: { c: 'd' } } };\n  const mockStore = {\n    getState() {\n      return {\n        x: mockState,\n      };\n    },\n  };\n\n  let deps = depsCollector.produce(() => {\n    const proxy = new ObjectDeps(mockStore, 'x');\n    const proxyState = proxy.start(mockState);\n    proxyState.a.b;\n    proxyState.a.b.c;\n  });\n\n  expect(deps[0]!.isDirty()).toBeFalsy();\n  expect(deps[1]!.isDirty()).toBeFalsy();\n\n  mockState = { a: { b: { c: 'd' } } };\n  expect(deps[0]!.isDirty()).toBeTruthy();\n  expect(deps[1]!.isDirty()).toBeFalsy();\n\n  // @ts-expect-error\n  mockState = { a: { b: 'm' } };\n  expect(deps[0]!.isDirty()).toBeTruthy();\n  expect(deps[1]!.isDirty()).toBeTruthy();\n\n  mockState = { a: { b: { c: 'e' } } };\n  expect(deps[0]!.isDirty()).toBeTruthy();\n  expect(deps[1]!.isDirty()).toBeTruthy();\n\n  mockState = { a: { b: { c: 'e' } } };\n  expect(deps[0]!.isDirty()).toBeTruthy();\n  expect(deps[1]!.isDirty()).toBeTruthy();\n});\n\ntest('visit proxy state without collecting mode should not collect deps', () => {\n  let mockState = { a: { b: { c: 'd' } } };\n  const mockStore = {\n    getState() {\n      return {\n        x: mockState,\n      };\n    },\n  };\n\n  const spy = vitest.spyOn(depsCollector, 'append');\n\n  let proxyState!: typeof mockState;\n  depsCollector.produce(() => {\n    proxyState = new ObjectDeps(mockStore, 'x').start(mockState);\n    proxyState.a;\n    proxyState.a.b;\n  });\n  expect(spy).toHaveBeenCalledTimes(2);\n\n  spy.mockClear();\n  expect(proxyState.a).toMatchObject({\n    b: {\n      c: 'd',\n    },\n  });\n  expect(proxyState.a.b).toMatchObject({\n    c: 'd',\n  });\n  expect(spy).toHaveBeenCalledTimes(0);\n\n  spy.mockClear();\n  depsCollector.produce(() => {\n    const custom = new ObjectDeps(mockStore, 'x').start(mockState);\n    custom.a;\n\n    proxyState.a;\n    proxyState.a.b;\n    proxyState.a.b.c;\n  });\n  expect(spy).toHaveBeenCalledTimes(1);\n});\n\ntest('ComputedValue can remove duplicated deps', () => {\n  const mockStore = {\n    getState() {\n      return {\n        [computedModel.name]: store.getState()[computedModel.name],\n      };\n    },\n  };\n\n  const computedValue = new ComputedValue(\n    mockStore,\n    computedModel.name,\n    'prop',\n    () => {\n      computedModel.state.firstName;\n      computedModel.state.firstName;\n      computedModel.state.lastName;\n      computedModel.fullName();\n      computedModel.fullName();\n    },\n  );\n\n  // Collecting\n  computedValue.value;\n\n  expect(computedValue.deps).toHaveLength(3);\n});\n\ntest('计算属性包含子计算属性时，子计算属性被作为一个整体', () => {\n  const mockStore = {\n    getState() {\n      return {\n        [computedModel.name]: store.getState()[computedModel.name],\n      };\n    },\n  };\n\n  const computedValue = new ComputedValue(\n    mockStore,\n    computedModel.name,\n    'prop',\n    () => {\n      return computedModel.fullName() + 1;\n    },\n  );\n\n  expect(computedValue.value).toBe('ticktock1');\n  computedModel.changeFirstName('hello-');\n  expect(computedValue.isDirty()).toBeTruthy();\n  expect(computedValue.value).toBe('hello-tock1');\n});\n\ntest('only execute computed function when deps changed', () => {\n  const spy = vitest.fn().mockImplementation(() => {\n    model.state.a;\n  });\n\n  const model = defineModel('x' + Math.random(), {\n    initialState: {\n      a: 0,\n      b: 2,\n    },\n    reducers: {\n      updateA(state) {\n        state.a += 1;\n      },\n      updateB(state) {\n        state.b += 1;\n      },\n    },\n    computed: {\n      testa: spy,\n    },\n  });\n\n  expect(spy).toBeCalledTimes(0);\n\n  model.testa();\n  expect(spy).toBeCalledTimes(1);\n\n  model.testa();\n  expect(spy).toBeCalledTimes(1);\n\n  model.updateB();\n  model.testa();\n  expect(spy).toBeCalledTimes(1);\n\n  model.updateA();\n  model.testa();\n  expect(spy).toBeCalledTimes(2);\n\n  model.testa();\n  expect(spy).toBeCalledTimes(2);\n});\n\ntest('Can handle JSON.stringify', () => {\n  expect(computedModel.testJSON()).toBe(JSON.stringify(computedModel.state));\n});\n\ntest('Fail to set value on proxy state', () => {\n  expect(() => computedModel.testExtendObject()).toThrowError();\n  expect(() => computedModel.testModifyValue()).toThrowError();\n});\n\ntest('no private computed value', () => {\n  // @ts-expect-error\n  expect(computedModel._privateFullname).toBeUndefined();\n});\n\ntest('support parameters', () => {\n  expect(computedModel.withParameter(31)).toBe('tick-age-31');\n  expect(computedModel.withDefaultParameter()).toBe('tick-age-20');\n  expect(computedModel.withMultipleParameters(50, 'adddddr')).toBe(\n    'tick-age-50-addr-adddddr',\n  );\n  expect(computedModel.withMultipleAndDefaultParameters(50)).toBe(\n    'tick-age-50-addr-undefined',\n  );\n});\n\ntest('never re-calculate value with same parameters', () => {\n  const spy = vitest.fn();\n  const model = defineModel('computed-with-params', {\n    initialState: { name: 'x' },\n    computed: {\n      myData(age: number, coding: boolean) {\n        spy();\n        return this.state.name + '-' + age + String(coding);\n      },\n    },\n  });\n\n  model.myData(20, true);\n  expect(spy).toBeCalledTimes(1);\n  model.myData(20, true);\n  model.myData(20, true);\n  model.myData(20, true);\n  expect(spy).toBeCalledTimes(1);\n  model.myData(20, false);\n  expect(spy).toBeCalledTimes(2);\n  model.myData(20, false);\n  expect(spy).toBeCalledTimes(2);\n  model.myData(34, false);\n  expect(spy).toBeCalledTimes(3);\n  model.myData(20, false); // cache\n  expect(spy).toBeCalledTimes(3);\n\n  spy.mockRestore();\n});\n\ntest('remove computed value for cache always be skipped', () => {\n  const spy = vitest.fn();\n  const model = defineModel('computed-with-remove-cache', {\n    initialState: { name: 'x' },\n    computed: {\n      myData(age: number, coding: boolean) {\n        spy();\n        return this.state.name + '-' + age + String(coding);\n      },\n    },\n  });\n\n  for (let i = 0; i < 30; ++i) {\n    model.myData(i, true);\n  }\n  expect(spy).toBeCalledTimes(30);\n  spy.mockReset();\n  model.myData(1, true);\n  expect(spy).toBeCalledTimes(1);\n  spy.mockRestore();\n});\n\ntest('complex parameter can not hit cache', () => {\n  const spy = vitest.fn();\n  const model = defineModel('computed-with-complex-parameter', {\n    initialState: { name: 'x' },\n    computed: {\n      myData(opts: object) {\n        spy();\n        return this.state.name + '-' + JSON.stringify(opts);\n      },\n    },\n  });\n\n  const obj = {};\n  model.myData(obj);\n  expect(spy).toBeCalledTimes(1);\n  model.myData(obj);\n  expect(spy).toBeCalledTimes(1);\n  model.myData({});\n  expect(spy).toBeCalledTimes(2);\n  model.myData(obj);\n  expect(spy).toBeCalledTimes(2);\n  spy.mockRestore();\n});\n\ntest('array should always be deps', () => {\n  const spy = vitest.fn();\n\n  const model = defineModel('computed-from-array', {\n    initialState: {\n      x: [{ foo: 'bar' } as { foo: string; other?: string }],\n      y: {},\n    },\n    reducers: {\n      update(state, other: string) {\n        state.x = [{ foo: 'bar' }, { foo: 'baz', other }];\n      },\n    },\n    computed: {\n      myData() {\n        spy();\n        return this.state.x.filter((item) => item.foo === 'bar');\n      },\n    },\n  });\n\n  model.myData();\n  expect(spy).toBeCalledTimes(1);\n  model.update('baz');\n  model.myData();\n  expect(spy).toBeCalledTimes(2);\n  model.update('x');\n  model.myData();\n  expect(spy).toBeCalledTimes(3);\n  model.update('y');\n  model.myData();\n  expect(spy).toBeCalledTimes(4);\n});\n\ntest('re-calculate when path changed', () => {\n  const spy = vitest.fn();\n\n  const model = defineModel('re-calculate-path-changed', {\n    initialState: {\n      foo: { bar: undefined } as undefined | { bar: string | undefined },\n      baz: '123',\n    },\n    reducers: {\n      updateFoo(state, foo: undefined | { bar: string | undefined }) {\n        state.foo = foo;\n      },\n    },\n    computed: {\n      myData() {\n        const foo = this.state.foo;\n        spy();\n        return foo ? foo.bar : this.state.baz;\n      },\n    },\n  });\n\n  expect(model.myData()).toBeUndefined();\n  expect(spy).toBeCalledTimes(1);\n  model.updateFoo(undefined);\n  expect(model.myData()).toBe('123');\n  expect(spy).toBeCalledTimes(2);\n  model.updateFoo({ bar: undefined });\n  expect(model.myData()).toBeUndefined();\n  expect(spy).toBeCalledTimes(3);\n  model.updateFoo({ bar: 'abc' });\n  expect(model.myData()).toBe('abc');\n  expect(spy).toBeCalledTimes(4);\n  model.updateFoo({ bar: undefined });\n  expect(model.myData()).toBeUndefined();\n  expect(spy).toBeCalledTimes(5);\n});\n"
  },
  {
    "path": "test/connect.test.tsx",
    "content": "import { FC } from 'react';\nimport { act, render, screen } from '@testing-library/react';\nimport { store, connect, FocaProvider, getLoading } from '../src';\nimport { basicModel } from './models/basic.model';\nimport { complexModel } from './models/complex.model';\n\nlet App: FC<ReturnType<typeof mapStateToProps>> = ({ count, loading }) => {\n  return (\n    <>\n      <div data-testid=\"count\">{count}</div>\n      <div data-testid=\"loading\">{loading.toString()}</div>\n    </>\n  );\n};\n\nconst mapStateToProps = () => {\n  return {\n    count: basicModel.state.count + complexModel.state.ids.length,\n    loading: getLoading(basicModel.pureAsync),\n  };\n};\n\nconst Wrapped = connect(mapStateToProps)(App);\n\nconst Root: FC = () => {\n  return (\n    <FocaProvider>\n      <Wrapped />\n    </FocaProvider>\n  );\n};\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('Get state from connect', async () => {\n  render(<Root />);\n  const $count = screen.queryByTestId('count')!;\n  const $loading = screen.queryByTestId('loading')!;\n\n  expect($count.innerHTML).toBe('0');\n  expect($loading.innerHTML).toBe('false');\n\n  act(() => {\n    basicModel.plus(0);\n  });\n  expect($count.innerHTML).toBe('0');\n\n  act(() => {\n    basicModel.plus(1);\n  });\n  expect($count.innerHTML).toBe('1');\n\n  act(() => {\n    basicModel.plus(20.5);\n  });\n  expect($count.innerHTML).toBe('21.5');\n\n  act(() => {\n    complexModel.addUser(40, '');\n  });\n  expect($count.innerHTML).toBe('22.5');\n\n  let promise!: Promise<any>;\n\n  act(() => {\n    promise = basicModel.pureAsync();\n  });\n  expect($loading.innerHTML).toBe('true');\n\n  await act(async () => {\n    await promise;\n  });\n  expect($loading.innerHTML).toBe('false');\n});\n"
  },
  {
    "path": "test/deep-equal.test.ts",
    "content": "import { deepEqual } from '../src/utils/deep-equal';\nimport { equals } from './fixtures/equals';\nimport { notEquals } from './fixtures/not-equals';\n\nObject.entries(equals).map(([title, { a, b }]) => {\n  test(`[equal] ${title}`, () => {\n    expect(deepEqual(a, b)).toBeTruthy();\n  });\n});\n\nObject.entries(notEquals).map(([title, { a, b }]) => {\n  test(`[not equal] ${title}`, () => {\n    expect(deepEqual(a, b)).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "test/engine.test.ts",
    "content": "import 'fake-indexeddb/auto';\nimport localforage from 'localforage';\nimport ReactNativeStorage from '@react-native-async-storage/async-storage';\nimport { toPromise } from '../src/utils/to-promise';\nimport { memoryStorage } from '../src';\n\nconst storages = [\n  [localStorage, 'local'],\n  [sessionStorage, 'session'],\n  [memoryStorage, 'memory'],\n  [\n    localforage.createInstance({ driver: localforage.LOCALSTORAGE }),\n    'localforage local',\n  ],\n  [\n    localforage.createInstance({ driver: localforage.INDEXEDDB }),\n    'localforage indexedDb',\n  ],\n  [ReactNativeStorage, 'react-native'],\n] as const;\n\ndescribe.each(storages)('storage io', (storage, name) => {\n  beforeEach(() => storage.clear());\n  afterEach(() => storage.clear());\n\n  test(`[${name}] Get and set data`, async () => {\n    await expect(toPromise(() => storage.getItem('test1'))).resolves.toBeNull();\n    await storage.setItem('test1', 'yes');\n    await expect(toPromise(() => storage.getItem('test1'))).resolves.toBe(\n      'yes',\n    );\n  });\n\n  test(`[${name}] Update data`, async () => {\n    await storage.setItem('test2', 'yes');\n    await expect(toPromise(() => storage.getItem('test2'))).resolves.toBe(\n      'yes',\n    );\n    await storage.setItem('test2', 'no');\n    await expect(toPromise(() => storage.getItem('test2'))).resolves.toBe('no');\n  });\n\n  test(`[${name}] Delete data`, async () => {\n    await storage.setItem('test3', 'yes');\n    await expect(toPromise(() => storage.getItem('test3'))).resolves.toBe(\n      'yes',\n    );\n    await storage.removeItem('test3');\n    await expect(toPromise(() => storage.getItem('test3'))).resolves.toBeNull();\n  });\n\n  test(`[${name}] Clear all data`, async () => {\n    await storage.setItem('test4', 'yes');\n    await storage.setItem('test5', 'yes');\n\n    await storage.clear();\n\n    await expect(toPromise(() => storage.getItem('test4'))).resolves.toBeNull();\n    await expect(toPromise(() => storage.getItem('test5'))).resolves.toBeNull();\n  });\n});\n"
  },
  {
    "path": "test/fixtures/equals.ts",
    "content": "export const equals: Record<string, { a: any; b: any }> = {\n  '0 and -0': {\n    a: 0,\n    b: -0,\n  },\n  'NaN': {\n    a: NaN,\n    b: NaN,\n  },\n  'number': {\n    a: 1,\n    b: 1,\n  },\n  'string': {\n    a: 'x',\n    b: 'x',\n  },\n  'empty array': {\n    a: [],\n    b: [],\n  },\n  'array': {\n    a: [1, 2, 'x'],\n    b: [1, 2, 'x'],\n  },\n  'complex array': {\n    a: [1, { x: { y: 2, z: 3, x: [3, 6] } }, 5],\n    b: [1, { x: { y: 2, z: 3, x: [3, 6] } }, 5],\n  },\n  'object': {\n    a: { x: 1, y: 2 },\n    b: { x: 1, y: 2 },\n  },\n  'complex object': {\n    a: {\n      x: {\n        y: {\n          z: [3, 6],\n          p: [\n            {\n              m: 6,\n            },\n          ],\n        },\n      },\n    },\n    b: {\n      x: {\n        y: {\n          z: [3, 6],\n          p: [\n            {\n              m: 6,\n            },\n          ],\n        },\n      },\n    },\n  },\n  'object without prototype': {\n    a: Object.create(null),\n    b: Object.create(null),\n  },\n};\n"
  },
  {
    "path": "test/fixtures/not-equals.ts",
    "content": "export const notEquals: Record<string, { a: any; b: any }> = {\n  '0 and NaN': {\n    a: 0,\n    b: NaN,\n  },\n  '0 and null': {\n    a: 0,\n    b: null,\n  },\n  '0 and undefined': {\n    a: 0,\n    b: undefined,\n  },\n  '0 and false': {\n    a: 0,\n    b: false,\n  },\n  'null and undefined': {\n    a: null,\n    b: undefined,\n  },\n  'array and arrayLike': {\n    a: [],\n    b: { length: 0 },\n  },\n  'array and arrayLike with length in prototype': {\n    a: [],\n    b: Object.create({ length: 0 }),\n  },\n  'array and arrayLike with items': {\n    a: [1, 'x'],\n    b: { 0: 1, 1: 'x', length: 2 },\n  },\n  'number': {\n    a: 1,\n    b: 2,\n  },\n  'string': {\n    a: 'x',\n    b: 'y',\n  },\n  'number and object': {\n    a: 1,\n    b: {},\n  },\n  'number and array': {\n    a: 3,\n    b: [],\n  },\n  'object and array': {\n    a: [],\n    b: {},\n  },\n  'array': {\n    a: [1],\n    b: [2],\n  },\n  'complex array': {\n    a: [1, { x: { y: 2, z: 3, x: [3] } }, 5],\n    b: [1, { x: { y: 2, z: 3, x: [3, 6] } }, 5],\n  },\n  'array with different length': {\n    a: [1],\n    b: [1, 2, 3],\n  },\n  'object': {\n    a: { x: 1 },\n    b: { x: 2 },\n  },\n  'complex object': {\n    a: {\n      x: {\n        y: {\n          z: [3, 6],\n        },\n      },\n    },\n    b: {\n      x: {\n        y: {\n          z: [3, 5],\n        },\n      },\n    },\n  },\n  'object with different properties': {\n    a: { x: 1 },\n    b: { x: 1, y: 2 },\n  },\n  'with different constructor': {\n    a: new (class {})(),\n    b: new (class {})(),\n  },\n  'Object.keys() get same length but not own property': {\n    a: { x: 3, y: 4 },\n    b: (() => {\n      const b = Object.create({ x: 3 });\n      b.y = 4;\n      b.z = 5;\n      return b;\n    })(),\n  },\n};\n"
  },
  {
    "path": "test/get-loading.test.ts",
    "content": "import sleep from 'sleep-promise';\nimport { defineModel, getLoading, store } from '../src';\nimport { basicModel } from './models/basic.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('Collect loading status for method', async () => {\n  expect(getLoading(basicModel.bos)).toBeFalsy();\n  const promise = basicModel.bos();\n  expect(getLoading(basicModel.bos)).toBeTruthy();\n  await promise;\n  expect(getLoading(basicModel.bos)).toBeFalsy();\n});\n\ntest('Collect error message for method', async () => {\n  expect(getLoading(basicModel.hasError)).toBeFalsy();\n\n  const promise = basicModel.hasError();\n  expect(getLoading(basicModel.hasError)).toBeTruthy();\n\n  await expect(promise).rejects.toThrowError('my-test');\n\n  expect(getLoading(basicModel.hasError)).toBeFalsy();\n});\n\ntest('Trace loadings', async () => {\n  expect(getLoading(basicModel.bos.room, 'x')).toBeFalsy();\n  expect(getLoading(basicModel.bos.room).find('x')).toBeFalsy();\n\n  const promise = basicModel.bos.room('x').execute();\n  expect(getLoading(basicModel.bos.room, 'x')).toBeTruthy();\n  expect(getLoading(basicModel.bos.room).find('x')).toBeTruthy();\n  expect(getLoading(basicModel.bos.room, 'y')).toBeFalsy();\n  expect(getLoading(basicModel.bos.room).find('y')).toBeFalsy();\n  expect(getLoading(basicModel.bos)).toBeFalsy();\n\n  await promise;\n  expect(getLoading(basicModel.bos.room, 'x')).toBeFalsy();\n  expect(getLoading(basicModel.bos.room).find('x')).toBeFalsy();\n});\n\ntest('async method in model.onInit should be activated automatically', async () => {\n  const hookModel = defineModel('loading' + Math.random(), {\n    initialState: {},\n    methods: {\n      async myMethod() {\n        await sleep(200);\n      },\n      async myMethod2() {\n        await sleep(200);\n      },\n    },\n    events: {\n      async onInit() {\n        await this.myMethod();\n        await this.myMethod2();\n      },\n    },\n  });\n  await store.onInitialized();\n  expect(getLoading(hookModel.myMethod)).toBeTruthy();\n  await sleep(220);\n  expect(getLoading(hookModel.myMethod)).toBeFalsy();\n\n  expect(getLoading(hookModel.myMethod2)).toBeTruthy();\n  await sleep(220);\n  expect(getLoading(hookModel.myMethod2)).toBeFalsy();\n});\n"
  },
  {
    "path": "test/helpers/render-hook.tsx",
    "content": "import { renderHook as originRenderHook } from '@testing-library/react';\nimport { FocaProvider } from '../../src';\n\nexport const renderHook: typeof originRenderHook = (\n  renderCallback,\n  options,\n) => {\n  return originRenderHook(renderCallback, {\n    wrapper: FocaProvider,\n    ...options,\n  });\n};\n"
  },
  {
    "path": "test/helpers/slow-engine.ts",
    "content": "import type { StorageEngine } from '../../src';\nimport { toPromise } from '../../src/utils/to-promise';\n\nlet cache: Partial<Record<string, string>> = {};\n\nexport const slowEngine: StorageEngine = {\n  getItem(key) {\n    return new Promise((resolve) => {\n      setTimeout(() => {\n        resolve(cache[key] === void 0 ? null : cache[key]!);\n      }, 300);\n    });\n  },\n  setItem(key, value) {\n    return toPromise(() => {\n      cache[key] = value;\n    });\n  },\n  removeItem(key) {\n    return toPromise(() => {\n      cache[key] = void 0;\n    });\n  },\n  clear() {\n    return toPromise(() => {\n      cache = {};\n    });\n  },\n};\n"
  },
  {
    "path": "test/lifecycle.test.ts",
    "content": "import sleep from 'sleep-promise';\nimport { cloneModel, defineModel, memoryStorage, store } from '../src';\nimport { PersistSchema } from '../src/persist/persist-item';\n\ndescribe('onInit', () => {\n  afterEach(() => {\n    store.unmount();\n  });\n\n  const createModel = () => {\n    return defineModel('events' + Math.random(), {\n      initialState: { count: 0 },\n      methods: {\n        invokeByReadyHook() {\n          this.setState((state) => {\n            state.count += 101;\n          });\n        },\n      },\n      events: {\n        onInit() {\n          this.invokeByReadyHook();\n        },\n      },\n    });\n  };\n\n  test('trigger ready events on store ready', async () => {\n    const hookModel = createModel();\n    store.init();\n\n    const hook2Model = createModel();\n    const clonedModel = cloneModel('events' + Math.random(), hookModel);\n\n    await Promise.resolve();\n\n    expect(hookModel.state.count).toBe(101);\n    expect(hook2Model.state.count).toBe(101);\n    expect(clonedModel.state.count).toBe(101);\n  });\n\n  test('trigger ready events on store and persist ready', async () => {\n    const hookModel = createModel();\n\n    await memoryStorage.setItem(\n      'mm:z',\n      JSON.stringify(<PersistSchema>{\n        v: 1,\n        d: {\n          [hookModel.name]: {\n            v: 0,\n            d: JSON.stringify({ count: 20 }),\n          },\n        },\n      }),\n    );\n\n    store.init({\n      persist: [\n        {\n          key: 'z',\n          keyPrefix: 'mm:',\n          version: 1,\n          engine: memoryStorage,\n          models: [hookModel],\n        },\n      ],\n    });\n\n    const hook2Model = createModel();\n    const clonedModel = cloneModel('events' + Math.random(), hookModel);\n\n    expect(hookModel.state.count).toBe(0);\n    expect(hook2Model.state.count).toBe(0);\n    expect(clonedModel.state.count).toBe(0);\n\n    await store.onInitialized();\n\n    expect(hookModel.state.count).toBe(101 + 20);\n    expect(hook2Model.state.count).toBe(101);\n    expect(clonedModel.state.count).toBe(101);\n  });\n\n  test('should call modelPreInit and modelPostInit', async () => {\n    const hookModel = createModel();\n    let publishCount = 0;\n    const token1 = store.topic.subscribe('modelPreInit', (modelName) => {\n      if (modelName === hookModel.name) {\n        publishCount += 1;\n      }\n    });\n    const token2 = store.topic.subscribe('modelPostInit', (modelName) => {\n      if (modelName === hookModel.name) {\n        publishCount += 0.4;\n      }\n    });\n\n    store.init();\n    await store.onInitialized();\n    expect(publishCount).toBe(1 + 0.4);\n    token1.unsubscribe();\n    token2.unsubscribe();\n  });\n\n  test('should call modelPreInit and modelPostInit for promise returning', async () => {\n    const hookModel = defineModel('events' + Math.random(), {\n      initialState: {},\n      events: {\n        async onInit() {\n          await sleep(200);\n        },\n      },\n    });\n    let publishCount = 0;\n    const token1 = store.topic.subscribe('modelPreInit', (modelName) => {\n      if (modelName === hookModel.name) {\n        publishCount += 1;\n      }\n    });\n    const token2 = store.topic.subscribe('modelPostInit', (modelName) => {\n      if (modelName === hookModel.name) {\n        publishCount += 0.4;\n      }\n    });\n\n    store.init();\n    await store.onInitialized();\n    expect(publishCount).toBe(1);\n    await sleep(210);\n    expect(publishCount).toBe(1 + 0.4);\n    token1.unsubscribe();\n    token2.unsubscribe();\n  });\n});\n\ndescribe('onChange', () => {\n  beforeEach(() => {\n    store.init();\n  });\n\n  afterEach(() => {\n    store.unmount();\n  });\n\n  test('onChange should call after onInit', async () => {\n    let testMessage = '';\n    const model = defineModel('events' + Math.random(), {\n      initialState: { count: 0 },\n      reducers: {\n        plus(state) {\n          state.count += 1;\n        },\n        minus(state) {\n          state.count -= 1;\n        },\n      },\n      methods: {\n        _invokeByReadyHook() {\n          this.setState((state) => {\n            state.count += 2;\n          });\n        },\n      },\n      events: {\n        onInit() {\n          testMessage += 'onInit-';\n          this._invokeByReadyHook();\n        },\n        onChange(prevState, nextState) {\n          testMessage += `prev-${prevState.count}-next-${nextState.count}-`;\n        },\n      },\n    });\n    model.plus();\n    model.minus();\n    expect(testMessage).toBe('');\n\n    await store.onInitialized();\n\n    expect(testMessage).toBe('onInit-prev-0-next-2-');\n    model.plus();\n    expect(testMessage).toBe('onInit-prev-0-next-2-prev-2-next-3-');\n    store.refresh();\n    expect(testMessage).toBe(\n      'onInit-prev-0-next-2-prev-2-next-3-prev-3-next-0-',\n    );\n  });\n});\n\ndescribe('onDestroy', () => {\n  beforeEach(() => {\n    store.init();\n  });\n\n  afterEach(() => {\n    store.unmount();\n  });\n\n  test('call onDestroy when invoke store.destroy()', async () => {\n    const spy = vitest.fn();\n    const model = defineModel('events' + Math.random(), {\n      initialState: { count: 0 },\n      reducers: {\n        update(state) {\n          state.count += 1;\n        },\n      },\n      events: {\n        onDestroy: spy,\n      },\n    });\n\n    await store.onInitialized();\n\n    model.update();\n    expect(spy).toBeCalledTimes(0);\n    store['removeReducer'](model.name);\n    expect(spy).toBeCalledTimes(1);\n    spy.mockRestore();\n  });\n\n  test('should not call onChange', async () => {\n    const destroySpy = vitest.fn();\n    const changeSpy = vitest.fn();\n    const model = defineModel('events' + Math.random(), {\n      initialState: { count: 0 },\n      reducers: {\n        update(state) {\n          state.count += 1;\n        },\n      },\n      events: {\n        onChange: changeSpy,\n        onDestroy: destroySpy,\n      },\n    });\n\n    await store.onInitialized();\n\n    model.update();\n    expect(destroySpy).toBeCalledTimes(0);\n    expect(changeSpy).toBeCalledTimes(1);\n    store['removeReducer'](model.name);\n    expect(destroySpy).toBeCalledTimes(1);\n    expect(changeSpy).toBeCalledTimes(1);\n    destroySpy.mockRestore();\n  });\n});\n"
  },
  {
    "path": "test/middleware.test.ts",
    "content": "import { defineModel, getLoading, store } from '../src';\nimport { DestroyLoadingAction, DESTROY_LOADING } from '../src/actions/loading';\nimport { loadingStore } from '../src/store/loading-store';\nimport { basicModel } from './models/basic.model';\nimport { complexModel } from './models/complex.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('dispatch the same state should be intercepted', () => {\n  const fn = vitest.fn();\n  const unsubscribe = store.subscribe(fn);\n\n  expect(fn).toHaveBeenCalledTimes(0);\n  basicModel.set(100);\n  expect(fn).toHaveBeenCalledTimes(1);\n  basicModel.set(100);\n  basicModel.set(100);\n  expect(fn).toHaveBeenCalledTimes(1);\n  basicModel.set(101);\n  expect(fn).toHaveBeenCalledTimes(2);\n\n  complexModel.deleteUser(30);\n  complexModel.deleteUser(34);\n  expect(fn).toHaveBeenCalledTimes(2);\n\n  complexModel.addUser(5, 'L');\n  expect(fn).toHaveBeenCalledTimes(3);\n  complexModel.addUser(5, 'L');\n  expect(fn).toHaveBeenCalledTimes(3);\n  complexModel.addUser(5, 'LT');\n  expect(fn).toHaveBeenCalledTimes(4);\n\n  unsubscribe();\n  fn.mockRestore();\n});\n\ntest('dispatch the same loading should be intercepted', async () => {\n  const fn = vitest.fn();\n  const unsubscribe = loadingStore.subscribe(fn);\n\n  loadingStore.inactivate(basicModel.name, 'pureAsync');\n\n  expect(fn).toHaveBeenCalledTimes(0);\n  await basicModel.pureAsync();\n  await basicModel.pureAsync();\n  expect(fn).toHaveBeenCalledTimes(0);\n\n  loadingStore.activate(basicModel.name, 'pureAsync');\n\n  await basicModel.pureAsync();\n  expect(fn).toHaveBeenCalledTimes(2);\n  await basicModel.pureAsync();\n  await basicModel.pureAsync();\n  expect(fn).toHaveBeenCalledTimes(6);\n  await Promise.all([basicModel.pureAsync(), basicModel.pureAsync()]);\n  expect(fn).toHaveBeenCalledTimes(8);\n\n  unsubscribe();\n  fn.mockRestore();\n});\n\ntest('destroy model will not trigger reducer without method called', () => {\n  const spy = vitest.fn();\n  loadingStore.subscribe(spy);\n  loadingStore.dispatch<DestroyLoadingAction>({\n    type: DESTROY_LOADING,\n    model: basicModel.name,\n  });\n  expect(spy).toBeCalledTimes(0);\n  spy.mockRestore();\n});\n\ntest('destroy model will trigger reducer with method called', async () => {\n  await basicModel.pureAsync();\n  const spy = vitest.fn();\n  loadingStore.subscribe(spy);\n  loadingStore.dispatch<DestroyLoadingAction>({\n    type: DESTROY_LOADING,\n    model: basicModel.name,\n  });\n  expect(spy).toBeCalledTimes(1);\n  spy.mockRestore();\n});\n\ntest('reducer in reducer is invalid operation', () => {\n  const model1 = defineModel('aia-1', {\n    initialState: {},\n    reducers: {\n      test1() {},\n    },\n    methods: {\n      async ok() {},\n      notOk() {\n        this.test1();\n      },\n    },\n  });\n  const model2 = defineModel('aia-2', {\n    initialState: {},\n    reducers: {\n      test2() {\n        model1.test1();\n      },\n      test3() {\n        model1.ok();\n      },\n      test4() {\n        model1.notOk();\n      },\n    },\n  });\n\n  expect(() => model2.test2()).toThrowError('[dispatch]');\n\n  getLoading(model1.ok);\n  expect(() => model2.test3()).not.toThrowError();\n\n  expect(() => model2.test4()).toThrowError();\n});\n\ntest('freeze model state', () => {\n  expect(Object.isFrozen(store.getState())).toBeTruthy();\n\n  expect(Object.isFrozen(complexModel.state)).toBeTruthy();\n  expect(Object.isFrozen(complexModel.state.ids)).toBeTruthy();\n  expect(Object.isFrozen(complexModel.state.users)).toBeTruthy();\n\n  complexModel.addUser(10, 'tom');\n  expect(Object.isFrozen(complexModel.state)).toBeTruthy();\n  expect(Object.isFrozen(complexModel.state.ids)).toBeTruthy();\n  expect(Object.isFrozen(complexModel.state.users)).toBeTruthy();\n  expect(() => complexModel.state.ids.push(2)).toThrowError();\n});\n\ntest('freeze loading state', async () => {\n  expect(Object.isFrozen(loadingStore.getState())).toBeTruthy();\n  expect(Object.isFrozen(getLoading(basicModel.pureAsync))).toBeTruthy();\n\n  loadingStore.activate(basicModel.name, 'pureAsync');\n\n  const promise = basicModel.pureAsync();\n  expect(Object.isFrozen(getLoading(basicModel.pureAsync.room))).toBeTruthy();\n  await promise;\n  expect(Object.isFrozen(getLoading(basicModel.pureAsync.room))).toBeTruthy();\n});\n"
  },
  {
    "path": "test/model.test.ts",
    "content": "import { defineModel, store } from '../src';\nimport { basicModel } from './models/basic.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('Model name', () => {\n  expect(basicModel.name).toBe('basic');\n});\n\ntest('initialState should be serializable', () => {\n  const createModel = (initialState: any) => {\n    return defineModel('model' + Math.random(), { initialState });\n  };\n\n  [\n    { x: Symbol('test') },\n    [Symbol('test')],\n    { x: function () {} },\n    { x: /test/ },\n    { x: new Map() },\n    { x: new Set() },\n    { x: new Date() },\n    [new (class {})()],\n    new (class {})(),\n  ].forEach((initialState) => {\n    expect(() => createModel(initialState)).toThrowError();\n  });\n\n  [\n    { x: undefined },\n    { x: undefined, y: null },\n    { x: 0 },\n    [0, 1, '2', {}, { x: null }],\n    { x: { y: { z: [{}, {}] } } },\n  ].forEach((initialState) => {\n    createModel(initialState);\n  });\n});\n\ntest('Reset model state', () => {\n  basicModel.moreParams(3, 'earth');\n  expect(basicModel.state.count).toBe(3);\n  expect(basicModel.state.hello).toBe('world, earth');\n\n  basicModel.reset();\n  expect(basicModel.state.count).toBe(0);\n  expect(basicModel.state.hello).toBe('world');\n});\n\ntest('Call reducer', () => {\n  expect(basicModel.state.count).toBe(0);\n\n  basicModel.plus(1);\n  expect(basicModel.state.count).toBe(1);\n\n  basicModel.plus(6);\n  expect(basicModel.state.count).toBe(7);\n\n  basicModel.minus(3);\n  expect(basicModel.state.count).toBe(4);\n});\n\ntest('call reducer with multiple parameters', () => {\n  expect(basicModel.state.count).toBe(0);\n  expect(basicModel.state.hello).toBe('world');\n\n  basicModel.moreParams(13, 'timi');\n  expect(basicModel.state.count).toBe(13);\n  expect(basicModel.state.hello).toBe('world, timi');\n});\n\ntest('Set state in methods', async () => {\n  expect(basicModel.state.count).toBe(0);\n  expect(basicModel.state.hello).toBe('world');\n\n  await expect(basicModel.foo('earth', 15)).resolves.toBe('OK');\n\n  expect(basicModel.state.count).toBe(15);\n  expect(basicModel.state.hello).toBe('earth');\n});\n\ntest('Set state without function callback in methods', () => {\n  expect(basicModel.state.count).toBe(0);\n\n  basicModel.setWithoutFn(15);\n  expect(basicModel.state.count).toBe(15);\n\n  basicModel.setWithoutFn(26);\n  expect(basicModel.state.count).toBe(26);\n\n  basicModel.setWithoutFn(54.3);\n  expect(basicModel.state.count).toBe(54.3);\n});\n\ntest('set partial object state in methods', () => {\n  type State = {\n    test: { count: number };\n    hello: string | undefined;\n    name: string;\n  };\n  const model = defineModel('partial-object-model', {\n    initialState: <State>{\n      test: {\n        count: 0,\n      },\n      hello: 'world',\n      name: 'timi',\n    },\n    methods: {\n      setNothing() {\n        this.setState({});\n      },\n      setCount() {\n        this.setState({\n          test: {\n            count: 2,\n          },\n        });\n      },\n      setHello() {\n        this.setState({\n          hello: 'x',\n        });\n      },\n      setHelloByFn() {\n        this.setState(() => {\n          return {\n            hello: 'xx',\n          };\n        });\n      },\n      setNothingByFn() {\n        this.setState(() => ({}));\n      },\n      override() {\n        this.setState({\n          // @ts-expect-error\n          test: 123,\n        });\n      },\n    },\n  });\n\n  model.setCount();\n  expect(model.state.test).toStrictEqual({\n    count: 2,\n  });\n  expect(model.state.hello).toBe('world');\n\n  model.setHello();\n  expect(model.state.test).toStrictEqual({\n    count: 2,\n  });\n  expect(model.state.hello).toBe('x');\n\n  model.setNothing();\n  expect(model.state.test).toStrictEqual({\n    count: 2,\n  });\n  expect(model.state.hello).toBe('x');\n\n  model.setHelloByFn();\n  expect(model.state.test).toStrictEqual({\n    count: 2,\n  });\n  expect(model.state.hello).toStrictEqual('xx');\n\n  model.setNothingByFn();\n  expect(model.state.test).toStrictEqual({\n    count: 2,\n  });\n  expect(model.state.hello).toStrictEqual('xx');\n\n  model.override();\n  expect(model.state.test).toBe(123);\n});\n\ntest('set partial array state in methods', () => {\n  const model = defineModel('partial-array-model', {\n    initialState: ['2'],\n    methods: {\n      set() {\n        this.setState(['20', '30']);\n      },\n    },\n  });\n\n  model.set();\n  expect(model.state).toStrictEqual(['20', '30']);\n});\n\ntest('private reducer and method', () => {\n  expect(\n    // @ts-expect-error\n    basicModel._actionIsPrivate,\n  ).toBeUndefined();\n\n  expect(\n    // @ts-expect-error\n    basicModel._effectIsPrivate,\n  ).toBeUndefined();\n\n  expect(\n    // @ts-expect-error\n    basicModel.____alsoPrivateAction,\n  ).toBeUndefined();\n\n  expect(\n    // @ts-expect-error\n    basicModel.____alsoPrivateEffect,\n  ).toBeUndefined();\n\n  expect(basicModel.plus).toBeInstanceOf(Function);\n  expect(basicModel.pureAsync).toBeInstanceOf(Function);\n});\n\ntest('define duplicated method keys will throw error', () => {\n  expect(() =>\n    defineModel('x' + Math.random(), {\n      initialState: {},\n      reducers: {\n        test1() {},\n      },\n      methods: {\n        test1() {},\n        test2() {},\n      },\n    }),\n  ).toThrowError('test1');\n\n  expect(() =>\n    defineModel('x' + Math.random(), {\n      initialState: {},\n      reducers: {\n        test2() {},\n      },\n      computed: {\n        test1() {},\n        test2() {},\n      },\n    }),\n  ).toThrowError('test2');\n\n  expect(() =>\n    defineModel('x' + Math.random(), {\n      initialState: {},\n      methods: {\n        test2() {},\n      },\n      computed: {\n        test1() {},\n        test2() {},\n      },\n    }),\n  ).toThrowError('test2');\n});\n"
  },
  {
    "path": "test/models/basic.model.ts",
    "content": "import sleep from 'sleep-promise';\nimport { cloneModel, defineModel } from '../../src';\n\nconst initialState: {\n  count: number;\n  hello: string;\n} = {\n  count: 0,\n  hello: 'world',\n};\n\nexport const basicModel = defineModel('basic', {\n  initialState,\n  reducers: {\n    plus(state, step: number) {\n      state.count += step;\n    },\n    minus(state, step: number) {\n      state.count -= step;\n    },\n    moreParams(state, step: number, hello: string) {\n      state.count += step;\n      state.hello += ', ' + hello;\n    },\n    set(state, count: number) {\n      state.count = count;\n    },\n    reset() {\n      return this.initialState;\n    },\n    _actionIsPrivate() {},\n    ____alsoPrivateAction() {},\n  },\n  methods: {\n    async foo(hello: string, step: number) {\n      await sleep(20);\n\n      this.setState((state) => {\n        state.count += step;\n        state.hello = hello;\n      });\n\n      return 'OK';\n    },\n    setWithoutFn(step: number) {\n      this.setState({\n        count: step,\n        hello: 'earth',\n      });\n    },\n    setPartialState(step: number) {\n      this.setState({\n        count: step,\n      });\n    },\n    async bar() {\n      return this.foo('', 100);\n    },\n    async bos() {\n      return this.plus(4);\n    },\n    async hasError(msg: string = 'my-test') {\n      throw new Error(msg);\n    },\n    async pureAsync() {\n      await sleep(300);\n      return 'OK';\n    },\n    normalMethod() {\n      return 'YES';\n    },\n    async _effectIsPrivate() {},\n    ____alsoPrivateEffect() {},\n  },\n});\n\nexport const basicSkipRefreshModel = cloneModel(\n  'basicSkipRefresh',\n  basicModel,\n  {\n    skipRefresh: true,\n  },\n);\n"
  },
  {
    "path": "test/models/complex.model.ts",
    "content": "import { defineModel } from '../../src';\n\nconst initialState: {\n  users: Record<number, string>;\n  ids: Array<number>;\n} = {\n  users: {},\n  ids: [],\n};\n\nexport const complexModel = defineModel('complex', {\n  initialState,\n  reducers: {\n    addUser(state, id: number, name: string) {\n      state.users[id] = name;\n      !state.ids.includes(id) && state.ids.push(id);\n    },\n    deleteUser(state, id: number) {\n      delete state.users[id];\n      state.ids = state.ids.filter((item) => item !== id);\n    },\n    updateUser(state, id: number, name: string) {\n      state.users[id] = name;\n    },\n  },\n});\n"
  },
  {
    "path": "test/models/computed.model.ts",
    "content": "import { defineModel } from '../../src';\n\nconst initialState: {\n  firstName: string;\n  lastName: string;\n  statusList: [string, string];\n  translate: Record<string, string>;\n} = {\n  firstName: 'tick',\n  lastName: 'tock',\n  statusList: ['online', 'offline'],\n  translate: {\n    online: 'Online',\n    offline: 'Offline',\n  },\n};\n\nexport const computedModel = defineModel('computed-model', {\n  initialState,\n  reducers: {\n    changeFirstName(state, value: string) {\n      state.firstName = value;\n    },\n    changeLastName(state, value) {\n      state.lastName = value;\n    },\n  },\n  methods: {\n    effectsGetFullName() {\n      return this.fullName();\n    },\n  },\n  computed: {\n    fullName() {\n      return this.state.firstName + this.state.lastName;\n    },\n    _privateFullname() {\n      return this.state.firstName + this.state.lastName;\n    },\n    testDependentOtherComputed() {\n      const status =\n        this.fullName() === 'ticktock'\n          ? this.state.statusList[0]\n          : this.state.statusList[1];\n      return `${this.fullName().trim()} [${status}]`;\n    },\n    isOnline() {\n      return this.fullName() === 'helloworld';\n    },\n    testArrayLength() {\n      return this.state.statusList.length;\n    },\n    testObjectKeys() {\n      return Object.keys(this.state.translate);\n    },\n    testFind() {\n      return this.state.statusList.find((item) => item.startsWith('off'));\n    },\n    testVisitArray() {\n      return this.state.statusList;\n    },\n    testJSON() {\n      return JSON.stringify(this.state);\n    },\n    testExtendObject() {\n      this.state.statusList.push('k');\n    },\n    testModifyValue() {\n      this.state.statusList[0] = 'BALA';\n    },\n    withParameter(age: number) {\n      return this.state.firstName + '-age-' + age;\n    },\n    withDefaultParameter(age: number = 20) {\n      return this.state.firstName + '-age-' + age;\n    },\n    withMultipleParameters(age: number = 20, address: string) {\n      return this.state.firstName + '-age-' + age + '-addr-' + address;\n    },\n    withMultipleAndDefaultParameters(age: number = 20, address?: string) {\n      return this.state.firstName + '-age-' + age + '-addr-' + address;\n    },\n  },\n});\n"
  },
  {
    "path": "test/models/persist.model.ts",
    "content": "import { cloneModel, defineModel } from '../../src';\n\nconst initialState: {\n  counter: number;\n} = {\n  counter: 0,\n};\n\nexport const persistModel = defineModel('persist', {\n  initialState,\n  reducers: {\n    plus(state, step: number) {\n      state.counter += step;\n    },\n    minus(state, step: number) {\n      state.counter -= step;\n    },\n  },\n  persist: {},\n});\n\nexport const hasVersionPersistModel = cloneModel('persist1', persistModel, {\n  initialState: {\n    counter: 56,\n  },\n  persist: {\n    version: 10,\n  },\n});\n\nexport const hasFilterPersistModel = cloneModel('persist2', persistModel, {\n  persist: {\n    dump(state) {\n      return state.counter;\n    },\n    load(counter) {\n      return { ...this.initialState, counter: counter + 1 };\n    },\n  },\n});\n"
  },
  {
    "path": "test/persist.gate.test.tsx",
    "content": "import { FC } from 'react';\nimport { act, render, screen } from '@testing-library/react';\nimport { FocaProvider, store } from '../src';\nimport { PersistGateProps } from '../src/persist/persist-gate';\nimport { basicModel } from './models/basic.model';\nimport { slowEngine } from './helpers/slow-engine';\n\nconst Loading: FC = () => <div data-testid=\"gateLoading\">Yes</div>;\n\nconst Root: FC<PersistGateProps & { useFunction?: boolean }> = ({\n  loading,\n  useFunction,\n}) => {\n  return (\n    <FocaProvider loading={loading}>\n      {useFunction ? (\n        (isReady: boolean) => (\n          <>\n            <div data-testid=\"isReady\">{String(isReady)}</div>\n            <div data-testid=\"inner\" />\n          </>\n        )\n      ) : (\n        <div data-testid=\"inner\" />\n      )}\n    </FocaProvider>\n  );\n};\n\nbeforeEach(() => {\n  store.init({\n    persist: [\n      {\n        version: 1,\n        key: 'test1',\n        models: [basicModel],\n        engine: slowEngine,\n      },\n    ],\n  });\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('PersistGate will inject to shadow dom', async () => {\n  render(<Root />);\n  expect(screen.queryByTestId('inner')).toBeNull();\n\n  await act(async () => {\n    await store.onInitialized();\n  });\n  expect(screen.queryByTestId('inner')).not.toBeNull();\n});\n\ntest('PersistGate allows function children', async () => {\n  render(<Root useFunction />);\n  expect(screen.queryByTestId('isReady')!.innerHTML).toBe('false');\n\n  await act(async () => {\n    await store.onInitialized();\n  });\n  expect(screen.queryByTestId('isReady')!.innerHTML).toBe('true');\n});\n\ntest('PersistGate allows loading children', async () => {\n  render(<Root loading={<Loading />} />);\n  expect(screen.queryByTestId('inner')).toBeNull();\n  expect(screen.queryByTestId('gateLoading')!.innerHTML).toBe('Yes');\n\n  await act(async () => {\n    await store.onInitialized();\n  });\n\n  expect(screen.queryByTestId('inner')).not.toBeNull();\n  expect(screen.queryByTestId('gateLoading')).toBeNull();\n});\n\ntest('PersistGate will warning for both function children and loading children', async () => {\n  const spy = vitest.spyOn(console, 'error').mockImplementation(() => {});\n\n  render(<Root useFunction loading={<Loading />} />);\n  expect(spy).toHaveBeenCalledTimes(1);\n\n  await act(() => store.onInitialized());\n  expect(spy).toHaveBeenCalledTimes(2);\n\n  spy.mockRestore();\n});\n"
  },
  {
    "path": "test/persist.test.ts",
    "content": "import sleep from 'sleep-promise';\nimport {\n  defineModel,\n  memoryStorage,\n  Model,\n  StorageEngine,\n  store,\n} from '../src';\nimport {\n  PersistItem,\n  PersistMergeMode,\n  PersistSchema,\n} from '../src/persist/persist-item';\nimport { stringifyState } from '../src/utils/serialize';\nimport { basicModel } from './models/basic.model';\nimport {\n  hasFilterPersistModel,\n  hasVersionPersistModel,\n  persistModel,\n} from './models/persist.model';\nimport { slowEngine } from './helpers/slow-engine';\n\nconst stringifyTwice = (model: Model) => {\n  return JSON.stringify(JSON.stringify(model.state));\n};\n\nconst createDefaultInstance = () => {\n  return new PersistItem({\n    version: 1,\n    key: 'test-' + Math.random(),\n    engine: memoryStorage,\n    models: [persistModel],\n  });\n};\n\nconst storageDump = (opts: {\n  key: string;\n  model: Model;\n  state?: object | string;\n  persistVersion?: number;\n  modelVersion?: number;\n  engine?: StorageEngine;\n}) => {\n  const {\n    key,\n    model,\n    state = model.state,\n    persistVersion = 1,\n    modelVersion = 0,\n    engine = memoryStorage,\n  } = opts;\n  return engine.setItem(\n    key,\n    JSON.stringify(<PersistSchema>{\n      v: persistVersion,\n      d: {\n        [model.name]: {\n          v: modelVersion,\n          d: typeof state === 'string' ? state : stringifyState(state),\n        },\n      },\n    }),\n  );\n};\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(async () => {\n  store.unmount();\n  memoryStorage.clear();\n});\n\ntest('dump state', async () => {\n  const persist = createDefaultInstance();\n\n  expect(memoryStorage.getItem(persist.key)).toBeNull();\n\n  await persist.init();\n\n  expect(memoryStorage.getItem(persist.key)).toBe(JSON.stringify(persist));\n  expect(memoryStorage.getItem(persist.key)).toContain(\n    stringifyTwice(persistModel),\n  );\n\n  persistModel.plus(15);\n  expect(persistModel.state.counter).toBe(15);\n\n  persist.update({\n    [persistModel.name]: persistModel.state,\n  });\n\n  await sleep(1);\n\n  const value = await memoryStorage.getItem(persist.key);\n  expect(value).toBe(JSON.stringify(persist));\n  expect(value).toContain(stringifyTwice(persistModel));\n});\n\ntest('load state', async () => {\n  const persist = createDefaultInstance();\n\n  await storageDump({\n    key: persist.key,\n    model: persistModel,\n    state: { counter: 15, extra: undefined },\n  });\n\n  await persist.init();\n\n  expect(persist.collect()).toMatchObject({\n    [persistModel.name]: {\n      counter: 15,\n      extra: undefined,\n    },\n  });\n  expect(persist.collect()[persistModel.name]).toHaveProperty(\n    'extra',\n    undefined,\n  );\n});\n\ntest('load failed due to persist version changed', async () => {\n  const persist = createDefaultInstance();\n\n  await storageDump({\n    key: persist.key,\n    model: persistModel,\n    persistVersion: 20,\n    state: { counter: 100 },\n  });\n  await persist.init();\n\n  expect(persist.collect()).toStrictEqual({\n    [persistModel.name]: { counter: 0 },\n  });\n});\n\ntest('load failed due to model version changed', async () => {\n  const persist = createDefaultInstance();\n\n  await storageDump({\n    key: persist.key,\n    model: persistModel,\n    modelVersion: 17,\n    state: { counter: 100 },\n  });\n\n  await persist.init();\n\n  expect(persist.collect()).toStrictEqual({\n    [persistModel.name]: { counter: 0 },\n  });\n});\n\ntest('load failed due to invalid JSON literal', async () => {\n  let persist = createDefaultInstance();\n\n  await storageDump({\n    key: persist.key,\n    model: persistModel,\n    state: stringifyState(persistModel.state) + '$$$$',\n  });\n\n  await expect(persist.init()).rejects.toThrowError();\n  expect(persist.collect()).toStrictEqual({});\n\n  await Promise.resolve();\n  await expect(persist.init()).resolves.toBeUndefined();\n  expect(persist.collect()).toStrictEqual({\n    [persistModel.name]: { counter: 0 },\n  });\n\n  persist = createDefaultInstance();\n  await memoryStorage.setItem(\n    persist.key,\n    JSON.stringify(<PersistSchema>{\n      v: 1,\n      d: {\n        [persistModel.name]: {\n          t: Date.now(),\n          v: 0,\n          d: stringifyState(persistModel.state) + '$$$$',\n        },\n      },\n    }) + '$$$$',\n  );\n  await expect(persist.init()).rejects.toThrowError();\n  expect(persist.collect()).toStrictEqual({});\n\n  await Promise.resolve();\n  await expect(persist.init()).resolves.toBeUndefined();\n  expect(persist.collect()).toStrictEqual({\n    [persistModel.name]: { counter: 0 },\n  });\n});\n\ntest('get rid of unregistered model', async () => {\n  const persist = new PersistItem({\n    version: 1,\n    key: 'test1',\n    engine: memoryStorage,\n    models: [basicModel],\n  });\n\n  await memoryStorage.setItem(\n    persist.key,\n    JSON.stringify(<PersistSchema>{\n      v: 1,\n      d: {\n        [persistModel.name]: {\n          t: Date.now(),\n          v: 0,\n          d: stringifyState(persistModel.state),\n        },\n        [basicModel.name]: {\n          t: Date.now(),\n          v: 0,\n          d: stringifyState(basicModel.state),\n        },\n      },\n    }),\n  );\n\n  await persist.init();\n\n  expect(persist.collect()).toMatchObject({\n    [basicModel.name]: basicModel.state,\n  });\n  expect(persist.collect()).not.toMatchObject({\n    [persistModel.name]: persistModel.state,\n  });\n\n  const storageValue = await memoryStorage.getItem(persist.key);\n  expect(storageValue).toContain(stringifyTwice(basicModel));\n  expect(storageValue).not.toContain(stringifyTwice(persistModel));\n});\n\ntest('model can specific persist version', async () => {\n  const persist = new PersistItem({\n    version: 1,\n    key: 'test1',\n    engine: memoryStorage,\n    models: [persistModel, hasVersionPersistModel],\n  });\n\n  await storageDump({\n    key: persist.key,\n    model: hasVersionPersistModel,\n    modelVersion: 10,\n  });\n\n  await persist.init();\n\n  expect(persist.collect()).toMatchObject({\n    [hasVersionPersistModel.name]: hasVersionPersistModel.state,\n  });\n});\n\ntest('model can specific persist filter function', async () => {\n  const persist = new PersistItem({\n    version: 1,\n    key: 'test1',\n    engine: memoryStorage,\n    models: [persistModel, hasFilterPersistModel],\n  });\n\n  await memoryStorage.setItem(\n    persist.key,\n    JSON.stringify(<PersistSchema>{\n      v: 1,\n      d: {\n        [hasFilterPersistModel.name]: {\n          t: Date.now(),\n          v: 0,\n          d: stringifyState(hasFilterPersistModel.state.counter),\n        },\n      },\n    }),\n  );\n\n  await persist.init();\n\n  expect(persist.collect()).toMatchObject({\n    [hasFilterPersistModel.name]: { counter: 1 },\n  });\n\n  hasFilterPersistModel.plus(100);\n  hasFilterPersistModel.plus(5);\n\n  persist.update({\n    [hasFilterPersistModel.name]: hasFilterPersistModel.state,\n  });\n\n  expect(memoryStorage.getItem(persist.key)).toContain('\"d\":\"105\"');\n});\n\ntest('no dump happen before load finished', async () => {\n  await storageDump({\n    key: '@test1',\n    model: persistModel,\n    engine: slowEngine,\n  });\n\n  store.unmount();\n  store.init({\n    persist: [\n      {\n        version: 1,\n        keyPrefix: '@',\n        key: 'test1',\n        engine: slowEngine,\n        models: [persistModel],\n      },\n    ],\n  });\n\n  const persistManager = store['persister']!;\n\n  const promise = persistManager.init(store, true);\n  persistModel.plus(6);\n  expect(persistModel.state.counter).toBe(6);\n  await promise;\n\n  expect(persistModel.state.counter).toBe(0);\n  await expect(slowEngine.getItem('@test1')).resolves.toContain(\n    stringifyTwice(persistModel),\n  );\n\n  await sleep(100);\n\n  expect(persistModel.state.counter).toBe(0);\n  await expect(slowEngine.getItem('@test1')).resolves.toContain(\n    stringifyTwice(persistModel),\n  );\n  expect(persistManager.collect()).toMatchObject({\n    [persistModel.name]: persistModel.state,\n  });\n\n  store.unmount();\n});\n\ntest('default merge mode is `merge`', async () => {\n  const persist = createDefaultInstance();\n\n  await storageDump({\n    key: persist.key,\n    model: persistModel,\n    state: { hello: 'world' },\n  });\n\n  await persist.init();\n\n  expect(persist.collect()).toStrictEqual({\n    [persistModel.name]: {\n      hello: 'world',\n      counter: 0,\n    },\n  });\n});\n\ntest('set merge mode to `replace`', async () => {\n  const persist = new PersistItem({\n    version: 1,\n    key: 'test-' + Math.random(),\n    engine: memoryStorage,\n    models: [persistModel],\n    merge: 'replace',\n  });\n\n  await storageDump({\n    key: persist.key,\n    model: persistModel,\n    state: { hello: 'world' },\n  });\n\n  await persist.init();\n\n  expect(persist.collect()).toStrictEqual({\n    [persistModel.name]: {\n      hello: 'world',\n    },\n  });\n});\n\ntest('set merge mode to `deep-merge`', async () => {\n  const model = defineModel('deep-merge-persist-model', {\n    initialState: { hi: 'here', data: { name: 'test', age: 20 } },\n  });\n\n  const persist = new PersistItem({\n    version: 1,\n    key: 'test-' + Math.random(),\n    engine: memoryStorage,\n    models: [model],\n    merge: 'deep-merge',\n  });\n\n  await storageDump({\n    key: persist.key,\n    model: model,\n    state: { hello: 'world', data: { name: 'g' } },\n  });\n  await persist.init();\n  expect(persist.collect()).toStrictEqual({\n    [model.name]: {\n      hello: 'world',\n      hi: 'here',\n      data: { name: 'g', age: 20 },\n    },\n  });\n\n  await storageDump({\n    key: persist.key,\n    model: model,\n    state: { hello: 'world', data: 'x' },\n  });\n  await persist.init();\n  expect(persist.collect()).toStrictEqual({\n    [model.name]: {\n      hello: 'world',\n      hi: 'here',\n      data: 'x',\n    },\n  });\n});\n\ntest('through dump and load function without storage data', async () => {\n  const spy1 = vitest.fn();\n  const spy2 = vitest.fn();\n  const model = defineModel('persist-model-' + Math.random(), {\n    initialState: {},\n    persist: {\n      dump: spy1,\n      load: spy2,\n    },\n  });\n\n  store.unmount();\n  store.init({\n    persist: [\n      {\n        engine: memoryStorage,\n        key: 'test-initial-state-dump-and-load',\n        version: 1,\n        models: [model],\n      },\n    ],\n  });\n  expect(spy1).toBeCalledTimes(0);\n  expect(spy2).toBeCalledTimes(0);\n  await store.onInitialized();\n  expect(spy1).toBeCalledTimes(1);\n  expect(spy2).toBeCalledTimes(1);\n  spy1.mockRestore();\n  spy2.mockRestore();\n});\n\ntest('context of load function contains initialState', async () => {\n  const model = defineModel('persist-model-' + Math.random(), {\n    initialState: { hello: 'world', count: 10 },\n    persist: {\n      dump(state) {\n        return state.hello;\n      },\n      // @ts-expect-error\n      load(hello) {\n        return { hello: hello + 'x', test: this.initialState };\n      },\n    },\n  });\n\n  store.unmount();\n  store.init({\n    persist: [\n      {\n        engine: memoryStorage,\n        key: 'test-initialState-context',\n        version: 1,\n        models: [model],\n      },\n    ],\n  });\n  await store.onInitialized();\n  expect(model.state).toStrictEqual({\n    hello: 'worldx',\n    count: 10,\n    test: {\n      hello: 'world',\n      count: 10,\n    },\n  });\n});\n\ndescribe('merge method', () => {\n  const persistItem = new PersistItem({\n    key: '',\n    version: '',\n    engine: memoryStorage,\n    models: [],\n  });\n\n  describe.each([\n    'replace',\n    'merge',\n    'deep-merge',\n  ] satisfies PersistMergeMode[])('array type', (mode) => {\n    test('object + array', () => {\n      expect(\n        persistItem.merge({ hello: 'world' }, [{ foo: 'bar' }, {}], mode),\n      ).toStrictEqual([{ foo: 'bar' }, {}]);\n    });\n    test('array + array', () => {\n      expect(\n        persistItem.merge([{ tick: 'tock' }], [{ foo: 'bar' }, {}], mode),\n      ).toStrictEqual([{ tick: 'tock' }]);\n    });\n    test('array + object', () => {\n      expect(\n        persistItem.merge([{ tick: 'tock' }], { hello: 'world' }, mode),\n      ).toStrictEqual({\n        hello: 'world',\n      });\n    });\n  });\n\n  test('replace', () => {\n    expect(\n      persistItem.merge({ hi: 'there' }, { hello: 'world' }, 'replace'),\n    ).toStrictEqual({ hi: 'there' });\n  });\n\n  test('merge', () => {\n    expect(\n      persistItem.merge(\n        { hello: 'world', hi: 'there', a: { c: '2' } },\n        { hi: 'here', a: { b: '1' } },\n        'merge',\n      ),\n    ).toStrictEqual({ hi: 'there', hello: 'world', a: { c: '2' } });\n  });\n\n  test('deep-merge', () => {\n    expect(\n      persistItem.merge(\n        { hello: 'world', hi: 'there', a: { c: '2' } },\n        { hi: 'here', a: { b: '1' } },\n        'deep-merge',\n      ),\n    ).toStrictEqual({ hello: 'world', hi: 'there', a: { b: '1', c: '2' } });\n\n    expect(\n      persistItem.merge(\n        { hello: 'world', hi: 'there', a: { c: '2' } },\n        { hi: 'here', a: 'x' },\n        'deep-merge',\n      ),\n    ).toStrictEqual({ hi: 'there', hello: 'world', a: { c: '2' } });\n\n    expect(\n      persistItem.merge(\n        { hello: 'world', hi: 'there', a: { c: '2' } },\n        { hi: 'here', a: ['x'] },\n        'deep-merge',\n      ),\n    ).toStrictEqual({ hi: 'there', hello: 'world', a: { c: '2' } });\n  });\n});\n"
  },
  {
    "path": "test/provider.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { FocaProvider, store } from '../src';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('render normal tag', () => {\n  render(\n    <FocaProvider>\n      <div data-testid=\"root\">Hello World</div>\n    </FocaProvider>,\n  );\n\n  expect(screen.queryByTestId('root')!.innerHTML).toBe('Hello World');\n});\n\ntest('render function tag', () => {\n  render(\n    <FocaProvider>\n      {() => <div data-testid=\"root\">Hello World</div>}\n    </FocaProvider>,\n  );\n\n  expect(screen.queryByTestId('root')!.innerHTML).toBe('Hello World');\n});\n"
  },
  {
    "path": "test/serialize.test.ts",
    "content": "import { parseState, stringifyState } from '../src/utils/serialize';\n\nit('can clone basic data', () => {\n  expect(\n    parseState(\n      stringifyState({\n        x: 1,\n        y: 'y',\n        z: true,\n      }),\n    ),\n  ).toMatchObject({\n    x: 1,\n    y: 'y',\n    z: true,\n  });\n});\n\nit('can clone complex data', () => {\n  expect(\n    parseState(\n      stringifyState({\n        x: 1,\n        y: {\n          z: [1, 2, '3'],\n        },\n      }),\n    ),\n  ).toMatchObject({\n    x: 1,\n    y: {\n      z: [1, 2, '3'],\n    },\n  });\n});\n\nit('can clone null and undefined', () => {\n  const data = {\n    x: [\n      undefined,\n      1,\n      {\n        test: undefined,\n        test1: 'hello',\n      },\n      null,\n      undefined,\n    ],\n    y: null,\n    z: undefined,\n  };\n\n  expect(stringifyState(data)).toMatchSnapshot();\n  expect(parseState(stringifyState(data))).toMatchSnapshot();\n  expect(parseState(stringifyState(data))).toMatchObject(data);\n});\n"
  },
  {
    "path": "test/store.test.ts",
    "content": "import { compose, StoreEnhancer } from 'redux';\nimport sleep from 'sleep-promise';\nimport { from, map } from 'rxjs';\nimport { composeWithDevTools } from '@redux-devtools/extension';\nimport { defineModel, memoryStorage, store } from '../src';\nimport { PersistSchema } from '../src/persist/persist-item';\nimport { PersistManager } from '../src/persist/persist-manager';\nimport { basicModel, basicSkipRefreshModel } from './models/basic.model';\nimport { complexModel } from './models/complex.model';\nimport {\n  hasFilterPersistModel,\n  hasVersionPersistModel,\n  persistModel,\n} from './models/persist.model';\n\nafterEach(() => {\n  store.unmount();\n  memoryStorage.clear();\n  localStorage.clear();\n  sessionStorage.clear();\n});\n\nconst initializeStoreWithMultiplePersist = () => {\n  return store.init({\n    persist: [\n      {\n        key: 'test1',\n        keyPrefix: 'Test:',\n        version: 1,\n        engine: memoryStorage,\n        models: [basicModel, complexModel],\n      },\n      {\n        key: 'test1',\n        keyPrefix: 'Test:',\n        version: 1,\n        engine: localStorage,\n        models: [persistModel],\n      },\n      {\n        key: 'test2',\n        keyPrefix: 'Test:',\n        version: 1,\n        engine: memoryStorage,\n        models: [hasVersionPersistModel, hasFilterPersistModel],\n      },\n    ],\n  });\n};\n\ntest('Store will throw error before initialize', () => {\n  expect(() => store.getState()).toThrowError();\n});\n\ntest('Method replaceReducer is deprecated', () => {\n  expect(() =>\n    store.replaceReducer(() => {\n      return {} as any;\n    }),\n  ).toThrowError();\n});\n\ntest('Store can initialize many times except production env', async () => {\n  store.init();\n  store.init();\n  expect(() => store.init()).not.toThrowError();\n\n  const oldEnv = process.env.NODE_ENV;\n  process.env.NODE_ENV = 'production';\n  expect(() => store.init()).toThrowError();\n  process.env.NODE_ENV = oldEnv;\n\n  await store.onInitialized();\n});\n\ndescribe('persist', () => {\n  test('Delay to dump data when state changed', async () => {\n    initializeStoreWithMultiplePersist();\n\n    await store.onInitialized();\n\n    // @ts-expect-error\n    const getTimer = () => store['persister']!.timer;\n    let prevTimer: ReturnType<typeof getTimer>;\n\n    expect(getTimer()).toBeUndefined();\n    basicModel.plus(1);\n    expect(getTimer()).not.toBeUndefined();\n    prevTimer = getTimer();\n    basicModel.plus(20);\n    expect(getTimer()).toBe(prevTimer);\n    basicModel.plus(1);\n    expect(getTimer()).toBe(prevTimer);\n\n    expect(store['persister']?.collect()[basicModel.name]).not.toBe(\n      basicModel.state,\n    );\n    await sleep(50);\n    expect(store['persister']?.collect()[basicModel.name]).toBe(\n      basicModel.state,\n    );\n\n    expect(getTimer()).toBeUndefined();\n    basicModel.plus(1);\n    expect(getTimer()).not.toBeUndefined();\n    expect(getTimer()).not.toBe(prevTimer);\n\n    expect(store['persister']?.collect()[basicModel.name]).not.toBe(\n      basicModel.state,\n    );\n    await sleep(50);\n    expect(store['persister']?.collect()[basicModel.name]).toBe(\n      basicModel.state,\n    );\n  });\n\n  test('Store can define persist with multiple engines', async () => {\n    initializeStoreWithMultiplePersist();\n\n    await store.onInitialized();\n\n    expect(JSON.stringify(store['persister']?.collect())).toMatchInlineSnapshot(\n      '\"{\"basic\":{\"count\":0,\"hello\":\"world\"},\"complex\":{\"users\":{},\"ids\":[]},\"persist\":{\"counter\":0},\"persist1\":{\"counter\":56},\"persist2\":{\"counter\":1}}\"',\n    );\n\n    basicModel.plus(1);\n    persistModel.plus(103);\n\n    await sleep(50);\n    expect(store['persister']?.collect()).toMatchInlineSnapshot(`\n    {\n      \"basic\": {\n        \"count\": 1,\n        \"hello\": \"world\",\n      },\n      \"complex\": {\n        \"ids\": [],\n        \"users\": {},\n      },\n      \"persist\": {\n        \"counter\": 103,\n      },\n      \"persist1\": {\n        \"counter\": 56,\n      },\n      \"persist2\": {\n        \"counter\": 1,\n      },\n    }\n  `);\n  });\n\n  test('Store can load persist state', async () => {\n    await memoryStorage.setItem(\n      'Test:test1',\n      JSON.stringify(<PersistSchema>{\n        v: 1,\n        d: {\n          [basicModel.name]: {\n            v: 0,\n            t: Date.now(),\n            d: JSON.stringify({\n              count: 123,\n              hello: 'earth',\n            }),\n          },\n        },\n      }),\n    );\n\n    initializeStoreWithMultiplePersist();\n\n    await store.onInitialized();\n\n    expect(basicModel.state).toMatchObject({\n      count: 123,\n      hello: 'earth',\n    });\n  });\n});\n\ntest('refresh the total state', () => {\n  store.init();\n\n  basicModel.plus(1);\n  basicSkipRefreshModel.plus(1);\n  expect(basicModel.state.count).toEqual(1);\n  expect(basicSkipRefreshModel.state.count).toEqual(1);\n\n  store.refresh();\n  expect(basicModel.state.count).toEqual(0);\n  expect(basicSkipRefreshModel.state.count).toEqual(1);\n\n  basicModel.plus(1);\n  basicSkipRefreshModel.plus(1);\n  expect(basicModel.state.count).toEqual(1);\n  expect(basicSkipRefreshModel.state.count).toEqual(2);\n\n  store.refresh(true);\n  expect(basicModel.state.count).toEqual(0);\n  expect(basicSkipRefreshModel.state.count).toEqual(0);\n});\n\ntest('duplicate init() will keep state', () => {\n  store.init();\n\n  expect(basicModel.state.count).toEqual(0);\n  basicModel.plus(1);\n  expect(basicModel.state.count).toEqual(1);\n\n  store.init();\n  expect(basicModel.state.count).toEqual(1);\n});\n\ntest('duplicate init() will replace persister', async () => {\n  store.init();\n  await store.onInitialized();\n  expect(store['persister']).toBeNull();\n\n  store.init({\n    persist: [\n      {\n        key: 'test',\n        version: 2,\n        engine: memoryStorage,\n        models: [],\n      },\n    ],\n  });\n  expect(store['persister']).toBeInstanceOf(PersistManager);\n  await store.onInitialized();\n  basicModel.plus(1);\n  await sleep(100);\n  expect(store['persister']!.collect()).toStrictEqual({});\n\n  store.init({\n    persist: [\n      {\n        key: 'test',\n        version: 2,\n        engine: memoryStorage,\n        models: [basicModel],\n      },\n    ],\n  });\n  await store.onInitialized();\n  basicModel.plus(1);\n  await sleep(100);\n  expect(store['persister']!.collect()).toStrictEqual({\n    [basicModel.name]: {\n      count: 2,\n      hello: 'world',\n    },\n  });\n\n  store.init({\n    persist: [\n      {\n        key: 'test',\n        version: 2,\n        engine: memoryStorage,\n        models: [],\n      },\n    ],\n  });\n  await store.onInitialized();\n  expect(store['persister']!.collect()).toStrictEqual({});\n});\n\ntest('Get custom compose', () => {\n  const get: (typeof store)['getCompose'] = store['getCompose'].bind(store);\n\n  expect(get(void 0)).toBe(compose);\n  expect(get(compose)).toBe(compose);\n\n  const customCompose = (): StoreEnhancer => '' as any;\n  expect(get(customCompose)).toBe(customCompose);\n\n  const devtoolsComposer = composeWithDevTools({\n    name: 'x',\n  });\n  expect(get(devtoolsComposer)).toBe(devtoolsComposer);\n  expect(get(composeWithDevTools)).toBe(composeWithDevTools);\n\n  expect(get('redux-devtools')).toBe(compose);\n\n  // @ts-expect-error\n  globalThis['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] = customCompose;\n  expect(get('redux-devtools')).toBe(customCompose);\n\n  const prevEnv = process.env.NODE_ENV;\n  process.env.NODE_ENV = 'production';\n  expect(get('redux-devtools')).toBe(compose);\n  process.env.NODE_ENV = prevEnv;\n});\n\ntest('rxjs can observe store', () => {\n  store.init();\n\n  const observable = from(store);\n  const results: any[] = [];\n\n  const model = defineModel('rxjs', {\n    initialState: {\n      foo: 0,\n      bar: 0,\n    },\n    reducers: {\n      foo(state) {\n        state.foo += 1;\n      },\n      bar(state) {\n        state.bar += 1;\n      },\n    },\n  });\n\n  const sub = observable\n    .pipe(\n      map((state) => {\n        return { fromRx: true, ...state[model.name] };\n      }),\n    )\n    .subscribe((state) => {\n      results.push(state);\n    });\n\n  model.foo();\n  model.foo();\n  sub.unsubscribe();\n  model.bar();\n\n  expect(results).toEqual(\n    expect.arrayContaining([\n      { foo: 0, bar: 0, fromRx: true },\n      { foo: 1, bar: 0, fromRx: true },\n      { foo: 2, bar: 0, fromRx: true },\n    ]),\n  );\n});\n\ntest('async callback for onInitialized', () => {\n  let msg = 'a';\n  store.onInitialized(() => {\n    msg += 'b';\n  });\n  msg += 'c';\n  store.init();\n  expect(msg).toBe('acb');\n});\n\ntest('sync callback for onInitialized', () => {\n  store.init();\n\n  let msg = 'a';\n  store.onInitialized(() => {\n    msg += 'b';\n  });\n  msg += 'c';\n  expect(msg).toBe('abc');\n});\n"
  },
  {
    "path": "test/typescript/computed.check.ts",
    "content": "import { expectType } from 'ts-expect';\nimport { cloneModel, defineModel, useComputed } from '../../src';\nimport { ComputedFlag } from '../../src/model/types';\n\nconst model = defineModel('test', {\n  initialState: {\n    firstName: 't',\n    lastName: 'r',\n  },\n  computed: {\n    fullName() {\n      return this.state.firstName + '.' + this.state.lastName;\n    },\n    nickName() {\n      return [this.fullName() + '-nick'];\n    },\n    _dirname() {\n      return 'whatever';\n    },\n    withAge(age: number = 20) {\n      return this.state.firstName + '-age-' + age;\n    },\n    withRequiredParameter(address: string) {\n      return this.state.firstName + this.withAge(15) + '-address-' + address;\n    },\n    withMultipleParameter(address: string, age: number, extra?: boolean) {\n      return address + age + extra;\n    },\n  },\n});\n\nexpectType<(() => string[]) & ComputedFlag>(model.nickName);\n\n// @ts-expect-error\nmodel.fullName = 'modify';\n// @ts-expect-error\nmodel.nickName = 'modify';\n// @ts-expect-error\nmodel._dirname;\n// @ts-expect-error\nmodel.firstName;\n\n// @ts-expect-error\nuseComputed(model);\nexpectType<string>(useComputed(model.fullName));\nexpectType<string[]>(useComputed(model.nickName));\n// @ts-expect-error\nuseComputed(model.fullName, 20);\nuseComputed(model.withAge);\nuseComputed(model.withAge, 20);\n// @ts-expect-error\nuseComputed(model.withAge, '20');\n// @ts-expect-error\nuseComputed(() => {});\n// @ts-expect-error\nuseComputed(model.withRequiredParameter);\nexpectType<string>(useComputed(model.withRequiredParameter, 'addr'));\nuseComputed(model.withRequiredParameter, 'addr').endsWith('ss');\n// @ts-expect-error\nuseComputed(model.withMultipleParameter, '');\n// @ts-expect-error\nuseComputed(model.withMultipleParameter, 0);\nuseComputed(model.withMultipleParameter, '', 0);\nuseComputed(model.withMultipleParameter, '', 0, false);\n\n{\n  const model1 = cloneModel('clone-model', model);\n  model1.fullName();\n  model1.withMultipleParameter('', 20);\n}\n"
  },
  {
    "path": "test/typescript/define-model.check.ts",
    "content": "import { expectType } from 'ts-expect';\nimport { UnknownAction, defineModel } from '../../src';\n\n// @ts-expect-error\ndefineModel('no-initial-state', {});\n\ndefineModel('null-state', {\n  // @ts-expect-error\n  initialState: null,\n});\n\ndefineModel('string-state', {\n  // @ts-expect-error\n  initialState: '',\n});\n\ndefineModel('array-state-reducers', {\n  initialState: [] as { test: number }[],\n  reducers: {\n    returnNormal(_) {\n      return [];\n    },\n    returnInitialize(_) {\n      return this.initialState;\n    },\n  },\n  methods: {\n    returnNormal() {\n      this.setState([]);\n      this.setState([{ test: 3 }]);\n      // @ts-expect-error\n      this.setState();\n      // @ts-expect-error\n      this.setState({});\n      // @ts-expect-error\n      this.setState(2);\n      // @ts-expect-error\n      this.setState();\n      // @ts-expect-error\n      this.setState([undefined]);\n      // @ts-expect-error\n      this.setState([{ test: 3 }, {}]);\n      // @ts-expect-error\n      this.setState(undefined);\n\n      this.setState((_) => {\n        return [];\n      });\n      // @ts-expect-error\n      this.setState((_) => {\n        return [] as object[];\n      });\n      this.setState((state) => {\n        state.push({ test: 3 });\n        // @ts-expect-error\n        state.push(4);\n      });\n    },\n    returnInitialize() {\n      this.setState(() => {\n        return this.initialState;\n      });\n      this.setState(this.initialState);\n    },\n  },\n});\n\ndefineModel('object-state-reducers', {\n  initialState: {} as {\n    test: { test1: number };\n    test2: string;\n    test3?: string;\n  },\n  reducers: {\n    returnNormal(_state) {\n      return { test: { test1: 3 }, test2: 'bar' };\n    },\n    returnInitialize() {\n      return this.initialState;\n    },\n  },\n  methods: {\n    returnNormal() {\n      this.setState((_) => {\n        return {};\n      });\n      this.setState((_) => {\n        return { test: { test1: 2 } };\n      });\n      this.setState((_) => {\n        return { test: { test1: 2 }, test2: 'bar' };\n      });\n      // FIXME:\n      this.setState((_) => {\n        return { test: { test1: 2 }, test2: 'bar', test3: '', other: '' };\n      });\n      // @ts-expect-error\n      this.setState((_) => {\n        return { test: { test1: 2 }, foo1: 'baz' };\n      });\n      // @ts-expect-error\n      this.setState((_) => {\n        return { xxx: 2 };\n      });\n      // @ts-expect-error\n      this.setState((_) => {\n        return { test: {} };\n      });\n      // @ts-expect-error\n      this.setState((_) => {\n        return { test: { test1: 2 }, t: 3 };\n      });\n      // FIXME:\n      this.setState((_) => {\n        return { test: { test1: 2, t: 3 } };\n      });\n      // @ts-expect-error\n      this.setState((_) => {\n        return { test: { test2: 2 } };\n      });\n      this.setState((state) => {\n        state.test.test1 = 4;\n      });\n    },\n    returnPartial() {\n      this.setState({});\n      this.setState({ test: { test1: 3 } });\n      // @ts-expect-error\n      this.setState({ test: { test1: 3, more: 4 } });\n      // @ts-expect-error\n      this.setState({ test: { test1: 3, more: undefined } });\n\n      this.setState({ test3: undefined });\n      this.setState({ test2: 'x', test3: undefined });\n      // @ts-expect-error\n      this.setState({ test: { test1: undefined } });\n\n      // @ts-expect-error\n      this.setState({ test2: undefined });\n      // @ts-expect-error\n      this.setState({ test: 'x' });\n      // @ts-expect-error\n      this.setState({ test: { test1: 'x' } });\n      // @ts-expect-error\n      this.setState({ test: { test1: 3 }, ok: 'test', more: 'test1' });\n      // @ts-expect-error\n      this.setState();\n      // @ts-expect-error\n      this.setState({ test: {} });\n      // @ts-expect-error\n      this.setState([]);\n      // @ts-expect-error\n      this.setState(2);\n      // @ts-expect-error\n      this.setState({ xxx: 2 });\n      this.setState({ test2: 'x' });\n      // @ts-expect-error\n      this.setState({ test2: 'x', more: 'y' });\n    },\n    returnInitialize() {\n      this.setState(() => {\n        return this.initialState;\n      });\n      this.setState(this.initialState);\n    },\n  },\n});\n\ndefineModel('wrong-reducer-state-1', {\n  initialState: {} as { test: { test1: number } },\n  // @ts-expect-error\n  reducers: {\n    returnUnexpected(_) {\n      return { test: {} };\n    },\n    right() {\n      return { test: { test1: 3 } };\n    },\n  },\n});\n\ndefineModel('wrong-reducer-state-2', {\n  initialState: {} as { test: { test1: number } },\n  // @ts-expect-error\n  reducers: {\n    returnUnexpected(_) {\n      return {};\n    },\n\n    right() {\n      return { test: { test1: 3 } };\n    },\n  },\n});\n\ndefineModel('wrong-reducer-state-3', {\n  initialState: {} as { test: { test1: number } },\n  // @ts-expect-error\n  reducers: {\n    returnUnexpected(_) {\n      return [];\n    },\n    right() {\n      return { test: { test1: 3 } };\n    },\n  },\n});\n\ndefineModel('private-and-context', {\n  initialState: {},\n  reducers: {\n    _action1() {},\n    _action2() {},\n    action3() {\n      // @ts-expect-error\n      this.method3;\n      // @ts-expect-error\n      this.xxx;\n      // @ts-expect-error\n      this._fullname;\n    },\n  },\n  methods: {\n    _method1() {\n      this._action1();\n    },\n    async _method2() {},\n    method3() {\n      this._method1();\n      this.xxx().endsWith('/');\n      this._fullname().endsWith('/');\n    },\n  },\n  computed: {\n    xxx() {\n      // @ts-expect-error\n      this._method1;\n      // @ts-expect-error\n      this._action1;\n      // @ts-expect-error\n      this.method3;\n\n      return '';\n    },\n    yyy() {\n      this.xxx();\n      return this._fullname();\n    },\n    _fullname() {\n      return '';\n    },\n  },\n  events: {\n    onInit() {\n      this._action1();\n      this._method1();\n      this.action3();\n      this.method3();\n      this.state;\n      // @ts-expect-error\n      this.initialState;\n      // @ts-expect-error\n      this.onInit;\n\n      expectType<string>(this._fullname());\n      expectType<() => Promise<void>>(this._method2);\n      expectType<() => UnknownAction>(this._action1);\n      expectType<() => UnknownAction>(this._action2);\n    },\n  },\n});\n"
  },
  {
    "path": "test/typescript/get-loading.check.ts",
    "content": "import { expectType } from 'ts-expect';\nimport { getLoading } from '../../src';\nimport { basicModel } from '../models/basic.model';\n\nexpectType<boolean>(getLoading(basicModel.foo));\nexpectType<boolean>(getLoading(basicModel.foo.room).find('xx'));\nexpectType<boolean>(getLoading(basicModel.foo.room, 'xx'));\n// @ts-expect-error\ngetLoading(basicModel.foo.room, basicModel.foo);\n// @ts-expect-error\ngetLoading(basicModel.foo.room, true);\n// @ts-expect-error\ngetLoading(basicModel.foo.room, false);\n// @ts-expect-error\ngetLoading(basicModel.normalMethod.room);\n"
  },
  {
    "path": "test/typescript/persist.check.ts",
    "content": "import { TypeEqual, expectType } from 'ts-expect';\nimport { cloneModel, defineModel } from '../../src';\nimport { GetInitialState } from '../../src/model/types';\n\nconst state: { hello: string } = { hello: 'world' };\n\ndefineModel('model', {\n  initialState: state,\n  // @ts-expect-error\n  persist: {\n    dump() {\n      return '';\n    },\n  },\n});\n\ndefineModel('model', {\n  initialState: state,\n  // @ts-expect-error\n  persist: {\n    load() {\n      return {} as typeof state;\n    },\n  },\n});\n\ndefineModel('model', {\n  initialState: state,\n  persist: {\n    dump() {\n      return '';\n    },\n    load() {\n      return {} as typeof state;\n    },\n  },\n});\n\ndefineModel('model', {\n  initialState: state,\n  persist: {},\n});\n\ndefineModel('model', {\n  initialState: state,\n  persist: {\n    version: 1,\n  },\n});\n\ndefineModel('model', {\n  initialState: state,\n  persist: {\n    version: '1.0.0',\n  },\n});\n\nconst model = defineModel('model', {\n  initialState: state,\n  persist: {\n    dump(state) {\n      return state.hello;\n    },\n    load(s) {\n      expectType<TypeEqual<string, typeof s>>(true);\n      expectType<TypeEqual<GetInitialState<typeof state>, typeof this>>(true);\n      return { hello: s };\n    },\n  },\n});\n\ncloneModel('model-1', model, {\n  persist: {},\n});\n\ncloneModel('model-1', model, {\n  persist: {\n    version: '',\n  },\n});\n\ncloneModel('model-1', model, {\n  persist: {\n    dump(state) {\n      return state.hello;\n    },\n    load(s) {\n      expectType<TypeEqual<string, typeof s>>(true);\n      expectType<TypeEqual<GetInitialState<typeof state>, typeof this>>(true);\n      return { hello: s };\n    },\n  },\n});\n\ncloneModel('model-1', model, {\n  persist: {\n    dump() {\n      return 0;\n    },\n    load(s) {\n      expectType<TypeEqual<number, typeof s>>(true);\n      return { hello: String(s) };\n    },\n  },\n});\n\ncloneModel('model-1', model, {\n  // @ts-expect-error\n  persist: {\n    dump() {\n      return 0;\n    },\n  },\n});\n\ncloneModel('model-1', model, {\n  // @ts-expect-error\n  persist: {\n    load(dumpData) {\n      return dumpData as typeof state;\n    },\n  },\n});\n"
  },
  {
    "path": "test/typescript/use-isolate.check.ts",
    "content": "import { TypeEqual, expectType } from 'ts-expect';\nimport { defineModel, useIsolate, useLoading, useModel } from '../../src';\nimport { basicModel } from '../models/basic.model';\n\nconst isolatedModel = useIsolate(basicModel);\n\nuseModel(isolatedModel);\nuseModel(isolatedModel, (state) => state.count);\nuseLoading(isolatedModel.pureAsync);\nuseLoading(isolatedModel.pureAsync.room);\n\nuseModel(basicModel, isolatedModel);\n{\n  const model = useModel(isolatedModel, basicModel);\n  expectType<\n    TypeEqual<\n      {\n        basic: { count: number; hello: string };\n      } & {\n        [x: string]: {\n          count: number;\n          hello: string;\n        };\n      },\n      typeof model\n    >\n  >(true);\n}\n\nuseModel(isolatedModel, basicModel, () => {});\n\n{\n  const model = useIsolate(isolatedModel);\n  expectType<TypeEqual<typeof isolatedModel, typeof model>>(true);\n}\n// @ts-expect-error\ncloneModel(isolatedModel);\n\ndefineModel('', {\n  initialState: {},\n  events: {\n    onDestroy() {\n      expectType<never>(this);\n    },\n  },\n});\n"
  },
  {
    "path": "test/typescript/use-loading.check.ts",
    "content": "import { expectType } from 'ts-expect';\nimport { useLoading } from '../../src';\nimport { basicModel } from '../models/basic.model';\n\nexpectType<boolean>(useLoading(basicModel.bar));\nexpectType<boolean>(useLoading(basicModel.foo, basicModel.bar));\n// @ts-expect-error\nuseLoading(basicModel.minus);\n// @ts-expect-error\nuseLoading(basicModel);\n// @ts-expect-error\nuseLoading({});\n\nexpectType<boolean>(useLoading(basicModel.foo.room).find('xx'));\nexpectType<boolean>(useLoading(basicModel.foo.room, 'xx'));\n// @ts-expect-error\nuseLoading(basicModel.foo.room, basicModel.foo);\n// @ts-expect-error\nuseLoading(basicModel.foo.room, true);\n// @ts-expect-error\nuseLoading(basicModel.foo.room, false);\n// @ts-expect-error\nuseLoading(basicModel.normalMethod.room);\n// @ts-expect-error\nuseLoading(basicModel.normalMethod.assign);\n// @ts-expect-error\nuseLoading(basicModel.minus.room);\n"
  },
  {
    "path": "test/typescript/use-model.check.ts",
    "content": "import { expectType } from 'ts-expect';\nimport { useModel } from '../../src';\nimport { basicModel } from '../models/basic.model';\nimport { complexModel } from '../models/complex.model';\n\nconst basic = useModel(basicModel);\n\nexpectType<number>(basic.count);\nexpectType<string>(basic.hello);\n// @ts-expect-error\nbasic.notExist;\n\nconst count = useModel(basicModel, (state) => state.count);\nexpectType<number>(count);\n\nconst obj = useModel(basicModel, complexModel);\nexpectType<number>(obj.basic.count);\nexpectType<number[]>(obj.complex.ids);\n// @ts-expect-error\nobj.notExists;\n\nconst hello = useModel(\n  basicModel,\n  complexModel,\n  (basic, complex) => basic.hello + complex.ids.length,\n);\n\nexpectType<string>(hello);\n"
  },
  {
    "path": "test/use-computed.test.ts",
    "content": "import { act } from '@testing-library/react';\nimport { renderHook } from './helpers/render-hook';\nimport { store, useComputed } from '../src';\nimport { computedModel } from './models/computed.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('get state from computed value', () => {\n  const { result } = renderHook(() => useComputed(computedModel.fullName));\n\n  expect(result.current).toEqual('ticktock');\n\n  act(() => {\n    computedModel.changeFirstName('hello');\n  });\n  expect(result.current).toEqual('hellotock');\n\n  act(() => {\n    computedModel.changeFirstName('tick');\n  });\n  expect(result.current).toEqual('ticktock');\n\n  act(() => {\n    computedModel.changeLastName('world');\n  });\n  expect(result.current).toEqual('tickworld');\n});\n\ntest('with parameters', () => {\n  const { result } = renderHook(() =>\n    useComputed(computedModel.withMultipleParameters, 43, 'address'),\n  );\n\n  expect(result.current).toEqual('tick-age-43-addr-address');\n\n  act(() => {\n    computedModel.changeFirstName('musk');\n  });\n  expect(result.current).toEqual('musk-age-43-addr-address');\n});\n"
  },
  {
    "path": "test/use-isolate.test.tsx",
    "content": "import { act, cleanup, render } from '@testing-library/react';\nimport { useEffect, useState } from 'react';\nimport sleep from 'sleep-promise';\nimport {\n  defineModel,\n  FocaProvider,\n  store,\n  useLoading,\n  useIsolate,\n  Model,\n  useModel,\n} from '../src';\nimport { loadingStore } from '../src/store/loading-store';\nimport { renderHook } from './helpers/render-hook';\nimport { basicModel } from './models/basic.model';\n\n(['development', 'production'] as const).forEach((env) => {\n  describe(`[${env} mode]`, () => {\n    beforeEach(() => {\n      store.init();\n      process.env.NODE_ENV = env;\n    });\n\n    afterEach(async () => {\n      process.env.NODE_ENV = 'testing';\n      cleanup();\n      await sleep(10);\n      store.unmount();\n    });\n\n    test('can register to modelStore and remove from modelStore', async () => {\n      const { result, unmount } = renderHook(() => useIsolate(basicModel));\n\n      expect(result.current).not.toBe(basicModel);\n      expect(store.getState()).toHaveProperty(\n        result.current.name,\n        result.current.state,\n      );\n\n      unmount();\n      await sleep(1);\n      expect(store.getState()).not.toHaveProperty(result.current.name);\n    });\n\n    test('can register to loadingStore and remove from loadingStore', async () => {\n      const { result, unmount } = renderHook(() => {\n        const model = useIsolate(basicModel);\n        useLoading(basicModel.pureAsync);\n        useLoading(model.pureAsync);\n\n        return model;\n      });\n\n      const key1 = `${result.current.name}.pureAsync`;\n      const key2 = `${basicModel.name}.pureAsync`;\n      expect(loadingStore.getState()).not.toHaveProperty(key1);\n\n      await act(async () => {\n        const promise1 = result.current.pureAsync();\n        const promise2 = basicModel.pureAsync();\n\n        expect(loadingStore.getState()).toHaveProperty(key1);\n        expect(loadingStore.getState()).toHaveProperty(key2);\n\n        await promise1;\n        await promise2;\n      });\n\n      expect(loadingStore.getState()).toHaveProperty(key1);\n      expect(loadingStore.getState()).toHaveProperty(key2);\n\n      unmount();\n      await sleep(1);\n      expect(loadingStore.getState()).not.toHaveProperty(key1);\n      expect(loadingStore.getState()).toHaveProperty(key2);\n    });\n\n    test('call onDestroy event when local model is destroyed', async () => {\n      const spy = vitest.fn();\n      const globalModel = defineModel('isolate-demo-1', {\n        initialState: {},\n        events: {\n          onDestroy: spy,\n        },\n      });\n\n      const { unmount } = renderHook(() => useIsolate(globalModel));\n\n      expect(spy).toBeCalledTimes(0);\n      unmount();\n      await sleep(1);\n      expect(spy).toBeCalledTimes(1);\n      basicModel.plus(1);\n      expect(spy).toBeCalledTimes(1);\n    });\n\n    test('recreate isolated model when global model changed', async () => {\n      const globalModel = defineModel('isolate-demo-2', {\n        initialState: {},\n      });\n\n      const { result } = renderHook(() => {\n        const [state, setState] = useState<Model>(basicModel);\n\n        const model = useIsolate(state);\n\n        useEffect(() => {\n          setTimeout(() => {\n            setState(globalModel);\n          }, 20);\n        }, []);\n\n        return model;\n      });\n\n      const name1 = result.current.name;\n      expect(name1).toMatch(basicModel.name);\n      expect(store.getState()).toHaveProperty(name1);\n\n      await act(async () => {\n        await sleep(30);\n      });\n\n      await sleep(10);\n\n      expect(result.current.name).not.toBe('isolate-demo-2');\n      expect(result.current.name).toMatch('isolate-demo-2');\n      expect(store.getState()).not.toHaveProperty(name1);\n    });\n\n    test.runIf(env === 'development')(\n      'Can get component name in dev mode',\n      () => {\n        let model!: Model;\n        function MyApp() {\n          model = useIsolate(basicModel);\n          return null;\n        }\n\n        render(\n          <FocaProvider>\n            <MyApp />\n          </FocaProvider>,\n        );\n\n        expect(model.name).toMatch('MyApp#');\n      },\n    );\n\n    test('can use with global model', async () => {\n      let model: typeof basicModel;\n      const { result } = renderHook(() => {\n        // @ts-expect-error\n        model = useIsolate(basicModel);\n        return useModel(model, basicModel, (local, basic) => {\n          return `local: ${local.count}, basic: ${basic.count}`;\n        });\n      });\n\n      expect(result.current).toBe('local: 0, basic: 0');\n      act(() => {\n        basicModel.plus(12);\n      });\n      expect(result.current).toBe('local: 0, basic: 12');\n      act(() => {\n        model.plus(7);\n      });\n      expect(result.current).toBe('local: 7, basic: 12');\n    });\n\n    test('can isolate from isolate model', async () => {\n      let model1: typeof basicModel;\n      let model2: typeof basicModel;\n      const { result } = renderHook(() => {\n        // @ts-expect-error\n        model1 = useIsolate(basicModel);\n        // @ts-expect-error\n        model2 = useIsolate(model1);\n        return useModel(model1, model2, (local1, local2) => {\n          return `local1: ${local1.count}, local2: ${local2.count}`;\n        });\n      });\n\n      expect(result.current).toBe('local1: 0, local2: 0');\n      act(() => {\n        model1.plus(12);\n      });\n      expect(result.current).toBe('local1: 12, local2: 0');\n      act(() => {\n        model2.plus(7);\n      });\n      expect(result.current).toBe('local1: 12, local2: 7');\n    });\n  });\n});\n"
  },
  {
    "path": "test/use-loading.test.ts",
    "content": "import { act } from '@testing-library/react';\nimport { renderHook } from './helpers/render-hook';\nimport { store, useLoading } from '../src';\nimport { basicModel } from './models/basic.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('Trace loading', async () => {\n  const { result } = renderHook(() => useLoading(basicModel.pureAsync));\n\n  expect(result.current).toBeFalsy();\n\n  let promise!: Promise<any>;\n\n  act(() => {\n    promise = basicModel.pureAsync();\n  });\n\n  expect(result.current).toBeTruthy();\n\n  await act(async () => {\n    await promise;\n  });\n\n  expect(result.current).toBeFalsy();\n});\n\ntest('Compose the loadings', async () => {\n  const { result } = renderHook(() =>\n    useLoading(basicModel.pureAsync, basicModel.foo, basicModel.bar),\n  );\n\n  expect(result.current).toBeFalsy();\n\n  let promise1!: Promise<any>;\n\n  act(() => {\n    promise1 = basicModel.pureAsync();\n  });\n\n  expect(result.current).toBeTruthy();\n\n  let promise2!: Promise<any>;\n\n  await act(async () => {\n    await promise1;\n    promise2 = basicModel.foo('', 2);\n  });\n\n  expect(result.current).toBeTruthy();\n\n  await act(async () => {\n    await promise2;\n  });\n\n  expect(result.current).toBeFalsy();\n});\n\ntest('Trace loadings', async () => {\n  const { result } = renderHook(() =>\n    useLoading(basicModel.pureAsync.room, 'x'),\n  );\n\n  expect(result.current).toBeFalsy();\n\n  let promise!: Promise<any>;\n\n  act(() => {\n    promise = basicModel.pureAsync.room('x').execute();\n  });\n\n  expect(result.current).toBeTruthy();\n\n  await act(async () => {\n    await promise;\n  });\n\n  expect(result.current).toBeFalsy();\n});\n\ntest('Pick loading from loadings', async () => {\n  const { result } = renderHook(() => useLoading(basicModel.pureAsync.room));\n\n  expect(result.current.find('m')).toBeFalsy();\n  expect(result.current.find('n')).toBeFalsy();\n\n  let promise!: Promise<any>;\n\n  act(() => {\n    promise = basicModel.pureAsync.room('m').execute();\n  });\n\n  expect(result.current.find('m')).toBeTruthy();\n  expect(result.current.find('n')).toBeFalsy();\n\n  await act(async () => {\n    await promise;\n  });\n\n  expect(result.current.find('m')).toBeFalsy();\n  expect(result.current.find('n')).toBeFalsy();\n});\n"
  },
  {
    "path": "test/use-model.test.ts",
    "content": "import { act } from '@testing-library/react';\nimport { renderHook } from './helpers/render-hook';\nimport { store, useModel } from '../src';\nimport { basicModel, basicSkipRefreshModel } from './models/basic.model';\nimport { complexModel } from './models/complex.model';\n\nbeforeEach(() => {\n  store.init();\n});\n\nafterEach(() => {\n  store.unmount();\n});\n\ntest('get state from one model', () => {\n  const { result } = renderHook(() => useModel(basicModel));\n\n  expect(result.current.count).toEqual(0);\n\n  act(() => {\n    basicModel.plus(1);\n  });\n\n  expect(result.current.count).toEqual(1);\n});\n\ntest('get state from multiple models', () => {\n  const { result } = renderHook(() => useModel(basicModel, complexModel));\n\n  expect(result.current).toMatchObject({\n    basic: {},\n    complex: {},\n  });\n\n  act(() => {\n    basicModel.plus(1);\n    complexModel.addUser(10, 'Lucifer');\n  });\n\n  expect(result.current.basic.count).toEqual(1);\n  expect(result.current.complex.users[10]).toEqual('Lucifer');\n});\n\ntest('get state with selector', () => {\n  const { result } = renderHook(() =>\n    useModel(basicModel, (state) => state.count * 10),\n  );\n\n  expect(result.current).toEqual(0);\n\n  act(() => {\n    basicModel.plus(2);\n  });\n\n  expect(result.current).toEqual(20);\n\n  act(() => {\n    basicModel.plus(3);\n  });\n\n  expect(result.current).toEqual(50);\n});\n\ntest('get multiple state with selector', () => {\n  const { result } = renderHook(() =>\n    useModel(basicModel, complexModel, (a, b) => a.count + b.ids.length),\n  );\n\n  expect(result.current).toEqual(0);\n\n  act(() => {\n    basicModel.plus(1);\n    complexModel.addUser(5, 'li');\n    complexModel.addUser(6, 'lu');\n  });\n\n  expect(result.current).toEqual(3);\n});\n\ntest('specific compare algorithm', async () => {\n  {\n    const hook = renderHook(() =>\n      useModel(\n        basicModel,\n        complexModel,\n        (a, b) => ({\n          a,\n          b,\n        }),\n        'strictEqual',\n      ),\n    );\n    const prevValue = hook.result.current;\n    act(() => {\n      hook.rerender();\n    });\n    expect(hook.result.current !== prevValue).toBeTruthy();\n  }\n\n  {\n    const hook = renderHook(() =>\n      useModel(\n        basicModel,\n        complexModel,\n        (a, b) => ({\n          a,\n          b,\n        }),\n        'shallowEqual',\n      ),\n    );\n    const prevValue = hook.result.current;\n    act(() => {\n      hook.rerender();\n    });\n    expect(hook.result.current === prevValue).toBeTruthy();\n  }\n\n  {\n    const hook = renderHook(() =>\n      useModel(\n        basicModel,\n        complexModel,\n        (a, b) => ({\n          a,\n          b,\n        }),\n        'deepEqual',\n      ),\n    );\n    const prevValue = hook.result.current;\n    act(() => {\n      hook.rerender();\n    });\n    expect(hook.result.current === prevValue).toBeTruthy();\n  }\n});\n\ntest('Memoize the selector result', () => {\n  const fn1 = vitest.fn();\n  const fn2 = vitest.fn();\n\n  const { result: result1 } = renderHook(() => {\n    return useModel(basicModel, (state) => {\n      fn1();\n      return state.count;\n    });\n  });\n\n  const { result: result2 } = renderHook(() => {\n    return useModel(basicModel, basicSkipRefreshModel, (state1, state2) => {\n      fn2();\n      return state1.count + state2.count;\n    });\n  });\n\n  expect(fn1).toBeCalledTimes(1);\n  expect(fn2).toBeCalledTimes(1);\n\n  act(() => {\n    basicModel.plus(6);\n  });\n\n  expect(result1.current).toBe(6);\n  expect(result2.current).toBe(6);\n  expect(fn1).toBeCalledTimes(3);\n  expect(fn2).toBeCalledTimes(3);\n\n  act(() => {\n    // Sure not basicModel, we need trigger subscriptions\n    complexModel.addUser(1, '');\n  });\n  expect(result1.current).toBe(6);\n  expect(result2.current).toBe(6);\n  expect(fn1).toBeCalledTimes(3);\n  expect(fn2).toBeCalledTimes(3);\n\n  act(() => {\n    // Sure not basicModel, we need trigger subscriptions\n    complexModel.addUser(2, 'L');\n  });\n  expect(result1.current).toBe(6);\n  expect(result2.current).toBe(6);\n  expect(fn1).toBeCalledTimes(3);\n  expect(fn2).toBeCalledTimes(3);\n\n  act(() => {\n    basicModel.plus(1);\n  });\n  expect(result1.current).toBe(7);\n  expect(result2.current).toBe(7);\n  expect(fn1).toBeCalledTimes(5);\n  expect(fn2).toBeCalledTimes(5);\n\n  act(() => {\n    basicSkipRefreshModel.plus(1);\n  });\n  expect(result1.current).toBe(7);\n  expect(result2.current).toBe(8);\n  expect(fn1).toBeCalledTimes(5);\n  expect(fn2).toBeCalledTimes(7);\n\n  fn1.mockRestore();\n  fn2.mockRestore();\n});\n\ntest('Hooks keep working after hot reload', async () => {\n  const { result } = renderHook(() => useModel(basicModel));\n\n  expect(result.current.count).toEqual(0);\n\n  store.init();\n\n  act(() => {\n    basicModel.plus(1);\n  });\n\n  expect(result.current.count).toEqual(1);\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping\n    \"target\": \"ES2015\",\n    \"module\": \"ES2015\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"allowJs\": false,\n    \"declaration\": true,\n    \"outDir\": \"./build\",\n    \"rootDir\": \".\",\n    \"importHelpers\": false,\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"strictBindCallApply\": true,\n    \"strictPropertyInitialization\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedIndexedAccess\": true,\n\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitOverride\": true,\n    \"useUnknownInCatchVariables\": true\n  }\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from 'tsup';\n\nexport default defineConfig({\n  entry: ['src/index.ts'],\n  splitting: true,\n  sourcemap: true,\n  clean: true,\n  format: ['cjs', 'esm'],\n  platform: 'node',\n  tsconfig: './tsconfig.json',\n  target: 'es2020',\n  legacyOutput: true,\n  shims: false,\n  dts: true,\n  onSuccess: 'echo {\\\\\"type\\\\\": \\\\\"module\\\\\"} > dist/esm/package.json',\n});\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "/// <reference types='vitest/globals' />\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      provider: 'istanbul',\n      enabled: true,\n      include: ['src/**'],\n      thresholds: {\n        lines: 99,\n        functions: 99,\n        branches: 99,\n        statements: 99,\n      },\n      reporter: ['html', 'lcovonly', 'text-summary'],\n    },\n    environment: 'jsdom',\n    globals: true,\n    snapshotFormat: {\n      escapeString: false,\n      printBasicPrototype: false,\n    },\n  },\n});\n"
  }
]