Full Code of foca-js/foca for AI

master 97b6d78d3d0e cached
120 files
255.9 KB
77.9k tokens
292 symbols
1 requests
Download .txt
Showing preview only (306K chars total). Download the full file or copy to clipboard to get everything.
Repository: foca-js/foca
Branch: master
Commit: 97b6d78d3d0e
Files: 120
Total size: 255.9 KB

Directory structure:
gitextract_3jrecnzc/

├── .github/
│   └── workflows/
│       ├── codeql.yml
│       ├── prerelease.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .husky/
│   ├── commit-msg
│   └── pre-commit
├── .prettierignore
├── .prettierrc.yml
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs/
│   ├── .nojekyll
│   ├── CNAME
│   ├── _sidebar.md
│   ├── advanced.md
│   ├── api.md
│   ├── changelog.md
│   ├── donate.md
│   ├── events.md
│   ├── home.md
│   ├── index.html
│   ├── initialize.md
│   ├── model.md
│   ├── persist.md
│   ├── redux-toolkit.md
│   ├── test.md
│   └── troubleshooting.md
├── package.json
├── src/
│   ├── actions/
│   │   ├── loading.ts
│   │   ├── model.ts
│   │   ├── persist.ts
│   │   └── refresh.ts
│   ├── api/
│   │   ├── get-loading.ts
│   │   ├── use-computed.ts
│   │   ├── use-isolate.ts
│   │   ├── use-loading.ts
│   │   └── use-model.ts
│   ├── engines/
│   │   ├── memory.ts
│   │   └── storage-engine.ts
│   ├── index.ts
│   ├── middleware/
│   │   ├── action-in-action.interceptor.ts
│   │   ├── destroy-loading.interceptor.ts
│   │   ├── freeze-state.middleware.ts
│   │   ├── loading.interceptor.ts
│   │   └── model.interceptor.ts
│   ├── model/
│   │   ├── clone-model.ts
│   │   ├── define-model.ts
│   │   ├── enhance-action.ts
│   │   ├── enhance-computed.ts
│   │   ├── enhance-effect.ts
│   │   ├── guard.ts
│   │   └── types.ts
│   ├── persist/
│   │   ├── persist-gate.tsx
│   │   ├── persist-item.ts
│   │   └── persist-manager.ts
│   ├── reactive/
│   │   ├── computed-value.ts
│   │   ├── create-computed-deps.ts
│   │   ├── deps-collector.ts
│   │   └── object-deps.ts
│   ├── redux/
│   │   ├── connect.ts
│   │   ├── contexts.ts
│   │   ├── create-reducer.ts
│   │   ├── foca-provider.tsx
│   │   └── use-selector.ts
│   ├── store/
│   │   ├── loading-store.ts
│   │   ├── model-store.ts
│   │   ├── proxy-store.ts
│   │   └── store-basic.ts
│   └── utils/
│       ├── deep-equal.ts
│       ├── get-method-category.ts
│       ├── getter.ts
│       ├── immer.ts
│       ├── is-promise.ts
│       ├── is-type.ts
│       ├── serialize.ts
│       ├── symbol-observable.ts
│       ├── to-args.ts
│       └── to-promise.ts
├── test/
│   ├── __snapshots__/
│   │   └── serialize.test.ts.snap
│   ├── action-in-action.test.tsx
│   ├── build.test.ts
│   ├── clone.test.ts
│   ├── computed.test.ts
│   ├── connect.test.tsx
│   ├── deep-equal.test.ts
│   ├── engine.test.ts
│   ├── fixtures/
│   │   ├── equals.ts
│   │   └── not-equals.ts
│   ├── get-loading.test.ts
│   ├── helpers/
│   │   ├── render-hook.tsx
│   │   └── slow-engine.ts
│   ├── lifecycle.test.ts
│   ├── middleware.test.ts
│   ├── model.test.ts
│   ├── models/
│   │   ├── basic.model.ts
│   │   ├── complex.model.ts
│   │   ├── computed.model.ts
│   │   └── persist.model.ts
│   ├── persist.gate.test.tsx
│   ├── persist.test.ts
│   ├── provider.test.tsx
│   ├── serialize.test.ts
│   ├── store.test.ts
│   ├── typescript/
│   │   ├── computed.check.ts
│   │   ├── define-model.check.ts
│   │   ├── get-loading.check.ts
│   │   ├── persist.check.ts
│   │   ├── use-isolate.check.ts
│   │   ├── use-loading.check.ts
│   │   └── use-model.check.ts
│   ├── use-computed.test.ts
│   ├── use-isolate.test.tsx
│   ├── use-loading.test.ts
│   └── use-model.test.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/codeql.yml
================================================
name: 'CodeQL'

on:
  push:
    branches: ['master']
  pull_request:
    branches: ['master']
  schedule:
    - cron: '29 3 * * 1'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [javascript]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: ${{ matrix.language }}
          queries: +security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v2

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2
        with:
          category: '/language:${{ matrix.language }}'


================================================
FILE: .github/workflows/prerelease.yml
================================================
name: Pre Release

on:
  release:
    types: [prereleased]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          cache: 'pnpm'
          node-version-file: 'package.json'
      - run: pnpm install
      - uses: JS-DevTools/npm-publish@v3
        with:
          token: ${{ secrets.NPM_TOKEN }}
          access: public
          tag: next


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  release:
    types: [released]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          cache: 'pnpm'
          node-version-file: 'package.json'
      - run: pnpm install
      - run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
      - run: npm publish


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on:
  push:
    branches:
      - '*'
    tags-ignore:
      - '*'
  pull_request:
    branches:

jobs:
  formatting:
    if: "!contains(toJson(github.event.commits), '[skip ci]')"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: 'package.json'
          cache: 'pnpm'
      - run: pnpm install
      - run: npx --no-install prettier --cache --verbose --check .

  type-checking:
    if: "!contains(toJson(github.event.commits), '[skip ci]')"
    strategy:
      matrix:
        ts-version:
          [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]
        react-version: [18.x, 19.x]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - name: Use Typescript ${{ matrix.ts-version }} & React ${{ matrix.react-version }}
        uses: actions/setup-node@v4
        with:
          cache: 'pnpm'
          node-version-file: 'package.json'
      - run: |
          pnpm install
          pnpm add -D \
            typescript@${{ matrix.ts-version }} \
            @types/react@${{ matrix.react-version }} \
            @types/react-dom@${{ matrix.react-version }}
      - run: npx --no-install tsc --noEmit

  test:
    if: "!contains(toJson(github.event.commits), '[skip ci]')"
    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]
        react-version: [18.x, 19.x]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - name: Use Node.js ${{ matrix.node-version }} & React ${{ matrix.react-version }}
        uses: actions/setup-node@v4
        with:
          cache: 'pnpm'
          node-version: ${{ matrix.node-version }}
      - run: |
          pnpm install
          pnpm add -D \
            react@${{ matrix.react-version }} \
            react-dom@${{ matrix.react-version }} \
            react-test-renderer@${{ matrix.react-version }}
      - run: pnpm run test
      - name: Upload Coverage
        uses: actions/upload-artifact@v4
        if: github.ref == 'refs/heads/master' && strategy.job-index == 0
        with:
          name: coverage
          path: coverage
          if-no-files-found: error
          retention-days: 1

  coverage:
    if: github.ref == 'refs/heads/master'
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Download Coverage
        uses: actions/download-artifact@v4
        with:
          name: coverage
          path: coverage
      - uses: codecov/codecov-action@v5


================================================
FILE: .gitignore
================================================
.idea/
.DS_Store
dist/
*.log
node_modules/
coverage
.nyc_output
TODO
/test.*
/dist/
/build/


================================================
FILE: .husky/commit-msg
================================================
npx --no-install commitlint --edit $1


================================================
FILE: .husky/pre-commit
================================================
npx --no-install prettier --cache --check .


================================================
FILE: .prettierignore
================================================
dist/
build/
coverage/
pnpm-lock.yaml
_sidebar.md


================================================
FILE: .prettierrc.yml
================================================
semi: true
singleQuote: true
# Change when properties in objects are quoted.
# If at least one property in an object requires quotes, quote all properties.
quoteProps: consistent
tabWidth: 2
printWidth: 80
endOfLine: lf
trailingComma: all
bracketSpacing: true
# Include parentheses around a sole arrow function parameter.
arrowParens: always
proseWrap: preserve
jsxSingleQuote: false
# Put > on the last line instead of at a new line.
bracketSameLine: false


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["esbenp.prettier-vscode"]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "typescript.preferences.quoteStyle": "single",
  "typescript.suggest.autoImports": true,
  "editor.tabSize": 2,
  "typescript.tsdk": "node_modules/typescript/lib",
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
}


================================================
FILE: CHANGELOG.md
================================================
## [4.0.1](https://github.com/foca-js/foca/compare/v4.0.0...v4.0.1)  (2025-03-03)

- 发布时未生成dist目录

## [4.0.0](https://github.com/foca-js/foca/compare/v3.2.0...v4.0.0)  (2025-03-03)

- 升级npm包的目标语法 ES5 -> ES2020
- 删除废弃的导出变量 `engines.localStorage` 和 `engines.sessionStorage`
- react-native 的最低版本要求为 0.69
- 底层包 redux 升级 4 -> 5 (目标语法为esnext)
- 底层包 react-redux 升级 8 -> 9 (目标语法为esnext)

## [3.2.0](https://github.com/foca-js/foca/compare/v3.1.1...v3.2.0)  (2023-10-20)

- 增加局部模型钩子 `useIsolate`

## [3.1.1](https://github.com/foca-js/foca/compare/v3.1.0...v3.1.1)  (2023-10-14)

- computed内使用数组状态数据时,更新数组不会触发重新计算
- 删除无用类型 `ComputedRef`

## [3.1.0](https://github.com/foca-js/foca/compare/v3.0.0...v3.1.0)  (2023-10-10)

- 持久化引擎支持`同步`引擎。因此可以直接使用浏览器内置的 localStorage 和 sessionStorage 接口

## [3.0.0](https://github.com/foca-js/foca/compare/v2.0.1...v3.0.0)  (2023-10-08)

### 破坏性更新

- 删除hooks函数 `useDefined`
- 删除模型内`onDestroy`事件钩子
- 删除持久化`maxAge`配置

```diff
store.init({
  persist: [
    {
      key: 'key',
-     maxAge: 3600,
      engines: engines.localStorage,
      models: [],
    }
  ]
})
```

- 非hooks状态下计算属性使用执行函数的方式获取

```diff
const model = defineModel('model', {
  initialState: { firstName: '', lastName: '' },
  methods: {
    myMethod() {
-     return this.fullName.value;
+     return this.fullName();
    }
  },
  computed: {
   fullName() {
     return this.state.firstName + this.state.lastName;
   },
  }
});
```

### 新特性

- 开启持久化的模型会立即存储initialState
- 计算属性支持传递参数

```diff
const model = defineModel('model', {
  initialState: { firstName: '', lastName: '' },
  methods: {
    myMethod() {
+     const profile = this.profile(30, 'addr', false);
    }
  },
  computed: {
   fullName() {
     return this.state.firstName + this.state.lastName;
   },
+  profile(age: number, address: string, coding: boolean = true) {
+    return this.fullName() + '-' + age + address + coding;
+  },
  }
});

const App: FC = () => {
  const fullName = useComputed(model.fullName);
+ const profile = useComputed(model.profile, 20, 'my-address');
}
```

- 持久化增加 **dump** 和 **load** 两个系列化函数

```diff
const model = defineModel('model', {
  initialState: { firstName: 'tick', lastName: 'tock' },
  persist: {
+   dump(state) {
+     return state.firstName;
+   },
+   load(dumpData) {
+     return { ...this.initialState, firstName: dumpData };
+   },
  }
});
```

- 持久化新增合并模式 `replace`, `merge`<small>(默认)</small>, `deep-merge`

```diff
store.init({
  persist: [
    {
      key: 'item1',
      version: '1.0',
+     merge: 'replace',
      engine: engines.localStorage,
      models: [],
    },
  ]
})
```

### 其它

- npm包转译为ES5语法以兼容更早的浏览器 (#41)
- immer版本降级:10.0.2 -> 9.0.21
- react-redux版本升级:8.1.2 -> 8.1.3

## [2.0.1](https://github.com/foca-js/foca/compare/v2.0.0...v2.0.1)&nbsp;&nbsp;(2023-08-10)

- react-redux 版本从 8.1.1 升级到 8.1.2 (#40)

## [2.0.0](https://github.com/foca-js/foca/compare/v1.3.1...v2.0.0)&nbsp;&nbsp;(2023-06-26)

- 不再兼容 IE 浏览器
- 最小 React 版本为 18
- 最小 TypeScript 版本为 5.0
- immer 版本从 9.0.21 升级到 10.0.2
- react-redux 版本从 8.0.5 升级到 8.1.1
- 删除废弃字段 `actions` 和 `effects`

## [1.3.1](https://github.com/foca-js/foca/compare/v1.3.0...v1.3.1)&nbsp;&nbsp;(2023-04-14)

- 在 onInit 事件内执行的 async method 未储存 loading 状态 (#38)
- immer 版本从 9.0.16 升级到 9.0.21
- redux 版本从 4.2.0 升级到 4.2.1
- 支持 typescript@5

## [1.3.0](https://github.com/foca-js/foca/compare/v1.2.1...v1.3.0)&nbsp;&nbsp;(2022-12-17)

- `setState` 的回调模式支持返回不完整数据

```typescript
defineModel('unique_name', {
  initialState: { a: 'a', b: 'b' },
  methods: {
    test() {
      this.setState(() => {
        return { a: 'xxx' };
      });
      console.log(this.state); // { a: 'xxx', b: 'b' }
    },
  },
});
```

- 传给 reducer 的 `initialState` 不再执行深拷贝,而是在开发环境下进行冻结处理以防开发者错误操作

```typescript
const initialState = { a: 'a', b: 'b' };

defineModel('unique_name', {
  initialState: initialState,
});

// 修改失败,严格模式下会报错
// TypeError: Cannot assign to read only property 'a' of object '#<Object>'
initialState.a = 'xxx';
```

## [1.2.1](https://github.com/foca-js/foca/compare/v1.2.0...v1.2.1)&nbsp;&nbsp;(2022-11-11)

- 销毁模型时可能触发`onChange`勾子

## [1.2.0](https://github.com/foca-js/foca/compare/v1.1.0...v1.2.0)&nbsp;&nbsp;(2022-11-10)

- 重命名 actions 为 reducers (#29)
- 重命名 effects 为 methods (#29)
- exports 导出 package.json 文件
- 不再支持typescript@4.4

## [1.1.0](https://github.com/foca-js/foca/compare/v1.0.2...v1.1.0)&nbsp;&nbsp;(2022-07-07)

- setState 支持传入非完整数据

```typescript
const initialState = { a: 1, b: 'x' };

this.setState({ a: 2, b: 'y' }); // state === { a: 2, b: 'y' }
this.setState({ a: 123 }); // state === { a: 123, b: 'y' }
this.setState({ b: 'hello' }); // state === { a: 123, b: 'hello' }
```

## [1.0.2](https://github.com/foca-js/foca/compare/v1.0.1...v1.0.2)&nbsp;&nbsp;(2022-06-30)

- 修复在 react17 下 action-in-action 中间件可能报错的问题 (#20)

## [1.0.1](https://github.com/foca-js/foca/compare/v1.0.0...v1.0.1)&nbsp;&nbsp;(2022-06-29)

- 完善 action-in-action 的报错信息

## [1.0.0](https://github.com/foca-js/foca/compare/v0.12.3...v1.0.0)&nbsp;&nbsp;(2022-06-17)

### 不兼容更新

- 删除已废弃函数 ~~`useDefinedModel`~~,代替函数:`useDefined`
- 删除已废弃属性 ~~`hooks`~~,代替属性:`events`
- 删除已废弃的方法 ~~`assign`~~ ,代替方法:`room`
- 支持最小 React 版本为 `16.14.0`

### 特性

- 在开发环境下检测 `action in action` 的错误操作并抛出异常

## [0.12.3](https://github.com/foca-js/foca/compare/v0.12.2...v0.12.3)&nbsp;&nbsp;(2022-06-15)

- 废弃函数 `useDefinedModel`,并新增函数 `useDefined` 作为代替
- 修复计算属性在返回 **原始数组** 或者 **原始对象** 时无法访问的问题
- 优化 initialState 深拷贝速度

## [0.12.2](https://github.com/foca-js/foca/compare/v0.12.1...v0.12.2)&nbsp;&nbsp;(2022-06-08)

- initialState 现在支持传递 `undefined` 值 (#17)

## [0.12.1](https://github.com/foca-js/foca/compare/v0.12.0...v0.12.1)&nbsp;&nbsp;(2022-05-27)

- 开发模式下局部模型的名称携带组件名称以方便调试

## [0.12.0](https://github.com/foca-js/foca/compare/v0.11.7...v0.12.0)&nbsp;&nbsp;(2022-05-26)

- 增加局部模型接口 `useDefinedModel`,数据跟随组件挂载和释放
- 提升计算属性的脏检测效率

## [0.11.7](https://github.com/foca-js/foca/compare/v0.11.6...v0.11.7)&nbsp;&nbsp;(2022-05-17)

- 优化 loading 写入性能
- 修复 react 命名导出在 node ESM 环境中可能报错的风险
- 打包不再使用 `.mjs` 后缀,设置新的 package.json 同样可以识别成 ESM
- 不再导出`combine`方法,因为几乎用不上

## [0.11.6](https://github.com/foca-js/foca/compare/v0.11.5...v0.11.6)&nbsp;&nbsp;(2022-05-13)

- 使用`.js`文件以适配旧的打包工具

## [0.11.5](https://github.com/foca-js/foca/compare/v0.11.3...v0.11.5)&nbsp;&nbsp;(2022-05-10)

- 优化持久化逻辑
- 使用中文提示错误和警告
- 废弃 effects 中的 `assign` 方法,并新增 `room` 作为代替

```diff
const testModel = defineModel('test', {
  effects: {
    xyz(id: number) {},
  },
});

- testModel.xyz.assign(1).execute(1)
+ testModel.xyz.room(1).execute(1)

- useLoading(testModel.xyz.assign).find(1)
+ useLoading(testModel.xyz.room).find(1)
```

## [0.11.3](https://github.com/foca-js/foca/compare/v0.11.2...v0.11.3)&nbsp;&nbsp;(2022-05-07)

- 修复 setTimeout 类型 (#15)

## [0.11.2](https://github.com/foca-js/foca/compare/v0.11.1...v0.11.2)&nbsp;&nbsp;(2022-05-06)

- 提升 computed 脏检查性能

## [0.11.1](https://github.com/foca-js/foca/compare/v0.11.0...v0.11.1)&nbsp;&nbsp;(2022-04-29)

- 优化 computed in computed 时的缓存对比策略
- 废弃属性 `hooks` 并推荐使用 `events` 以防止和 react-hooks 在名字上混淆。属性 `hooks` 将在 1.0.0 版本发布时删除。

```diff
export const testModel = defineModel('test', {
  initialState,
- hooks: {},
+ events: {},
});
```

## [0.11.0](https://github.com/foca-js/foca/compare/v0.10.2...v0.11.0)&nbsp;&nbsp;(2022-04-24)

- 模型新增生命周期 `onChange(prevState, nextState)` 以监听当前模型的状态变化
- 模型新增 computed 计算属性,并新增 `useComputed` 配合使用

## [0.10.2](https://github.com/foca-js/foca/compare/v0.10.0...v0.10.2)&nbsp;&nbsp;(2022-04-21)

- 使用新的文件打包方案以解决在 node 环境下无法使用 ESM 的问题
- 使用简单的 JSON.stringify 和 JSON.parse 处理初始值的深度拷贝任务

## [0.10.0](https://github.com/foca-js/foca/compare/v0.9.3...v0.10.0)&nbsp;&nbsp;(2022-04-15)

- 支持 react-18 并发渲染

## [0.9.3](https://github.com/foca-js/foca/compare/v0.9.2...v0.9.3)&nbsp;&nbsp;(2022-04-14)

- 持久化数据有可能被初始值覆盖
- 模型名称唯一性检测

## [0.9.2](https://github.com/foca-js/foca/compare/v0.9.1...v0.9.2)&nbsp;&nbsp;(2021-12-23)

- 增强初始化时的 compose 类型
- 设置 sideEffects 以适配 tree-shaking
- 日志字符串 `redux-devtools` 现在只在非生产环境生效

## [0.9.1](https://github.com/foca-js/foca/compare/v0.9.0...v0.9.1)&nbsp;&nbsp;(2021-12-20)

- 在开发环境下允许多次执行`store.init()`以适应热重载
- 持久化解析失败时一律抛出异常

## [0.9.0](https://github.com/foca-js/foca/compare/v0.8.1...v0.9.0)&nbsp;&nbsp;(2021-12-17)

- [Breaking] 删除 `useMeta()`, `getMeta()` 接口,移除 meta 概念
- 修复 IDE 中 React 组件调用的模型方法无法点击跳转回模型的问题

## [0.8.1](https://github.com/foca-js/foca/compare/v0.8.0...v0.8.1)&nbsp;&nbsp;(2021-12-17)

- 私有方法在运行时也不该被导出

## [0.8.0](https://github.com/foca-js/foca/compare/v0.7.1...v0.8.0)&nbsp;&nbsp;(2021-12-17)

- 支持私有方法,在模型外部使用会触发 TS 报错(属性不存在)

## [0.7.1](https://github.com/foca-js/foca/compare/v0.7.0...v0.7.1)&nbsp;&nbsp;(2021-12-13)

- 通过缓存提升 useModel 的性能

## [0.7.0](https://github.com/foca-js/foca/compare/v0.6.0...v0.7.0)&nbsp;&nbsp;(2021-12-12)

- [Breaking] ctx.dispatch 重命名为 ctx.setState

```diff
difineModel('name', {
  effects: {
    foo() {
-     this.dispatch({ count: 1 });
+     this.setState({ count: 1 });
    }
  }
})
```

- 删除部分继承的 Error 类,直接使用原生 Error
- 过期的持久化数据不再自动重新生成

## [0.6.0](https://github.com/foca-js/foca/compare/v0.5.0...v0.6.0)&nbsp;&nbsp;(2021-12-10)

- [Breaking] 删除 Map/Set 特性
- 内置并简化深对比函数

## [0.5.0](https://github.com/foca-js/foca/compare/v0.4.1...v0.5.0)&nbsp;&nbsp;(2021-12-09)

- [Breaking] effect.meta() 重命名为 effect.assign()

```diff
- model.effect.meta(ID).execute(...);
+ model.effect.assign(ID).execute(...);
```

- [Breaking] {get|use}Meta 和 {get|use}Loading 的 pick() 重命名为 find()

```diff
- useLoading(model.effect, 'pick').pick(ID)
+ useLoading(model.effect.assign).find(ID)

- useLoading(model.effect, 'pick', ID)
+ useLoading(model.effect.assign, ID)
```

- 取消导出部分 redux 模块
- 增加 metas 和 loadings 在开发环境下的不可变特性

## [0.4.1](https://github.com/foca-js/foca/compare/v0.4.0...v0.4.1)&nbsp;&nbsp;(2021-12-08)

- 修复循环引用问题

## [0.4.0](https://github.com/foca-js/foca/compare/v0.3.6...v0.4.0)&nbsp;&nbsp;(2021-12-08)

- [Breaking] 删除重复且难以理解的 api `useLoadings`, `useMetas`, `getLoadings`, `getMetas`

## [0.3.6](https://github.com/foca-js/foca/compare/v0.3.5...v0.3.6)&nbsp;&nbsp;(2021-12-04)

- 模型增加钩子函数 onInit
- 修复 getLoadings 和 useLoadings 始终返回新对象的问题

## [0.3.5](https://github.com/foca-js/foca/compare/v0.3.4...v0.3.5)&nbsp;&nbsp;(2021-12-01)

- 使用 Object.assign 代替插件包 object-assign
- 增加 combine() 函数以覆盖状态库共存时使用 connect() 高阶组件的场景

## [0.3.4](https://github.com/foca-js/foca/compare/v0.3.3...v0.3.4)&nbsp;&nbsp;(2021-11-29)

- 提升 useModel 在传递单个模型时的执行效率
- useModel 没有传回调函数时,不再提供对比算法参数

## [0.3.3](https://github.com/foca-js/foca/compare/v0.3.2...v0.3.3)&nbsp;&nbsp;(2021-11-28)

- react 最小依赖版本现在为 16.9.0
- 优化 dispatch 性能
- 引入 process.env.NODE_ENV 以减少生产环境的体积

## [0.3.2](https://github.com/foca-js/foca/compare/v0.3.1...v0.3.2)&nbsp;&nbsp;(2021-11-27)

- 精简代码
- 内置插件包 symbol-observable

## [0.3.1](https://github.com/foca-js/foca/compare/v0.3.0...v0.3.1)&nbsp;&nbsp;(2021-11-26)

- 升级 immer 版本
- 重写 action 和 effect 增强函数

## [0.3.0](https://github.com/foca-js/foca/compare/v0.2.3...v0.3.0)&nbsp;&nbsp;(2021-11-24)

- [Breaking] keepStateFromRefresh 重命名为 skipRefresh
- 修复 dispatch meta 时未命中拦截条件
- 重构拦截器
- 重构 reducer 生成器
- 完善测试用例

## [0.2.3](https://github.com/foca-js/foca/compare/v0.2.2...v0.2.3)&nbsp;&nbsp;(2021-11-23)

- 对 action 进行拦截以避免无意义的状态更新和组件重渲染

## [0.2.2](https://github.com/foca-js/foca/compare/v0.2.1...v0.2.2)&nbsp;&nbsp;(2021-11-22)

- meta 数据使用新的内部 store 存储

## [0.2.1](https://github.com/foca-js/foca/compare/v0.2.0...v0.2.1)&nbsp;&nbsp;(2021-11-22)

- 异步函数中的`metaId()`重命名为`meta()`

## [0.2.0](https://github.com/foca-js/foca/compare/v0.1.5...v0.2.0)&nbsp;&nbsp;(2021-11-21)

- 增加及时状态方法:`getLoading`, `getLoadings`, `getMeta`, `getMetas`
- 增加 hooks 方法:`useLoadings`, `useMetas`
- meta 增加 type 字段,并由此检测 loading 状态

## [0.1.5](https://github.com/foca-js/foca/compare/v0.1.4...v0.1.5)&nbsp;&nbsp;(2021-11-19)

- useModel 可以手动传入对比算法,未传则由框架动态决策
- 提升异步状态追踪性能
- 提升数据合并性能

## [0.1.4](https://github.com/foca-js/foca/compare/v0.1.3...v0.1.4)&nbsp;&nbsp;(2021-11-13)

- 删除 tslib 依赖
- 定义模型时的属性 state 重构为 initialState,防止和 actions 的 state 变量名重叠以及 eslint 规则报错。

## [0.1.3](https://github.com/foca-js/foca/compare/v0.1.2...v0.1.3)&nbsp;&nbsp;(2021-11-02)

- action 的返回类型更新为 AnyAction
- 内部方法 dispatch 现支持**直接**传入完整的新 state。如果你只想更新 state 的某个值,则仍然使用回调。
- 修改异步方法报错时 action.type 的文字

## [0.1.2](https://github.com/foca-js/foca/compare/v0.1.1...v0.1.2)&nbsp;&nbsp;(2021-11-01)

- 存储引擎可自定义 keyPrefix 参数

## [0.1.1](https://github.com/foca-js/foca/compare/v0.1.0...v0.1.1)&nbsp;&nbsp;(2021-10-31)

- 存储引擎放回当前库

## [0.1.0](https://github.com/foca-js/foca/compare)&nbsp;&nbsp;(2021-10-31)

- 模块化
- 持久化
- 支持类型提示
- 支持 Map/Set
- 支持 immer
- 与其他 redux 库共存,方便迁移


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021-2023 geekact

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# FOCA

流畅的 react 状态管理库,基于[redux](https://github.com/reduxjs/redux)和[react-redux](https://github.com/reduxjs/react-redux)。简洁、极致、高效。

[![npm peer react version](https://img.shields.io/npm/dependency-version/foca/peer/react?logo=react)](https://github.com/facebook/react)
[![npm peer typescript version](https://img.shields.io/npm/dependency-version/foca/peer/typescript?logo=typescript)](https://github.com/microsoft/TypeScript)
[![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)
[![Codecov](https://img.shields.io/codecov/c/github/foca-js/foca?logo=codecov)](https://codecov.io/gh/foca-js/foca)
[![npm](https://img.shields.io/npm/v/foca?logo=npm)](https://www.npmjs.com/package/foca)
[![npm](https://img.shields.io/npm/dt/foca?logo=codeforces)](https://www.npmjs.com/package/foca)
[![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/foca?label=bundle+size&cacheSeconds=3600&logo=esbuild)](https://bundlephobia.com/package/foca@latest)
[![License](https://img.shields.io/github/license/foca-js/foca?logo=open-source-initiative)](https://github.com/foca-js/foca/blob/master/LICENSE)
[![Code Style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier)](https://github.com/prettier/prettier)

<br>

![mind map](https://raw.githubusercontent.com/foca-js/foca/master/docs/mindMap.svg)

# 特性

- 模块化开发,导出即可使用
- 专注 TS 极致体验,超强的类型自动推导
- 内置 [immer](https://github.com/immerjs/immer) 响应式修改数据
- 支持计算属性,自动收集依赖,可传参数
- 自动管理异步函数的 loading 状态
- 可定制的多引擎数据持久化
- 支持局部模型,用完即扔
- 支持私有方法

# 使用环境

- Browser
- React Native
- Taro
- Electron

# 安装

```bash
# npm
npm install foca
# yarn
yarn add foca
# pnpm
pnpm add foca
```

# 初始化

```typescript
import { store } from 'foca';

store.init();
```

# 创建模型

### reducers 修改数据

```typescript
import { defineModel } from 'foca';

const initialState: { count: number } = { count: 0 };

export const counterModel = defineModel('counter', {
  initialState,
  reducers: {
    // 支持无限参数
    plus(state, value: number, times: number = 1) {
      state.count += value * times;
    },
    minus(state, value: number) {
      return { count: state.count - value };
    },
  },
});
```

### computed 计算属性

```typescript
export const counterModel = defineModel('counter', {
  initialState,
  // 自动收集依赖
  computed: {
    filled() {
      return Array(this.state.count)
        .fill('')
        .map((_, index) => index)
        .map((item) => item * 2);
    },
  },
});
```

### methods 组合逻辑

```typescript
export const counterModel = defineModel('counter', {
  initialState,
  reducers: {
    increment(state) {
      state.count += 1;
    },
  },
  methods: {
    async incrementAsync() {
      await this._sleep(100);

      this.increment();
      // 也可直接修改状态而不通过reducers,仅在内部使用
      this.setState({ count: this.state.count + 1 });
      this.setState((state) => {
        state.count += 1;
      });

      return 'OK';
    },
    // 私有方法,外部使用时不会提示该方法
    _sleep(duration: number) {
      return new Promise((resolve) => {
        setTimeout(resolve, duration);
      });
    },
  },
});
```

### events 事件回调

```typescript
export const counterModel = defineModel('counter', {
  initialState,
  events: {
    // 模型初始化
    onInit() {
      console.log(this.state);
    },
    // 模型数据变更
    onChange(prevState, nextState) {},
  },
});
```

# 使用

### 在 function 组件中使用

```tsx
import { FC, useEffect } from 'react';
import { useModel, useLoading } from 'foca';
import { counterModel } from './counterModel';

const App: FC = () => {
  const count = useModel(counterModel, (state) => state.count);
  const loading = useLoading(counterModel.incrementAsync);

  useEffect(() => {
    counterModel.incrementAsync();
  }, []);

  return (
    <div onClick={() => counterModel.plus(1)}>
      {count} {loading ? 'Loading...' : null}
    </div>
  );
};

export default App;
```

### 在 class 组件中使用

```tsx
import { Component } from 'react';
import { connect, getLoading } from 'foca';
import { counterModel } from './counterModel';

type Props = ReturnType<typeof mapStateToProps>;

class App extends Component<Props> {
  componentDidMount() {
    counterModel.incrementAsync();
  }

  render() {
    const { count, loading } = this.props;

    return (
      <div onClick={() => counterModel.plus(1)}>
        {count} {loading ? 'Loading...' : null}
      </div>
    );
  }
}

const mapStateToProps = () => {
  return {
    count: counterModel.state.count,
    loading: getLoading(counterModel.incrementAsync),
  };
};

export default connect(mapStateToProps)(App);
```

# 文档

https://foca.js.org

# 例子

沙盒在线试玩:https://codesandbox.io/s/foca-demos-e8rh3
<br />
React 案例仓库:https://github.com/foca-js/foca-demo-web
<br>
RN 案例仓库:https://github.com/foca-js/foca-demo-react-native
<br>
Taro 案例仓库:https://github.com/foca-js/foca-demo-taro

# 生态

#### 网络请求

| 仓库                                                    | 版本                                                                                            | 描述                          |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |
| [axios](https://github.com/axios/axios)                 | [![npm](https://img.shields.io/npm/v/axios)](https://www.npmjs.com/package/axios)               | 当下最流行的请求库            |
| [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++ 支持 节流、缓存、重试 |
| [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文档生成请求服务   |

#### 持久化存储引擎

| 仓库                                                                                      | 版本                                                                                                                                                      | 描述                                       | 平台     |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- |
| [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       |
| [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     |
| [localforage](https://github.com/localForage/localForage)                                 | [![npm](https://img.shields.io/npm/v/localforage)](https://www.npmjs.com/package/localforage)                                                             | 浏览器端持久化引擎,支持 IndexedDB, WebSQL | Web      |
| [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 |
| [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       |
| [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      |

#### 日志

| 仓库                                                                       | 版本                                                                                                                      | 描述           | 平台          |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- |
| [@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       |
| [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            |
| [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 |

# 常见疑问

### 函数里 this 的类型是 any

答:需要在文件 **tsconfig.json** 中开启`"strict": true`或者`"noImplicitThis": true`

---

更多答案请[查看文档](https://foca.js.org/#/troubleshooting)

# 捐赠

开源不易,升级维护框架和解决各种 issue 需要十分多的精力和时间。希望能得到你的支持,让项目处于良性发展的状态。捐赠地址:[二维码](https://foca.js.org#donate)

<table>
 <tr>
  <td align="center">
    <a target="_blank" href="https://github.com/arcsin1?donate_money=16.66">
      <img src="https://avatars.githubusercontent.com/u/13724222" width=80 height=80 style="border-radius: 100%;" />
    </a><br>
    <a target="_blank" href="https://github.com/arcsin1?donate_money=16.66">arcsin1</a>
  </td>
  <td align="center">
    <a target="_blank" href="https://github.com/xiongxliu?donate_money=50">
      <img src="https://avatars.githubusercontent.com/u/17661296" width=80 height=80 style="border-radius: 100%;" />
    </a><br>
    <a target="_blank" href="https://github.com/xiongxliu?donate_money=50">xiongxliu</a>
  </td>
 </tr>
</table>


================================================
FILE: docs/.nojekyll
================================================


================================================
FILE: docs/CNAME
================================================
foca.js.org

================================================
FILE: docs/_sidebar.md
================================================
* [关于Foca](/)

- [更新日志](/changelog.md)

* [开始使用](/initialize.md)

- [模型](/model.md)

* [接口](/api.md)

- [事件钩子](/events.md)

* [持久化](/persist.md)

- [进阶用法](/advanced.md)

* [问题解答](/troubleshooting.md)

- [编写测试](/test.md)

* [与Redux-Toolkit对比](/redux-toolkit.md)

- [捐赠](/donate.md)


================================================
FILE: docs/advanced.md
================================================
# <!-- {docsify-ignore} -->

# 克隆模型

虽然比较不常用,但有时候为了同一个页面的不同模块能独立使用模型数据,你就得需要复制这个模型,并把名字改掉。其实也不用这么麻烦,foca 给你来个惊喜:

```typescript
import { defineModel, cloneModel } from 'foca';

// 你打算用在各个普通页面里。
cosnt userModel = defineModel('users', { ... });

// 你打算用在通用的用户列表弹窗里。
const user1Model = cloneModel('users1', userModel);
// 你打算用在页头或页脚模块里。
const user2Model = cloneModel('users2', userModel);
```

共享方法但状态是独立的,这是个不错的主意,你只要维护一份代码就行了。

克隆时支持修改 `initialState, events, persist, skipRefresh` 这些属性

```typescript
const user3Model = cloneModel('users3', userModel, {
  initialState: {...},
  ...
});

const user3Model = cloneModel('users3', userModel, (prev) => {
  return {
    initialState: {
      ...prev.initialState,
      customData: 'xyz',
    },
    ...
  }
});
```

# 局部模型

通过`defineModel`和`cloneModel`创建的模型均为全局类别的模型,数据一直保持在内存中,直到应用关闭或者退出才会释放,对于比较大的项目,这可能会有性能问题。所以有时候你其实想要一种`用完就扔`的模型,即在 React 组件初始化时把模型数据扔到 store 中,当 React 组件被销毁时,模型的数据也跟着销毁。现在,局部模型很适合你的需求:

```diff
import { useEffect } from 'react';
import { defineModel, useIsolate } from 'foca';

// test.model.ts
export const testModel = defineModel('test', {
  initialState: { count: 0 },
  reducers: {
    plus(state, value: number) {
      state.count += value;
    },
  },
});

// App.tsx
const App: FC = () => {
+ const model = useIsolate(testModel);
  const { count } = useModel(model);

  useEffect(() => {
    model.plus(1);
  }, []);

  return <div>{count}</div>;
};
```

只需增加一行代码的工作量,利用 `useIsolate` 函数根据全局模型创建一个新的局部模型。局部模型拥有一份独立的状态数据,任何操作都不会影响到原来的全局模型,而且会随着组件一起 `挂载/销毁`,能有效降低内存占用。

另外,别忘了模型上还有两个对应的事件`onInit`和`onDestroy`可以使用

```typescript
export const testModel = defineModel('test', {
  initialState: { count: 0 },
  events: {
    onInit() {
      // 全局模型创建时触发
      // 局部模型随组件一起挂载时触发
    },
    onDestroy() {
      // 局部模型随组件一起销毁时触发
    },
  },
});
```

!> 如果不需要持久化,那么它可以完全代替克隆模型

# loadings

默认地,methods 函数只会保存一份执行状态,如果你在同一时间多次执行同一个函数,那么状态就会互相覆盖,产生错乱的数据。如果现在有 10 个按钮,点击每个按钮都会执行`model.methodX(id)`,那么我们如何知道是哪个按钮执行的呢?这时候我们需要为执行状态开辟一个独立的存储空间,让同一个函数拥有多个状态互不干扰。

```tsx
import { useLoading } from 'foca';

const App: FC = () => {
  const loadings = useLoading(model.myMethod.room);

  const handleClick = (id: number) => {
    model.myMethod.room(id).execute(id);
  };

  return (
    <div>
      <button onClick={() => handleClick(1)}>
        A {loadings.find(1) ? 'Loading...' : ''}
      </button>
      <button onClick={() => handleClick(2)}>
        B {loadings.find(2) ? 'Loading...' : ''}
      </button>
      <button onClick={() => handleClick(3)}>
        C {loadings.find(3) ? 'Loading...' : ''}
      </button>
    </div>
  );
};
```

这种场景也常出现在一些表格里,每一行通常都带有切换(switch UI)控件,点击后该控件需要被禁用或者出现 loading 图标,提前是你得知道是谁。

如果你能确定 find 的参数,那么也可以直接传递:

```typescript
// 适用于明确地知道编号的场景,比如是从组件props直接传入
const loading = useLoading(model.myMethod.room, 100); // boolean

// 适用于列表,编号只能在for循环中获取的场景
const loadings = useLoading(model.myMethod.room);
list.forEach(({ id }) => {
  const loading = loadings.find(id);
});
```

# 重置所有数据

当用户退出登录时,你需要清理与用户相关的一些数据,然后把页面切换到`登录页`。清理操作其实是比较麻烦的,首先 model 太多了,然后就是后期也可能再增加其它模型,不可能手动一个个清理。这时候可以用上 store 自带的方法:

```diff
import { store } from 'foca';

// onLogout是你的业务方法
onLogout().then(() => {
+ store.refresh();
});
```

一个方法就能把所有数据都恢复成初始值状态,太方便了吧?

重置时,你也可以保留部分模型的数据不被影响(可能是一些全局的配置数据),在相应的模型下加入关键词`skipRefresh`即可:

```diff
defineModel('my-global-model', {
  initialState: {},
+ skipRefresh: true,
});
```

对了,如果你实在是想无情地删除所有数据(即无视 skipRefresh 参数),那么就用`强制模式`好了:

```typescript
store.refresh(true);
```

# 私有方法

我们总是会想抽出一些逻辑作为独立的方法调用,但又不想暴露给模型外部使用,而且方法一多,调用方法时 TS 会提示长长的一串方法列表,显得十分混乱。是时候声明一些私有方法了,foca 使用约定俗成的`前置下划线(_)`来代表私有方法

```typescript
const userModel = defineModel('users', {
  initialState,
  reducers: {
    addUser(state, user: UserItem) {},
    _deleteUser(state, userId: number) {},
  },
  methods: {
    async retrieve(id: number) {
      const user = await http.get<UserItem>(`/users/${id}`);
      this.addUser(user);

      // 私有reducers方法
      this._deleteUser(15);
      // 私有methods方法
      this._myLogic();
      // 私有computed变量
      this._fullname.value;
    },
    async _myLogic() {},
  },
  computed: {
    _fullname() {},
  },
});

userModel.retrieve; // OK
userModel._deleteUser; // 报错了,找不到属性 _deleteUser
userModel._myLogic; // 报错了,找不到属性 _myLogic
userModel._fullname; // 报错了,找不到属性 _fullname
```

对外接口变得十分清爽,减少出错概率的同时,也提升了数据的安全性。


================================================
FILE: docs/api.md
================================================
# <!-- {docsify-ignore} -->

# useModel

使用频率::star2::star2::star2::star2::star2:

你绝对想不到在 React 组件中获取一个模型的数据有多简单,试试:

```tsx
// File: App.tsx
import { FC } from 'react';
import { useModel } from 'foca';
import { userModel } from './userModel';

const App: FC = () => {
  const users = useModel(userModel);

  return (
    <>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </>
  );
};

export default App;
```

就这么一小行,朴实无华,你也可以对数据进行改造,返回你当前需要的数据:

```typescript
const userIds = useModel(userModel, (state) => state.map((item) => item.id));
```

不错,你拿到了所有用户的 id 编号,同时 userIds 的类型会自动推断为`number[]`。

!> 只要 useModel() 返回的最终值不变,就不会触发 react 组件刷新。foca 使用 **深对比** 来判断值是否变化。

---

等等,事情还没结束!useModel 还能增加更多模型上去,这样可以少用几个 hooks:

```typescript
const { users, agents, teachers } = useModel(
  userModel,
  agentModel,
  teacherModel,
);
```

传递超过一个模型作为参数时,返回值将变成对象,而 key 就是模型的名称,value 就是模型的 state。这很酷,而且你仍然不用担心类型的问题。

如果你有一些数据需要多个模型才能计算出来,那么现在就是 useModel 大展身手的时候了:

```typescript
const count = useModel(
  userModel,
  agentModel,
  teacherModel,
  (users, agents, teachers) => users.length + agents.length + teachers.length,
);
```

返回的是一个数字,如假包换,TS 也自动推导出来了这是`number`类型

# useLoading

使用频率::star2::star2::star2::star2::star2:

methods 函数大部分是异步的,你可能正在函数里执行一个请求 api 的操作。在用户等待期间,你需要为用户渲染`loading...`之类的字样或者图标以缓解用户的焦虑心情。利用 foca 提供的逻辑,你可以轻松地知道某个函数是否正在执行:

```tsx
import { useLoading } from 'foca';

const App: FC = () => {
  const loading = useLoading(userModel.getUser);

  const handleClick = () => {
    userModel.getUser(1);
  };

  return <div onClick={handleClick}>{loading ? 'loading...' : 'OK'}</div>;
};
```

每次开始执行`getUser`函数,loading 自动变成`true`,从而触发组件刷新。

在某些编辑表单的场景,很可能会同时有新增和修改两种操作。对于 restful API,你需要写两个异步函数来处理请求。但你的表单保存按钮只有一个,很显然不管是新增还是修改,你都想让保存按钮渲染`保存中...`的字样。

为了减少业务代码,useLoading 允许你传入多个异步函数,只要有任何一个函数在执行,那么最终值就会是 true。

```typescript
const loading = useLoading(userModel.create, userModel.update, ...);
```

# useComputed

使用频率::star2::star2::star2:

配合 computed 计算属性使用。携带参数的情况下,则从第二个参数开始依次传入

```tsx
import { useComputed } from 'foca';

// 假设有这么一个model
const userModel = defineModel('user', {
  initialState: {
    firstName: 'tick',
    lastName: 'tock',
  },
  computed: {
    fullName() {
      return this.state.firstName + '.' + this.state.lastName;
    },
    profile(age: number) {
      return this.fullName() + '-' + age;
    },
  },
});

const App: FC = () => {
  // 只有当 firstName 或者 lastName 变化,才会重新刷新该组件
  const fullName = useComputed(userModel.fullName);
  // 这里会有TS提示你应该传几个参数,以及参数类型
  const profile = useComputed(userModel.profile, 24);

  return <div>{fullName}</div>;
};
```

# getLoading

使用频率::star2:

如果想实时获取异步函数的执行状态,则可以通过 `getLoading(...)` 的方式获取。它与 **useLoading** 的唯一区别就是是否hooks。

```typescript
const loading = getLoading(userModel.create);
```

# connect

使用频率::star2:

如果你写烦了函数式组件,偶尔想写一下 class 组件,那么 foca 已经为你准备好了`connect()`函数。如果不知道这是什么,可以参考[react-redux](https://github.com/reduxjs/react-redux)的文档。事实上,我们内置了这个库并对其做了一些封装。

```typescript
import { PureComponent } from 'react';
import { connect } from 'foca';
import { userModel } from './userModel';

type Props = ReturnType<typeof mapStateToProps>;

class App extends PureComponent<Props> {
  render() {
    const { users, loading } = this.props;

    if (loading) {
      return <p>Loading...</p>;
    }

    return <p>Hello, {users.length} people</p>;
  }
}

const mapStateToProps = () => {
  return {
    users: userModel.state,
    loading: getLoading(userModel.fetchUser),
  };
};

export default connect(mapStateToProps)(App);
```

没有了 hooks 的帮忙,我们只能从模型或者方法上获取实时的数据。但只要你是在 mapStateToProps 中获取的数据,foca 就会自动为你更新并注入到组件里。


================================================
FILE: docs/changelog.md
================================================
# 更新日志 <!-- {docsify-ignore-all} -->

[changelog](https://raw.githubusercontent.com/foca-js/foca/master/CHANGELOG.md ':include')


================================================
FILE: docs/donate.md
================================================
# 捐赠 <!-- {docsify-ignore} -->

开源不易,升级维护框架和解决各种 issue 需要十分多的精力和时间。希望能得到你的支持,让项目处于良性发展的状态。

> 捐赠时请备注你的 github 账号,对于每一个捐赠者,我都会放到 README.md 以及当前页表示感谢。

### 二维码

<center>
<img src="./alipay.png" width="40%" />
<span style="display:inline-block; width: 8%"></span>
<img src="./wepay.png" width="40%" />
</center>

### 鸣谢

<!-- 匿名捐赠共 `10` 元 -->

<table>
 <tr>
  <td align="center">
    <a target="_blank" href="https://github.com/arcsin1?donate_money=16.66">
      <img src="https://avatars.githubusercontent.com/u/13724222" width=80 height=80 style="border-radius: 100%;" />
    </a><br>
    <a target="_blank" href="https://github.com/arcsin1?donate_money=16.66">arcsin1</a>
  </td>
  <td align="center">
    <a target="_blank" href="https://github.com/xiongxliu?donate_money=50">
      <img src="https://avatars.githubusercontent.com/u/17661296" width=80 height=80 style="border-radius: 100%;" />
    </a><br>
    <a target="_blank" href="https://github.com/xiongxliu?donate_money=50">xiongxliu</a>
  </td>
 </tr>
</table>


================================================
FILE: docs/events.md
================================================
每个模型都有针对自身的事件回调,在某些复杂的业务场景下,事件和其它属性的组合将变得十分灵活。

## onInit

当 store 初始化完成 并且持久化(如果有)数据已经恢复时,onInit 就会被自动触发。你可以调用 methods 或者 reducers 做一些额外操作。

```typescript
import { defineModel } from 'foca';

// 如果是持久化的模型,则初始值不一定是0
const initialState = { count: 0 };

export const myModel = defineModel('my', {
  initialState,
  reducers: {
    add(state, step: number) {
      state.count += step;
    },
  },
  methods: {
    async requestApi() {
      const result = await http.get('/path/to');
      // ...
    },
  },
  events: {
    onInit() {
      this.add(10);
      this.requestApi();
    },
  },
});
```

## onChange

每当 state 有变化时的回调通知。初始化(onInit)执行之前不会触发该回调。如果在 onInit 中做了修改 state 的操作,则会触发该回调。

```typescript
import { defineModel } from 'foca';

const initialState = { count: 0 };

export const testModel = defineModel('test', {
  initialState,
  reducers: {
    add(state, step: number) {
      state.count += step;
    },
  },
  methods: {
    _notify() {
      // do something
    },
  },
  events: {
    onChange(prevState, nextState) {
      if (prevState.count !== nextState.count) {
        // 达到watch的效果
        this._notify();
      }
    },
  },
});
```

## onDestroy

模型数据从 store 卸载时的回调通知。onDestroy 事件只针对[局部模型](/advanced?id=局部模型),即通过`useIsolate`这个 hooks api 创建的模型才会触发,因为局部模型是跟随组件一起创建和销毁的。

注意,当触发 onDestroy 回调时,模型已经被卸载了,所以无法再拿到当前数据,而且`this`上下文也被限制使用了。

```typescript
import { defineModel } from 'foca';

export const testModel = defineModel('test', {
  initialState: { count: 0 },
  events: {
    onDestroy(modelName) {
      console.log('Destroyed', modelName);
    },
  },
});
```


================================================
FILE: docs/home.md
================================================
# FOCA

流畅的 react 状态管理库,基于[redux](https://github.com/reduxjs/redux)和[react-redux](https://github.com/reduxjs/react-redux)。简洁、极致、高效。

[![npm peer react version](https://img.shields.io/npm/dependency-version/foca/peer/react?logo=react)](https://github.com/facebook/react)
[![npm peer typescript version](https://img.shields.io/npm/dependency-version/foca/peer/typescript?logo=typescript)](https://github.com/microsoft/TypeScript)
[![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)
[![Codecov](https://img.shields.io/codecov/c/github/foca-js/foca?logo=codecov)](https://codecov.io/gh/foca-js/foca)
[![npm](https://img.shields.io/npm/v/foca?logo=npm)](https://www.npmjs.com/package/foca)
[![npm](https://img.shields.io/npm/dt/foca?logo=codeforces)](https://www.npmjs.com/package/foca)
[![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/foca?label=bundle+size&cacheSeconds=3600&logo=esbuild)](https://bundlephobia.com/package/foca@latest)
[![License](https://img.shields.io/github/license/foca-js/foca?logo=open-source-initiative)](https://github.com/foca-js/foca/blob/master/LICENSE)
[![Code Style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?logo=prettier)](https://github.com/prettier/prettier)

<br>

![mind map](./mindMap.svg)

# 使用环境

- Browser
- React Native
- Taro
- Electron

# 特性

#### 模块化开发,导出即可使用

一个模型包含了状态操作的所有方法,基础代码全部剥离,不再像原生 redux 一般拆分成 action/type/reducer 三个文件,既不利于管理,又难以在提示上关联起来。

定义完模型,导出即可在组件中使用,不用再怕忘记注册或者嫌麻烦了。

#### 专注 TS 极致体验,超强的类型自动推导

无 TS 不编程,foca 提供 **100%** 的基础类型提示,强大的自动推导能力让你产生一种提示上瘾的快感,而你只需关注业务中的类型。

#### 内置 [immer](https://github.com/immerjs/immer) 响应式修改数据

可以说加入 immer 是非常有必要的,当 reducer 数据多层嵌套时,你不必再忍受更改里层的数据而不断使用 rest/spread(...)扩展符的烦恼,相反地,直接赋值就好了,其他的交给 immer 搞定。

#### 支持计算属性,自动收集依赖,可传参数

现在,redux 家族不需要再羡慕`vue`或者`mobx`等响应式框架,咱也能支持计算属性并且自动收集依赖,而且是时候把[reselect](https://github.com/reduxjs/reselect)**扔进垃圾桶**了。

#### 自动管理异步函数的 loading 状态

我们总是想知道某个异步方法(或者请求)正在执行,然后在页面上渲染出`loading...`字样,幸运地是框架自动(按需)为你记录了执行状态。

#### 可定制的多引擎数据持久化

某些数据在一个时间段内可能是不变的,比如登录凭证 token。所以你想着先把数据存到本地,下次自动恢复到模型中,这样用户就不需要频繁登录了。

#### 支持局部模型,用完即扔

利用全局模型派生出局部模型,并跟随组件`挂载/卸载`,状态与外界隔离,组件卸载后状态自动删除,严格控制内存使用量

#### 支持私有方法

一个前置下划线(`_`)就能让方法变成私有的,外部使用时 TS 不会提示私有方法和私有变量,简单好记又省心。

# 生态

#### 网络请求

| 仓库                                                    | 版本                                                                                            | 描述                          |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------- |
| [axios](https://github.com/axios/axios)                 | [![npm](https://img.shields.io/npm/v/axios)](https://www.npmjs.com/package/axios)               | 当下最流行的请求库            |
| [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++ 支持 节流、缓存、重试 |
| [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文档生成请求服务   |

#### 持久化存储引擎

| 仓库                                                                                      | 版本                                                                                                                                                      | 描述                                       | 平台     |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- |
| [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       |
| [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     |
| [localforage](https://github.com/localForage/localForage)                                 | [![npm](https://img.shields.io/npm/v/localforage)](https://www.npmjs.com/package/localforage)                                                             | 浏览器端持久化引擎,支持 IndexedDB, WebSQL | Web      |
| [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 |
| [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       |
| [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      |

| 仓库                                                                       | 版本                                                                                                                      | 描述           | 平台          |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- |
| [@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       |
| [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            |
| [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 |

# 缺陷

- [不支持 SSR](/troubleshooting?id=为什么不支持-ssr)

# 例子

React 案例仓库:https://github.com/foca-js/foca-demo-web
<br>
RN 案例仓库:https://github.com/foca-js/foca-demo-react-native
<br>
Taro 案例仓库:https://github.com/foca-js/foca-demo-taro
<br>

# 在线试玩

<iframe src="https://codesandbox.io/embed/foca-demos-e8rh3?fontsize=14&hidenavigation=1&theme=dark&view=preview"
     style="width:100%; height:600px; border:0; border-radius: 4px; overflow:hidden;"
     title="foca-demos"
     allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
     sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
   ></iframe>


================================================
FILE: docs/index.html
================================================
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Foca</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="description" content="现代化react状态管理库" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, minimum-scale=1.0"
    />
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
    <link
      rel="stylesheet"
      href="//fastly.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"
    />

    <style type="text/css">
      h1[id='']:first-of-type {
        display: none;
      }

      span.token.deleted {
        color: #ffdedb;
        background-color: #cc1421;
        text-decoration: none;
        padding-bottom: 2px;
      }

      span.token.inserted {
        color: #acf7b6;
        background-color: #007728;
        border-bottom: 0;
        padding-bottom: 2px;
      }

      img.emoji {
        vertical-align: text-top;
      }

      .sidebar {
        width: 260px;
      }

      section.content {
        left: 260px;
      }

      .markdown-section {
        max-width: 100%;
        width: 100%;
        box-sizing: border-box;
      }

      .markdown-section tr {
        background: transparent !important;
      }

      .markdown-section table pre {
        padding: 0 0.5em;
      }

      .markdown-section table pre > code {
        padding: 1.5em 0;
      }

      .markdown-section table thead {
        background-color: #eee;
      }
    </style>
  </head>
  <body>
    <div id="app">加载中...</div>
    <script>
      window.$docsify = {
        name: 'Foca',
        repo: 'https://github.com/foca-js/foca',
        loadSidebar: true,
        subMaxLevel: 2,
        auto2top: true,
        homepage: 'home.md',
        autoHeader: true,
        topMargin: 30,
        search: {
          placeholder: '搜索',
          noData: '未找到相关信息',
          hideOtherSidebarContent: true,
        },
      };
    </script>
    <!-- Docsify v4 -->
    <script src="//fastly.jsdelivr.net/npm/docsify@4"></script>
    <script src="//fastly.jsdelivr.net/npm/docsify@4/lib/plugins/emoji.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-typescript.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-jsx.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-diff.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-tsx.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/prismjs@1/components/prism-json5.min.js"></script>
    <script src="//fastly.jsdelivr.net/npm/docsify-tabs"></script>
  </body>
</html>


================================================
FILE: docs/initialize.md
================================================
# <!-- {docsify-ignore} -->

# 安装

```bash
# npm
npm install foca
# yarn
yarn add foca
# pnpm
pnpm add foca
```

# 激活

foca 遵循`唯一store`原则,并提供了快速初始化的入口。

```typescript
// File: store.ts
import { store } from 'foca';

store.init();
```

好吧,就是这么简单!

# 导入

与原生 react-redux 类似,你需要把 foca 提供的 Provider 组件放置到入口文件,这样才能在业务组件中获取到数据。

<!-- tabs:start -->

#### ** React **

```diff
+ import './store';
+ import { FocaProvider } from 'foca';
import ReactDOM from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);

root.render(
+ <FocaProvider>
    <App />
+ </FocaProvider>
);
```

#### ** React-Native **

```diff
+ import './store';
+ import { FocaProvider } from 'foca';
import { Text, View } from 'react-native';

export default function App() {
  return (
+   <FocaProvider>
      <View>
        <Text>Hello World</Text>
      </View>
+   </FocaProvider>
  )
}
```

#### ** Taro.js **

```diff
+ import './store';
+ import { FocaProvider } from 'foca';
import { Component } from 'react';

export default class App extends Component {
  render() {
-   return this.props.children;
+   return <FocaProvider>{this.props.children}</FocaProvider>;
  }
}
```

<!-- tabs:end -->

# 日志

在开发阶段,如果你想实时查看状态的操作过程以及数据的变化细节,那么开启可视化界面是必不可少的一个环节。

<!-- tabs:start -->

#### ** 全局软件 **

**优势:** 一次安装,所有项目都可以无缝使用。

- 对于 Web 项目,可以安装 Chrome 浏览器的 [redux-devtools](https://github.com/reduxjs/redux-devtools) 扩展,然后打开控制台查看。
- 对于 React-Native 项目,可以安装并启动软件 [react-native-debugger](https://github.com/jhen0409/react-native-debugger),然后点击 App 里的按钮 `Debug with Chrome`即可连接软件,其本质也是 Chrome 的控制台

接着,我们在 store 里注入增强函数:

```typescript
store.init({
  // 字符串 redux-devtools 即 window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 的缩写
  // 设置 redux-devtools 在生产环境(process.env.NODE_ENV === 'production')下会自动关闭
  // 你也可以安装等效的插件包 @redux-devtools/extension 自由控制
  compose: 'redux-devtools',
});
```

compose 也支持回调形式,目的是为了注入更多插件。

```typescript
import { composeWithDevTools as compose } from '@redux-devtools/extension';
// 或者使用原生的compose
// import { compose } from 'foca';

store.init({
  compose(enhancer) {
    return compose(enhancer, ...more[]);
  },
});
```

#### ** 项目插件 **

**优势:** 可选配置参数多,且在 Web 和 React-Native 中都能使用。

```bash
# npm
npm install redux-logger @types/redux-logger --save-dev
# yarn
yarn add redux-logger @types/redux-logger --dev
# pnpm
pnpm add redux-logger @types/redux-logger -D
```

接着我们把这个包注入 store:

```typescript
import { store, Middleware } from 'foca';
import { createLogger } from 'redux-logger';

const middleware: Middleware[] = [];

if (process.env.NODE_ENV !== 'production') {
  middleware.push(
    createLogger({
      collapsed: true,
      diff: true,
      duration: true,
      logErrors: true,
    }),
  );
}

store.init({
  middleware,
});
```

大功告成,下次你对 store 的数据做操作时,控制台就会有相应的通知输出。

<!-- tabs:end -->

# 开发热更

<small>如果是 React-Native,你可以跳过这一节。</small>

因为 store.ts 需要被入口文件引入,而 store.ts 又引入了部分 model(<small>持久化需要这么做</small>),所以如果相应的 model 做了修改操作时,会导致浏览器页面全量刷新而非热更新。如果你正在使用当前流行的打包工具,强烈建议加上`hot.accept`手动处理模块更新。

<!-- tabs:start -->

#### ** Vite **

```typescript
// File: store.ts

store.init(...);

// https://cn.vitejs.dev/guide/api-hmr.html#hot-acceptcb
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    console.log('Hot updated: store');
  });
}
```

#### ** Webpack **

```typescript
// File: store.ts

// ##################################################
// ######                                     #######
// ###### yarn add @types/webpack-env --dev   #######
// ######                                     #######
// ##################################################

store.init(...);

// https://webpack.docschina.org/api/hot-module-replacement/
if (module.hot) {
  module.hot.accept(() => {
    console.log('Hot updated: store');
  });
}
```

#### ** Webpack ESM **

```typescript
// File: store.ts

// ##################################################
// ######                                     #######
// ###### yarn add @types/webpack-env --dev   #######
// ######                                     #######
// ##################################################

store.init(...);

// https://webpack.docschina.org/api/hot-module-replacement/
if (import.meta.webpackHot) {
  import.meta.webpackHot.accept(() => {
    console.log('Hot updated: store');
  });
}
```

<!-- tabs:end -->


================================================
FILE: docs/model.md
================================================
# <!-- {docsify-ignore} -->

# Model

原生的 redux 由 action/type/reducer 三个部分组成,大多数情况我们会分成 3 个文件分别存储。在实际使用中,这种模板式的书写方式不仅繁琐,而且难以将他们关联起来,类型提示就更麻烦了。

基于此,我们提出了模型概念,以 state 为核心,任何更改 state 的操作都应该放在一起。

```typescript
// models/user.model.ts
import { defineModel } from 'foca';

export interface UserItem {
  id: number;
  name: string;
  age: number;
}

const initialState: UserItem[] = [];

export const userModel = defineModel('users', {
  initialState,
});
```

你已`defineModel`经定义了一个最基础的模型,其中第一个字符串参数为redux中的`唯一标识`,请确保其它模型不会再使用这个名字。

对了,怎么注册到store?躺着别动!foca 已经自动把模型注册到 store 中心,也让你享受一下 **DRY** <small>(Don't Repeat Yourself)</small> 原则,因此在业务文件内直接导入模型就能使用。

# State

foca 基于 redux 深度定制,所以 state 必须是个纯对象或者数组。

```typescript
// 对象
const initialState: { [K: string]: string } = {};
const objModel = defineModel('model-object', {
  initialState,
});

// 数组
const initialState: number[] = [];
const arrayModel = defineModel('model-array', {
  initialState,
});
```

# Reducers

模型光有 state 也不行,你现在拿到的就是一个空数组([])。加点数据上去吧,这时候就要用到 reducers:

```typescript
export const userModel = defineModel('users', {
  initialState,
  reducers: {
    addUser(state, user: UserItem) {
      state.push(user);
    },
    updateName(state, id: number, name: string) {
      const user = state.find((item) => item.id === id);
      if (user) {
        user.name = name;
      }
    },
    removeUser(state, id: number) {
      const index = state.findIndex((item) => item.id === id);
      if (index >= 0) {
        state.splice(index, 1);
      }
    },
    clear() {
      // 返回初始值
      return this.initialState;
    },
  },
});
```

就这么干,你已经赋予了模型生命,你等下可以和它互动了。现在,我们来说说这些 reducers 需要注意的几个点:

- 函数的第一个参数一定是 state ,而且它是能自动识别到类型`State`的,你不用刻意地去指定。
- 函数是可以带多个参数的,这全凭你自己的喜好。
- 函数体内可以直接修改 state 对象(数组也属于对象)里的任何内容,这得益于 [immer](https://github.com/immerjs/immer) 的功劳。
- 函数返回值必须是`State`类型。当然你也可以不返回,这时 foca 会认为你正在直接修改 state。
- 如果你想使用`this`上下文,比如上面的 **clear()** 函数返回了初始值,那么请~~不要使用箭头函数~~。

# Methods

不可否认,你的数据不可能总是凭空捏造,在真实的业务场景中,数据总是通过接口获得,然后保存到 state 中。foca 贴心地为你准备了组合逻辑的函数,快来试试吧:

```typescript
const userModel = defineModel('users', {
  initialState,
  methods: {
    async get() {
      const users = await http.get<UserItem[]>('/users');
      this.setState(users);
      return users;
    },
    async retrieve(id: number) {
      const user = await http.get<UserItem>(`/users/${id}`);
      this.setState((state) => {
        state.push(user);
      });
    },
    // 也可以是非异步的普通函数
    findUser(id: number) {
      return this.state.users.find((user) => user.id === id);
    },
  },
});
```

瞧见没,你可以在 methods 里自由地使用 async/await 方案,然后通过上下文`this.setState`快速更新 state。

接下来我们说说`setState`,这其实完全就是 reducers 的快捷方式,你可以直接传入数据或者使用匿名函数来操作,十分方便。这不禁让我们想起了 React Component 里的 setState?咳咳~~读书人的事,那能叫抄吗?

<!-- tabs:start -->

#### ** 直接修改 **

依赖 immer 的能力,你可以直接修改回调函数给的 state 参数,这也是框架最推荐的方式

```typescript
this.setState((state) => {
  state.b = 2;
});

this.setState((state) => {
  state.push('a');
  state.shift();
});
```

#### ** 部分更新 **

是的,你可以返回一部分数据,而且这个特性很简洁高效,框架会使用`Object.assign`帮你把剩余的属性加回去。

!> 只针对 object 类型,而且只有第一级属性可以缺省(参考 React Class Component)

```typescript
this.setState({ a: 1 });

this.setState((state) => {
  return { a: 1 }; // <==> state.a = 1;
});
```

#### ** 全量更新 **

就是重新设置所有数据

```typescript
this.setState({ a: 1, b: 2 });
this.setState((state) => {
  return { a: 1, b: 2 };
});

this.setState(['a', 'b', 'c']);
this.setState((state) => {
  return ['a', 'b', 'c'];
});

// 重新设置成初始值
this.setState(this.initialState);
```

<!-- tabs:end -->

嗯?你压根就不想用`setState`,你觉得这样看起来很混乱?Hold on,你突然想起可以使用 reducers 去改变 state 不是吗?

```typescript
const userModel = defineModel('users', {
  initialState,
  reducers: {
    addUser(state, user: UserItem) {
      state.push(user);
    },
  },
  methods: {
    async retrieve(id: number) {
      const user = await http.get<UserItem>(`/users/${id}`);
      // 调用reducers里的函数
      this.addUser(user);
    },
  },
});
```

好吧,这样看起来更纯粹一些,代价就是要委屈你多写几行代码了。

# Computed

对于一些数据,其实是需要经过比较冗长的拼接或者复杂的计算才能得出结果,同时你想自动缓存这些结果?来吧,展示:

```typescript
const initialState = {
  firstName: 'tick',
  lastName: 'tock',
  country: 0,
};

const userModel = defineModel('users', {
  initialState,
  computed: {
    fullName() {
      return this.state.firstName + '.' + this.state.lastName;
    },
    profile(age: number, address?: string) {
      return this.fullName() + age + (address || 'empty');
    },
  },
});
```

恕我直言,有点 Methods 的味道了。味道是这个味道,但是本质不一样,当我们多次执行computed函数时,因为存在缓存的概念,所以不会真正地执行该函数。

```typescript
userModel.fullName(); // 执行函数,生成缓存
userModel.fullName(); // 使用缓存
userModel.fullName(); // 使用缓存
```

带参数的计算属性可以理解为所有参数就是一个key,每个key都会生成一个计算属性实例,互不干扰。

```typescript
userModel.profile(20); // 执行函数,生成实例1缓存
userModel.profile(20); // 实例1缓存
userModel.profile(123); // 执行函数,生成实例2缓存
userModel.profile(123); // 实例2缓存

userModel.profile(20); // 实例1缓存
userModel.profile(123); // 实例2缓存
```

参数尽量使用基本类型,**不建议**使用对象或者数组作为计算属性的实参,因为如果每次都传新建的复合类型,无法起到缓存的效果,执行速度反而变慢,这和`useMemo(callback, deps)`函数的第二个参数(依赖项)是一个原理。如果实在想用复合类型作为参数,不烦考虑一下放到`Methods`里?

---

缓存什么时候才会更新?框架自动收集依赖,只有其中某个依赖更新了,计算属性才会更新。上面的例子中,当`firstName`或者`lastName`有变化时,fullName 将被标记为`dirty`状态,下一次访问则会重新计算结果。而当`country`变化时,不影响 fullName 的结果,下一次访问仍使用缓存作为结果。

!> 可以在 computed 中使用其它 model 的数据。


================================================
FILE: docs/persist.md
================================================
# 持久化

持久化是自动把数据通过引擎存储到某个空间的过程。

如果你的某个 api 数据常年不变,那么建议你把它扔到本地做个缓存,这样用户下次再访问你的页面时,可以第一时间看到缓存的内容。如果你不想让用户每次刷新页面就重新登录,那么持久化很适合你。

# 入口

你需要在初始化 store 时开启持久化

```typescript
// File: store.ts
import { store } from 'foca';
import { userModel } from './userModel';
import { agentModel } from './agentModel';

store.init({
  persist: [
    {
      key: '$PROJECT_$ENV',
      version: 1,
      engine: localStorage,
      // 模型白名单列表
      models: [userModel, agentModel],
    },
  ],
});
```

把需要持久化的模型扔进去,foca 就能自动帮你存取数据。

`key`即为存储路径,最好采用**项目名-环境名**的形式组织。纯前端项目如果和其他前端项目共用一个域名,或者在同一域名下,则有可能使用共同的存储空间,因此需要保证`key`是唯一的值。

# 存储引擎

不同的引擎会把数据存储到不同的位置,使用哪个引擎取决于项目跑在什么环境。引擎操作可以是同步的也可以是异步的。下面列举的第三方库也可以**直接当作**存储引擎:

- window.localStorage - 浏览器自带
- window.sessionStorage - 浏览器自带
- [localforage](https://www.npmjs.com/package/localforage) (IndexedDB, WebSQL) - 浏览器专用
- [@react-native-async-storage/async-storage](https://www.npmjs.com/package/@react-native-async-storage/async-storage) - React-Native专用
- [foca-mmkv-storage](https://github.com/foca-js/foca-mmkv-storage) - React-Native专用
- [foca-taro-storage](https://github.com/foca-js/foca-taro-storage) - Taro专用
- [foca-electron-storage](https://github.com/foca-js/foca-taro-storage) - Electron专用
- [foca-cookie-storage](https://github.com/foca-js/foca-cookie-storage) - 浏览器专用,存储到cookie

如果有必要,你也可以自己实现一个引擎:

```typescript
// import { StorageEngine } from 'foca';

interface StorageEngine {
  getItem(key: string): string | null | Promise<string | null>;
  setItem(key: string, value: string): any;
  removeItem(key: string): any;
  clear(): any;
}
```

如果是在测试环境,也可以尝试使用内置的内存引擎

```typescript
import { memoryStorage } from 'foca';
```

# 设置版本号

当数据结构变化,我们不得不升级版本号来`删除`持久化数据,版本号又分为`全局版本`和`模型版本`两种。当修改模型内版本号时,仅删除该模型的持久化数据,而修改全局版本号时,白名单内所有模型的持久化数据都被删除。

建议优先修改模型内版本号!!

```diff
const stockModel = defineModel('stock', {
  initialState: {},
  persist: {
    // 模型版本号,影响当前模型
+   version: '2.0',
  },
});

store.init({
  persist: [
    {
      key: '$PROJECT_normal_$ENV',
      // 全局版本号,影响白名单全部模型
+     version: '3.6',
      engine: engines.localStorage,
      models: [musicModel, stockModel],
    },
  ],
});
```

# 数据合并

> v3.0.0

在项目的推进过程中,难免需要根据产品需求更新模型数据结构,结构变化后,我们可以简单粗暴地通过`版本号+1`的方式来删除持久化的数据。但如果只是新增了某一个字段,我们希望持久化恢复时能自动识别。试试推荐的`合并模式`吧:

```diff
store.init({
  persist: [
    {
      key: 'myproject-a-prod',
      version: 1,
+     merge: 'merge',
      engine: engines.localStorage,
      models: [userModel],
    },
  ],
});
```

很轻松就设置上了,合并模式目前有3种可选的类型:

- `replace` - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据
- `merge` - 合并模式(默认)。数据从存储引擎取出后,与初始数据多余部分进行合并,可以理解为 **Object.assign()** 操作
- `deep-merge` - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作

如果某个模型比较特殊,我们也可以在里面单独设置合并模式。

```diff
const userModel = defineModel('user', {
  initialState: {},
  persist: {
+   merge: 'deep-merge',
  },
});
```

接下来看看它的具体表现:

```typescript
const persistState = { obj: { test1: 'persist' } };
const initialState = { obj: { test2: 'initial' }, foo: 'bar' };

// replace 效果
const state = { obj: { test1: 'persist' } };
// merge 效果
const state = { obj: { test1: 'persist' }, foo: 'bar' };
// deep-merge 效果
const state = { obj: { test1: 'persist', test2: 'initial' }, foo: 'bar' };
```

需要注意的是合并模式对`数组无效`,当持久化数据和初始数据都为数组类型时,会强制使用持久化数据。当持久化数据和初始数据任何一边为数组类型时,会强制使用初始化数据。

```typescript
const persistState = [1, 2, 3];  ✅
const initialState = [4, 5, 6, 7];  ❌
// 合并效果
const state = [1, 2, 3];

// -------------------------

const persistState = [1, 2, 3];  ❌
const initialState = { foo: 'bar' };  ✅
// 合并效果
const state = { foo: 'bar' };

// -------------------------

const persistState = { foo: 'bar' };  ❌
const initialState = [1, 2, 3];  ✅
// 合并效果
const state = [1, 2, 3];
```

# 系列化钩子

> v3.0.0

数据在模型与持久化引擎互相转换期间,我们希望对它进行一些额外操作以满足业务需求。比如:

- 只缓存部分字段,避免存储尺寸超过存储空间限制
- 改变数据结构或者内容
- 更新时间等动态信息

foca提供了一对实用的过滤函数`dump`和`load`。**dump** 即 model->persist,**load** 即 persist->model。

```typescript
const model = defineModel('model', {
  initialState: {
    mode: 'foo', // 本地的设置,需要持久化缓存
    hugeDate: [], // API请求数据,数据量太大
  },
  persist: {
    // 系列化
    dump(state) {
      return state.mode;
    },
    // 反系列化
    load(mode) {
      return { ...this.initialState, mode };
    },
  },
});
```

# 分组

我们注意到 persist 其实是个数组,这意味着你可以多填几组配置上去,把不同的模型数据存储到不同的地方。这看起来很酷,但我猜你不一定需要!

```typescript
import { store, engines } from 'foca';

store.init({
  persist: [
    {
      key: 'myproject-a-prod',
      version: 1,
      engine: engines.localStorage,
      models: [userModel],
    },
    {
      key: 'myproject-b-prod',
      version: 5,
      engine: engines.sessionStorage,
      models: [agentModel, teacherModel],
    },
    {
      key: 'myproject-vip-prod',
      version: 1,
      engine: engines.localStorage,
      models: [customModel, otherModel],
    },
  ],
});
```


================================================
FILE: docs/redux-toolkit.md
================================================
<table>
<thead>
<tr>
<th>foca</th>
<th>toolkit</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2">
开源时间
</th>
</tr>
<tr>
<td>2021-10</td>
<td>2018-03</td>
</tr>
<tr>
<th colspan="2">
文档地址
</th>
</tr>
<tr>
<td valign="top">

[foca.js.org](https://foca.js.org)(中文文档)

</td>
<td>

[redux-toolkit.js.org](https://redux-toolkit.js.org/)(English documentation)

</td>
</tr>
<tr>
<th colspan="2">
安装
</th>
</tr>
<tr>
<td  valign="top">

```bash
pnpm add foca
```

</td>
<td>

```bash
pnpm add @reduxjs/toolkit react-redux
# 持久化
# pnpm add redux-persist
# 计算属性
# pnpm add reselect
```

</td>
</tr>
<tr>
<th colspan="2">
初始化
</th>
</tr>
<tr>
<td valign="top">

```typescript
// store.ts
import { store } from 'foca';

foca.init({});
```

</td>
<td>

```typescript
// store.ts
import { useDispatch, useSelector } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    // 项目中所有reducer都要import注册到这里(枯燥)
    counter: counterReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
```

</td>
</tr>
<tr>
<th colspan="2">
注入React
</th>
</tr>
<tr>
<td valign="top">

```typescript
import './store';
import { FocaProvider } from 'foca';

ReactDOM.render(
  <FocaProvider>
    <App />
  </FocaProvider>,
);
```

</td>
<td>

```typescript
import { store } from './store';
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
);
```

</td>
</tr>
<tr>
<th colspan="2">
创建Reducer
</th>
</tr>
<tr>
<td valign="top">

```typescript
// counter.model.ts
import { defineModel } from 'foca';

const initialState: { value: number } = {
  value: 0,
};

export const counterModel = defineModel('counter', {
  initialState,
  reducers: {
    increment(state) {
      state.value += 1;
    },
    decrement(state) {
      state.value -= 1;
    },
    incrementByAmount(state, amount: number) {
      state.value += amount;
    },
  },
});
```

</td>
<td>

```typescript
// counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';

const initialState: { value: number } = {
  value: 0,
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value += 1;
    },
    decrement(state) {
      state.value -= 1;
    },
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload;
    },
  },
});

const { actions, reducer } = counterSlice;
export const { increment, decrement, incrementByAmount } = actions;
export default reducer;
```

</td>
</tr>
<tr>
<th colspan="2">
组件中获取数据
</th>
</tr>
<tr>
<td valign="top">

```tsx
import { useModel } from 'foca';
import { counterModel } from './counter.model';

export const Counter: FC = () => {
  const count = useModel(counterModel, (state) => state.value);

  return <div onClick={counterModel.increment}>{count}</div>;
};
```

</td>
<td>

```tsx
import { useAppSelector, useAppDispatch } from './store';
import { increment } from './counterSlice';

export const Counter: FC = () => {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();

  return <div onClick={() => dispatch(increment())}>{count}</div>;
};
```

</td>
</tr>
<tr>
<th colspan="2">
异步请求和loading
</th>
</tr>
<tr>
<td valign="top">

```typescript
import { defineModel } from 'foca';

export const todoModel = defineModel('todos', {
  initialState: { todos: [] },
  reducers: {},
  methods: {
    // 返回Promise时自带loading
    async fetchTodos() {
      const response = await http.request('/api');
      this.setState({ todos: response.data });
    },
  },
});
```

</td>
<td>

```typescript
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchTodosAsync = createAsyncThunk(
  'todos/fetchTodos',
  async () => {
    const response = await http.request('/api');
    return response.data;
  },
);

const todoSlice = createSlice({
  name: 'todos',
  initialState: { todos: [], loading: false },
  reducers: {},
  extraReducers(builder) {
    builder
      .addCase(fetchTodosAsync.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchTodosAsync.fulfilled, (state, action) => {
        state.loading = false;
        state.todos = action.payload;
      })
      .addCase(fetchTodosAsync.rejected, (state, action) => {
        state.loading = false;
      });
  },
});

export default todosSlice.reducer;
```

</td>
</tr>
<tr>
<th colspan="2">
在组件中使用loading状态
</th>
</tr>
<tr>
<td valign="top">

```typescript
import { useLoading } from 'foca';
import { todoModel } from './todo.model';

const Todo: FC = () => {
  const loading = useLoading(todoModel.fetchTodos);

  useEffect(() => {
    todoModel.fetchTodos();
  }, []);

  return <div />;
};
```

</td>
<td>

```typescript
import { useAppSelector, useAppDispatch } from './store';

const Todo: FC = () => {
  const dispatch = useAppDispatch();
  const loading = useAppSelector((s) => s.todos.loading);

  useEffect(() => {
    dispatch(fetchTodosAsync());
  }, [dispatch]);

  return <div />;
};
```

</td>
</tr>
<tr>
<th colspan="2">
持久化
</th>
</tr>
<tr>
<td valign="top">

```typescript
import { store } from 'foca';
import { counterModel } from './counter.model';

foca.init({
  persist: [
    {
      key: 'root',
      engine: localStorage,
      models: [counterModel],
    },
  ],
});
```

</td>
<td>

```bash
pnpm add redux-persist
```

```typescript
import { configureStore } from '@reduxjs/toolkit';
import storage from 'redux-persist/lib/storage';
import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';
import counterReducer from './counterSlice';

const reducers = combineReducers({
  counter: counterReducer,
});

const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['counter'],
};

const persistedReducer = persistReducer(persistConfig, reducers);
const store = configureStore({ reducer: persistedReducer });

export default store;
```

```tsx
import { store } from './store';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';

ReactDOM.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistStore(store)}>
      <App />
    </PersistGate>
  </Provider>,
);
```

</td>
</tr>
<tr>
<th colspan="2">
计算属性
</th>
</tr>
<tr>
<td valign="top">

```typescript
import { defineModel } from 'foca';

export const todoModel = defineModel('todos', {
  initialState: { todos: [] },
  computed: {
    todos() {
      return this.state.todos.filter((todo) => !!todo.completed);
    },
  },
});

// 在组件中使用
const memoTodos = useComputed(todoModel.todos);
```

</td>
<td>

```bash
pnpm add reselect
```

```typescript
import { createSlice } from '@reduxjs/toolkit';
import { createSelector } from 'reselect';

const todoSlice = createSlice({
  name: 'todos',
  initialState: { todos: [] },
});

const memoizedSelectCompletedTodos = createSelector(
  [(state: RootState) => state.todos],
  (todos) => {
    return todos.filter((todo) => !!todo.completed);
  },
);

// 在组件中使用
const memoTodos = memoizedSelectCompletedTodos(state);
```

</td>
</tr>
</tbody>
</table>


================================================
FILE: docs/test.md
================================================
前端的需求变化总是太快导致测试用例跟不上,甚至部分程序员根本就没想过为自己写的代码编写测试,他们心里总是想着`出错了再说`。对于要求拥有高质量体验的项目,测试是必不可少的,它能使得代码更加稳健,并且在新增功能和重构代码时,都无需太担心会破坏原有的逻辑。在多人协作的项目中,充足的测试可以让其他人员对相应的逻辑有更充分的了解。

## 测试框架

- [Vitest](https://cn.vitest.dev/)
- [Jest](https://jestjs.io/zh-Hans/)
- [Mocha](https://mochajs.org/)
- [node:test](https://nodejs.org/dist/latest-v18.x/docs/api/test.html#test-runner) node@18.8.0开始提供

## 准备工作

我们已经知道,foca 是基于 redux 存储数据的,所以在测试模型之前,需要先激活 store,并且在测试完毕后销毁以免影响其他测试。

```typescript
// test/model.test.ts
import { store } from 'foca';

beforeEach(() => {
  store.init();
});

afterEach(() => {
  store.unmount();
});
```

## 单元测试

我们假设你已经写好了一个模型

```typescript
// src/models/my-custom.model.ts
import { defineModel } from 'foca';

export const myCustomModel = defineModel('my-model', {
  initialState: { count: 0 },
  reducers: {
    plus(state, step: number = 1) {
      state.count += step;
    },
    minus(state, step: number = 1) {
      state.count -= step;
    },
  },
});
```

对,它现在很简洁,但是已经满足测试条件了

```typescript
// test/model.test.ts
import { store } from 'foca';
import { myCustomModel } from '../src/models/my-custom.model.ts';

beforeEach(() => {
  store.init();
});

afterEach(() => {
  store.unmount();
});

test('initial state', () => {
  expect(myCustomModel.state.count).toBe(0);
});

test('myCustomModel.plus', () => {
  myCustomModel.plus();
  expect(myCustomModel.state.count).toBe(1);
  myCustomModel.plus(5);
  expect(myCustomModel.state.count).toBe(6);
  myCustomModel.plus(100);
  expect(myCustomModel.state.count).toBe(106);
});

test('myCustomModel.minus', () => {
  myCustomModel.minus();
  expect(myCustomModel.state.count).toBe(-1);
  myCustomModel.minus(10);
  expect(myCustomModel.state.count).toBe(-11);
  myCustomModel.minus(28);
  expect(myCustomModel.state.count).toBe(-39);
});
```

**只测试**业务上的那部分逻辑,每处逻辑分开测试,这就是 `Unit Test` 和你该做的。

## 覆盖率

对于大一些的项目,你很难保证所有逻辑都已经写进了测试,则建议打开测试框架的覆盖率功能并检查每一行的覆盖情况。一般情况下,覆盖率的报告会放到`coverage`目录,你只需要在浏览器中打开`coverage/index.html`就可以查看了。


================================================
FILE: docs/troubleshooting.md
================================================
# <!-- {docsify-ignore} -->

# 函数里 this 的类型是 any

需要在文件 **tsconfig.json** 中开启`"strict": true`或者`"noImplicitThis": true`。

# 为什么要用 this

1. 可调用额外的内部属性和方法;
2. 可调用自定义的私有方法;
3. 方便克隆(cloneModel),this 作为 context 是可变的。

# 没找到持久化守卫组件

内置在入口组件 `FocaProvider` 里了,初始化 store 的时候如果配置了 persist 属性,守卫会自动开启。

# setState 和 reducers 的区别

互补关系。methods.setState 是专门为网络请求和一些组合业务设置的快捷操作(直接传入 state 或者回调)。相对于一些不需要复用的 reducer 函数,用 setState 反而能让模型对外暴露更少的接口,组件里用起来就会更舒服一些。

# 追踪 methods 的执行状态有性能问题吗

没有。我们已经知道如果想获得状态,就必须通过`useLoading`, `getLoading` 这些 api 获取,但如果你没有显性地通过这些 api 获取某个函数的状态,就不会触发该函数的状态追踪逻辑,即自动忽略。

状态数据使用独立的内部 store 存储,任何变动都不会触发模型数据(useModel, connect)的重新检查。

# 为什么不支持 SSR

因为 foca 是遵循单一 store 存储(单例),它的优点就是 model 创建后无需手动注册,在 CSR(Client-Side-Rendering) 中用起来很流畅。而 SSR(Server-Side-Rendering) 方案中,node 进程常驻于内存,这意味着所有的请求都会共享同一个 store,数据也必然会乱套。所以一些 SSR 框架比如 next.js, remix 都无法使用了。

再者,需要SSR的页面,一般是需要 SEO 的展示页,这种项目也用不上状态管理。并且 SSR 其实不是唯一的 SEO 优化方案,利用 user-agent 配合服务端动态渲染一样可以搞定,参考文章:https://segmentfault.com/a/1190000023481810

# this.initialState 是否多余

大部分情况下你会觉得多余,直到你使用`cloneModel`复制出一个新的模型。我们允许复制模型的同时修改初始值,所以`this.initialState`就和`this.state`一样能明确自己归属于哪个模型。

同时,每次获取`this.initialState`,框架都会返回给你一份全新的数据(deep clone),这样再也不怕你会改动初始值了。

# 命名有什么建议

模型文件名建议采用 `some-word.model.ts` 这种命名方式,可读性好。<br/>
模型内容建议采用 `export const someWordModel = defineModel('some-word')` 驼峰的方式来创建,变量名和模型名具有一定的关联性,也不容易与其它模型冲突。


================================================
FILE: package.json
================================================
{
  "name": "foca",
  "version": "4.0.1",
  "repository": "git@github.com:foca-js/foca.git",
  "homepage": "https://foca.js.org",
  "keywords": [
    "redux",
    "redux-model",
    "redux-typescript",
    "react-redux",
    "react-model",
    "redux-toolkit"
  ],
  "description": "流畅的React状态管理库",
  "contributors": [
    "罪 <fanwenhua1990@gmail.com> (https://github.com/geekact)"
  ],
  "license": "MIT",
  "main": "dist/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "sideEffects": false,
  "scripts": {
    "test": "vitest run",
    "prepublishOnly": "tsup",
    "docs": "docsify serve ./docs",
    "prepare": "husky"
  },
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/index.js"
    },
    "./package.json": "./package.json"
  },
  "files": [
    "dist",
    "LICENSE",
    "package.json",
    "README.md",
    "CHANGELOG.md"
  ],
  "commitlint": {
    "extends": [
      "@commitlint/config-conventional"
    ]
  },
  "volta": {
    "node": "18.16.0",
    "pnpm": "9.15.6"
  },
  "packageManager": "pnpm@9.15.6",
  "peerDependencies": {
    "react": "^18 || ^19",
    "react-native": ">=0.69",
    "typescript": "^5"
  },
  "peerDependenciesMeta": {
    "typescript": {
      "optional": true
    },
    "react-native": {
      "optional": true
    }
  },
  "dependencies": {
    "immer": "^9.0.21",
    "react-redux": "^9.2.0",
    "redux": "^5.0.1",
    "topic": "^3.0.2"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.7.1",
    "@commitlint/config-conventional": "^19.7.1",
    "@react-native-async-storage/async-storage": "^2.1.2",
    "@redux-devtools/extension": "^3.3.0",
    "@testing-library/react": "^16.2.0",
    "@types/node": "^22.13.8",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "@vitest/coverage-istanbul": "^3.0.7",
    "docsify-cli": "^4.4.4",
    "fake-indexeddb": "^6.0.0",
    "husky": "^9.1.7",
    "jsdom": "^26.0.0",
    "localforage": "^1.10.0",
    "prettier": "^3.5.3",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-test-renderer": "^19.0.0",
    "rxjs": "^7.8.2",
    "sleep-promise": "^9.1.0",
    "ts-expect": "^1.3.0",
    "tsup": "^8.4.0",
    "typescript": "^5.8.2",
    "vitest": "^3.0.7"
  }
}


================================================
FILE: src/actions/loading.ts
================================================
import type { UnknownAction } from 'redux';

export const TYPE_SET_LOADING = '@@store/loading';

export const LOADING_CATEGORY = '##' + Math.random();

export const DESTROY_LOADING = TYPE_SET_LOADING + '/destroy';

export interface LoadingAction extends UnknownAction {
  type: typeof TYPE_SET_LOADING;
  model: string;
  method: string;
  payload: {
    loading: boolean;
    category: string | number;
  };
}

export const isLoadingAction = (
  action: UnknownAction | unknown,
): action is LoadingAction => {
  const tester = action as LoadingAction;
  return (
    tester.type === TYPE_SET_LOADING &&
    !!tester.model &&
    !!tester.method &&
    !!tester.payload
  );
};

export interface DestroyLoadingAction extends UnknownAction {
  type: typeof DESTROY_LOADING;
  model: string;
}

export const isDestroyLoadingAction = (
  action: UnknownAction | unknown,
): action is DestroyLoadingAction => {
  const tester = action as DestroyLoadingAction;
  return tester.type === DESTROY_LOADING && !!tester.model;
};


================================================
FILE: src/actions/model.ts
================================================
import type { Action, UnknownAction } from 'redux';
import { isFunction } from '../utils/is-type';

export interface PreModelAction<State extends object = object, Payload = object>
  extends UnknownAction,
    Action<string> {
  model: string;
  preModel: true;
  payload: Payload;
  actionInActionGuard?: () => void;
  consumer(state: State, action: PreModelAction<State, Payload>): State | void;
}

export interface PostModelAction<State = object>
  extends UnknownAction,
    Action<string> {
  model: string;
  postModel: true;
  next: State;
}

export const isPreModelAction = (
  action: UnknownAction | unknown,
): action is PreModelAction => {
  const test = action as PreModelAction;
  return test.preModel && !!test.model && isFunction(test.consumer);
};

export const isPostModelAction = <State extends object>(
  action: UnknownAction,
): action is PostModelAction<State> => {
  const test = action as PostModelAction<State>;
  return test.postModel && !!test.next;
};


================================================
FILE: src/actions/persist.ts
================================================
import type { UnknownAction } from 'redux';

const TYPE_PERSIST_HYDRATE = '@@persist/hydrate';

export interface PersistHydrateAction extends UnknownAction {
  type: typeof TYPE_PERSIST_HYDRATE;
  payload: Record<string, object>;
}

export const actionHydrate = (
  states: Record<string, object>,
): PersistHydrateAction => {
  return {
    type: TYPE_PERSIST_HYDRATE,
    payload: states,
  };
};

export const isHydrateAction = (
  action: UnknownAction,
): action is PersistHydrateAction => {
  return (action as PersistHydrateAction).type === TYPE_PERSIST_HYDRATE;
};


================================================
FILE: src/actions/refresh.ts
================================================
import type { UnknownAction } from 'redux';

const TYPE_REFRESH_STORE = '@@store/refresh';

export interface RefreshAction extends UnknownAction {
  type: typeof TYPE_REFRESH_STORE;
  payload: {
    force: boolean;
  };
}

export const actionRefresh = (force: boolean): RefreshAction => {
  return {
    type: TYPE_REFRESH_STORE,
    payload: {
      force,
    },
  };
};

export const isRefreshAction = (
  action: UnknownAction,
): action is RefreshAction => {
  return (action as RefreshAction).type === TYPE_REFRESH_STORE;
};


================================================
FILE: src/api/get-loading.ts
================================================
import { LOADING_CATEGORY } from '../actions/loading';
import { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';
import { loadingStore, FindLoading } from '../store/loading-store';
import { isFunction } from '../utils/is-type';

/**
 * 检测给定的effect方法中是否有正在执行的。支持多个方法同时传入。
 *
 * ```typescript
 * loading = getLoading(effect);
 * loading = getLoading(effect1, effect2, ...);
 * ```
 *
 */
export function getLoading(
  effect: PromiseEffect,
  ...more: PromiseEffect[]
): boolean;

/**
 * 检测给定的effect方法是否正在执行。
 *
 * ```typescript
 * loadings = getLoading(effect.room);
 * loading = loadings.find(CATEGORY)
 * ```
 */
export function getLoading(effect: PromiseRoomEffect): FindLoading;

/**
 * 检测给定的effect方法是否正在执行。
 *
 * ```typescript
 * loading = getLoading(effect.room, CATEGORY);
 * ```
 */
export function getLoading(
  effect: PromiseRoomEffect,
  category: string | number,
): boolean;

export function getLoading(
  effect: PromiseEffect | PromiseRoomEffect,
  category?: string | number | PromiseEffect,
): boolean | FindLoading {
  const args = arguments;

  if (effect._.hasRoom && !isFunction(category)) {
    const loadings = loadingStore.get(effect).loadings;
    return category === void 0 ? loadings : loadings.find(category);
  }

  for (let i = args.length; i-- > 0; ) {
    if (loadingStore.get(args[i]).loadings.find(LOADING_CATEGORY)) {
      return true;
    }
  }
  return false;
}


================================================
FILE: src/api/use-computed.ts
================================================
import { ComputedFlag } from '../model/types';
import { useModelSelector } from '../redux/use-selector';
import { toArgs } from '../utils/to-args';

export interface UseComputedFlag extends ComputedFlag {
  (...args: any[]): any;
}

/**
 * 计算属性hooks函数,第二个参数开始传入计算属性的参数(如果有)
 *
 * ```typescript
 *
 * const App: FC = () => {
 *   const fullName = useComputed(model.fullName);
 *   const profile = useComputed(model.profile, 25);
 *   return <p>{profile}</p>;
 * }
 *
 * const model = defineModel('my-model', {
 *   initialState: { firstName: '', lastName: '' },
 *   computed: {
 *     fullName() {
 *       return this.state.firstName + this.state.lastName;
 *     },
 *     profile(age: number, address?: string) {
 *       return this.fullName() + age + address;
 *     }
 *   }
 * });
 *
 * ```
 */
export function useComputed<T extends UseComputedFlag>(
  ref: T,
  ...args: Parameters<T>
): T extends (...args: any[]) => infer R ? R : never;

export function useComputed(ref: UseComputedFlag) {
  const args = toArgs(arguments, 1);
  return useModelSelector(() => ref.apply(null, args));
}


================================================
FILE: src/api/use-isolate.ts
================================================
import { useEffect, useMemo, useRef, useState } from 'react';
import { DestroyLoadingAction, DESTROY_LOADING } from '../actions/loading';
import { loadingStore } from '../store/loading-store';
import { modelStore } from '../store/model-store';
import { cloneModel } from '../model/clone-model';
import { Model } from '../model/types';

let globalCounter = 0;
const hotReloadCounter: Record<string, number> = {};

/**
 * 创建局部模型,它的数据变化不会影响到全局模型,而且会随着组件一起销毁
 * ```typescript
 * const testModel = defineModel({
 *   initialState,
 *   events: {
 *     onInit() {},    // 挂载回调
 *     onDestroy() {}, // 销毁回调
 *   }
 * });
 *
 * function App() {
 *   const model = useIsolate(testModel);
 *   const state = useModel(model);
 *
 *   return <p>Hello</p>;
 * }
 * ```
 */
export const useIsolate = <
  State extends object = object,
  Action extends object = object,
  Effect extends object = object,
  Computed extends object = object,
>(
  globalModel: Model<string, State, Action, Effect, Computed>,
): Model<string, State, Action, Effect, Computed> => {
  const initialCount = useState(() => globalCounter++)[0];
  const uniqueName =
    process.env.NODE_ENV === 'production'
      ? useProdName(globalModel.name, initialCount)
      : useDevName(globalModel, initialCount, new Error());

  const localModelRef = useRef<typeof globalModel | undefined>(undefined);
  useEffect(() => {
    localModelRef.current = isolateModel;
  });

  // 热更时会重新执行useMemo,因此只能用ref
  const isolateModel =
    localModelRef.current?.name === uniqueName
      ? localModelRef.current
      : cloneModel(uniqueName, globalModel);

  return isolateModel;
};

const useProdName = (modelName: string, count: number) => {
  const uniqueName = `@isolate:${modelName}#${count}`;

  useEffect(
    () => () => {
      setTimeout(unmountModel, 0, uniqueName);
    },
    [uniqueName],
  );

  return uniqueName;
};

/**
 * 开发模式下,需要Hot Reload。
 * 必须保证数据不会丢,即如果用户一直保持`model.name`不变,就被判定为可以共享热更新之前的数据。
 *
 * 必须严格控制count在组件内的自增次数,否则在第一次修改model的name时,总是会报错:
 * Warning: Cannot update a component (`XXX`) while rendering a different component (`XXX`)
 */
const useDevName = (model: Model, count: number, err: Error) => {
  const componentName = useMemo((): string => {
    try {
      const stacks = err.stack!.split('\n');
      const innerNamePattern = new RegExp(
        // vitest测试框架的stack增加了 Module.
        `at\\s(?:Module\\.)?${useIsolate.name}\\s\\(`,
        'i',
      );
      const componentNamePattern = /at\s(.+?)\s\(/i;
      for (let i = 0; i < stacks.length; ++i) {
        if (innerNamePattern.test(stacks[i]!)) {
          return stacks[i + 1]!.match(componentNamePattern)![1]!;
        }
      }
    } catch {}
    return 'Component';
  }, [err.stack]);

  /**
   * 模型文件重新保存时组件会导入新的对象,需根据这个特性重新克隆模型
   */
  const globalModelRef = useRef<{ model?: Model; count: number }>({ count: 0 });
  useEffect(() => {
    if (globalModelRef.current.model !== model) {
      globalModelRef.current = { model, count: ++globalModelRef.current.count };
    }
  });

  const uniqueName = `@isolate:${model.name}:${componentName}#${count}-${
    globalModelRef.current.count +
    Number(globalModelRef.current.model !== model)
  }`;

  /**
   * 计算热更次数,如果停止热更,说明组件被卸载
   */
  useMemo(() => {
    hotReloadCounter[uniqueName] ||= 0;
    ++hotReloadCounter[uniqueName];
  }, [uniqueName]);

  /**
   * 热更新时会重新执行一次useEffect
   * setTimeout可以让其他useEffect有充分的时间使用model
   *
   * 需要卸载模型的场景是:
   * 1. 组件hooks增减或者调换顺序(initialCount会自增)
   * 2. 组件卸载
   * 3. model.name变更
   * 4. model逻辑变更
   */
  useEffect(() => {
    const prev = hotReloadCounter[uniqueName];
    return () => {
      setTimeout(() => {
        const unmounted = prev === hotReloadCounter[uniqueName];
        unmounted && unmountModel(uniqueName);
      });
    };
  }, [uniqueName]);

  return uniqueName;
};

const unmountModel = (modelName: string) => {
  modelStore['removeReducer'](modelName);
  loadingStore.dispatch<DestroyLoadingAction>({
    type: DESTROY_LOADING,
    model: modelName,
  });
};


================================================
FILE: src/api/use-loading.ts
================================================
import { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';
import { FindLoading } from '../store/loading-store';
import { useLoadingSelector } from '../redux/use-selector';
import { getLoading } from './get-loading';

/**
 * 检测给定的effect方法中是否有正在执行的。支持多个方法同时传入。
 *
 * ```typescript
 * loading = useLoading(effect);
 * loading = useLoading(effect1, effect2, ...);
 * ```
 *
 */
export function useLoading(
  effect: PromiseEffect,
  ...more: PromiseEffect[]
): boolean;

/**
 * 检测给定的effect方法是否正在执行。
 *
 * ```typescript
 * loadings = useLoading(effect.room);
 * loading = loadings.find(CATEGORY);
 * ```
 */
export function useLoading(effect: PromiseRoomEffect): FindLoading;

/**
 * 检测给定的effect方法是否正在执行。
 *
 * ```typescript
 * loading = useLoading(effect.room, CATEGORY);
 * ```
 */
export function useLoading(
  effect: PromiseRoomEffect,
  category: string | number,
): boolean;

export function useLoading(): boolean | FindLoading {
  const args = arguments as unknown as Parameters<typeof getLoading>;

  return useLoadingSelector(() => {
    return getLoading.apply(null, args);
  });
}


================================================
FILE: src/api/use-model.ts
================================================
import { shallowEqual } from 'react-redux';
import { deepEqual } from '../utils/deep-equal';
import type { Model } from '../model/types';
import { toArgs } from '../utils/to-args';
import { useModelSelector } from '../redux/use-selector';
import { isFunction, isString } from '../utils/is-type';

/**
 * hooks新旧数据的对比方式:
 *
 * - `deepEqual`     深度对比,对比所有层级的内容。传递selector时默认使用。
 * - `shallowEqual`  浅对比,只比较对象第一层。传递多个模型但没有selector时默认使用。
 * - `strictEqual`   全等(===)对比。只传一个模型但没有selector时默认使用。
 */
export type Algorithm = 'strictEqual' | 'shallowEqual' | 'deepEqual';

/**
 * * 获取模型的状态数据。
 * * 传入一个模型时,将返回该模型的状态。
 * * 传入多个模型时,则返回一个以模型名称为key、状态为value的大对象。
 * * 最后一个参数如果是**函数**,则为状态过滤函数,过滤函数的结果视为最终返回值。
 */
export function useModel<State extends object>(
  model: Model<string, State>,
): State;
export function useModel<State extends object, T>(
  model: Model<string, State>,
  selector: (state: State) => T,
  algorithm?: Algorithm,
): T;

export function useModel<
  Name1 extends string,
  State1 extends object,
  Name2 extends string,
  State2 extends object,
>(
  model1: Model<Name1, State1>,
  model2: Model<Name2, State2>,
): {
  [K in Name1]: State1;
} & {
  [K in Name2]: State2;
};
export function useModel<State1 extends object, State2 extends object, T>(
  model1: Model<string, State1>,
  model2: Model<string, State2>,
  selector: (state1: State1, state2: State2) => T,
  algorithm?: Algorithm,
): T;

export function useModel<
  Name1 extends string,
  State1 extends object,
  Name2 extends string,
  State2 extends object,
  Name3 extends string,
  State3 extends object,
>(
  model1: Model<Name1, State1>,
  model2: Model<Name2, State2>,
  model3: Model<Name3, State3>,
): {
  [K in Name1]: State1;
} & {
  [K in Name2]: State2;
} & {
  [K in Name3]: State3;
};
export function useModel<
  State1 extends object,
  State2 extends object,
  State3 extends object,
  T,
>(
  model1: Model<string, State1>,
  model2: Model<string, State2>,
  model3: Model<string, State3>,
  selector: (state1: State1, state2: State2, state3: State3) => T,
  algorithm?: Algorithm,
): T;

export function useModel<
  Name1 extends string,
  State1 extends object,
  Name2 extends string,
  State2 extends object,
  Name3 extends string,
  State3 extends object,
  Name4 extends string,
  State4 extends object,
>(
  model1: Model<Name1, State1>,
  model2: Model<Name2, State2>,
  model3: Model<Name3, State3>,
  model4: Model<Name4, State4>,
): {
  [K in Name1]: State1;
} & {
  [K in Name2]: State2;
} & {
  [K in Name3]: State3;
} & {
  [K in Name4]: State4;
};
export function useModel<
  State1 extends object,
  State2 extends object,
  State3 extends object,
  State4 extends object,
  T,
>(
  model1: Model<string, State1>,
  model2: Model<string, State2>,
  model3: Model<string, State3>,
  model4: Model<string, State4>,
  selector: (
    state1: State1,
    state2: State2,
    state3: State3,
    state4: State4,
  ) => T,
  algorithm?: Algorithm,
): T;

export function useModel<
  Name1 extends string,
  State1 extends object,
  Name2 extends string,
  State2 extends object,
  Name3 extends string,
  State3 extends object,
  Name4 extends string,
  State4 extends object,
  Name5 extends string,
  State5 extends object,
>(
  model1: Model<Name1, State1>,
  model2: Model<Name2, State2>,
  model3: Model<Name3, State3>,
  model4: Model<Name4, State4>,
  model5: Model<Name5, State5>,
): {
  [K in Name1]: State1;
} & {
  [K in Name2]: State2;
} & {
  [K in Name3]: State3;
} & {
  [K in Name4]: State4;
} & {
  [K in Name5]: State5;
};
export function useModel<
  State1 extends object,
  State2 extends object,
  State3 extends object,
  State4 extends object,
  State5 extends object,
  T,
>(
  model1: Model<string, State1>,
  model2: Model<string, State2>,
  model3: Model<string, State3>,
  model4: Model<string, State4>,
  model5: Model<string, State5>,
  selector: (
    state1: State1,
    state2: State2,
    state3: State3,
    state4: State4,
    state5: State5,
  ) => T,
  algorithm?: Algorithm,
): T;

export function useModel(): any {
  const args = toArgs(arguments);
  let algorithm: Algorithm | false =
    args.length > 1 && isString(args[args.length - 1]) && args.pop();
  const selector: Function | false =
    args.length > 1 && isFunction(args[args.length - 1]) && args.pop();
  const models: Model[] = args;
  const modelsLength = models.length;
  const onlyOneModel = modelsLength === 1;

  if (!algorithm) {
    if (selector) {
      // 返回子集或者计算过的内容。
      // 如果只是从模型中获取数据且没有做转换,则大部分时间会降级为shallow或者strict。
      // 如果对数据做了转换,则肯定需要使用深对比。
      algorithm = 'deepEqual';
    } else if (onlyOneModel) {
      // 一个model属于一个reducer,reducer已经使用了深对比来判断是否变化,
      algorithm = 'strictEqual';
    } else {
      // { key => model } 集合。
      // 一个model属于一个reducer,reducer已经使用了深对比来判断是否变化,
      algorithm = 'shallowEqual';
    }
  }

  // 储存了结果说明是state状态变化导致的对比计算。
  // 因为存在闭包,除模型外的所有参数都是旧的,
  // 所以我们只需要保证用到的模型数据不变即可,这样可以减少无意义的计算。
  let hasMemo = false,
    snapshot: any,
    prevState: Record<string, object>,
    currentStates: object[],
    i: number,
    changed: boolean;

  const reducerNames: string[] = [];
  for (i = 0; i < modelsLength; ++i) {
    reducerNames.push(models[i]!.name);
  }

  return useModelSelector((state: Record<string, object>) => {
    if (hasMemo) {
      changed = false;
      for (i = modelsLength; i-- > 0; ) {
        const reducerName = reducerNames[i]!;
        if (state[reducerName] !== prevState[reducerName]) {
          changed = true;
          break;
        }
      }

      if (!changed) {
        prevState = state;
        return snapshot;
      }
    }

    prevState = state;
    hasMemo = true;

    if (onlyOneModel) {
      const firstState = state[reducerNames[0]!];
      return (snapshot = selector ? selector(firstState) : firstState);
    }

    if (selector) {
      currentStates = [];
      for (i = modelsLength; i-- > 0; ) {
        currentStates[i] = state[reducerNames[i]!]!;
      }
      return (snapshot = selector.apply(null, currentStates));
    }

    snapshot = {};
    for (i = modelsLength; i-- > 0; ) {
      const reducerName = reducerNames[i]!;
      snapshot[reducerName] = state[reducerName];
    }
    return snapshot;
  }, compareFn[algorithm]);
}

const compareFn: Record<
  Algorithm,
  undefined | ((previous: any, next: any) => boolean)
> = {
  deepEqual: deepEqual,
  shallowEqual: shallowEqual,
  strictEqual: void 0,
};


================================================
FILE: src/engines/memory.ts
================================================
import type { StorageEngine } from './storage-engine';

let cache: Partial<Record<string, string>> = {};

export const memoryStorage: StorageEngine = {
  getItem(key) {
    return cache[key] === void 0 ? null : cache[key]!;
  },
  setItem(key, value) {
    cache[key] = value;
  },
  removeItem(key) {
    cache[key] = void 0;
  },
  clear() {
    cache = {};
  },
};


================================================
FILE: src/engines/storage-engine.ts
================================================
export interface StorageEngine {
  getItem(key: string): string | null | Promise<string | null>;
  setItem(key: string, value: string): any;
  removeItem(key: string): any;
  clear(): any;
}


================================================
FILE: src/index.ts
================================================
// 模型中使用
export { defineModel } from './model/define-model';
export { cloneModel } from './model/clone-model';

// 组件中使用
export { useModel } from './api/use-model';
export { useLoading } from './api/use-loading';
export { getLoading } from './api/get-loading';
export { useComputed } from './api/use-computed';
export { useIsolate } from './api/use-isolate';
export { connect } from './redux/connect';

// 入口使用
export { compose } from 'redux';
export { modelStore as store } from './store/model-store';
export { FocaProvider } from './redux/foca-provider';
export { memoryStorage } from './engines/memory';

// 可能用到的TS类型
export type {
  Action,
  UnknownAction,
  Dispatch,
  MiddlewareAPI,
  Middleware,
  StoreEnhancer,
  Unsubscribe,
} from 'redux';
export type { Model } from './model/types';
export type { StorageEngine } from './engines/storage-engine';


================================================
FILE: src/middleware/action-in-action.interceptor.ts
================================================
import type { Middleware, UnknownAction } from 'redux';
import { isPreModelAction } from '../actions/model';

// 开发者有可能在action中执行action,这是十分不规范的操作。
export const actionInActionInterceptor: Middleware = () => {
  let dispatching = false;
  let prevAction: UnknownAction | null = null;

  return (dispatch) => (action) => {
    if (!isPreModelAction(action)) {
      // 非model的action会直接进入redux,redux中已经有dispatch保护机制。
      return dispatch(action);
    }

    // model的action如果没有变化则不会进入redux,所以需要在这里额外保护。
    if (dispatching) {
      throw new Error(
        '[dispatch] 派发任务冲突,请检查是否在reducers函数中直接或者间接执行了其他reducers或者methods函数。\nreducers的唯一职责是更新当前的state,有额外的业务逻辑时请把methods作为执行入口并按需调用reducers。\n\n当前冲突的reducer:\n\n' +
          JSON.stringify(action, null, 4) +
          '\n\n上次执行未完成的reducer:\n\n' +
          JSON.stringify(prevAction, null, 4) +
          '\n\n',
      );
    }

    try {
      dispatching = true;
      prevAction = action;
      /**
       * react-redux@8+ 主要服务于react18
       * 在react17中,有可能出现redux遍历subscriber时立即触发dispatch,然后这边来不及设置dispatching=false
       * 在react18中,如果使用`ReactDOM.render()`旧入口,则依旧会有这个问题。
       * @link https://github.com/foca-js/foca/issues/20
       */
      action.actionInActionGuard = () => {
        dispatching = false;
        prevAction = null;
      };
      return dispatch(action);
    } catch (e) {
      prevAction = null;
      dispatching = false;
      throw e;
    }
  };
};


================================================
FILE: src/middleware/destroy-loading.interceptor.ts
================================================
import type { Middleware } from 'redux';
import { isDestroyLoadingAction } from '../actions/loading';

export const destroyLoadingInterceptor: Middleware =
  (api) => (dispatch) => (action) => {
    if (
      !isDestroyLoadingAction(action) ||
      api.getState().hasOwnProperty(action.model)
    ) {
      return dispatch(action);
    }

    return action;
  };


================================================
FILE: src/middleware/freeze-state.middleware.ts
================================================
import { freeze } from 'immer';
import type { Middleware } from 'redux';

export const freezeStateMiddleware: Middleware = (api) => {
  freeze(api.getState(), true);

  return (dispatch) => (action) => {
    try {
      return dispatch(action);
    } finally {
      freeze(api.getState(), true);
    }
  };
};


================================================
FILE: src/middleware/loading.interceptor.ts
================================================
import type { Middleware } from 'redux';
import type { LoadingStore, LoadingStoreState } from '../store/loading-store';
import { isLoadingAction } from '../actions/loading';

export const loadingInterceptor = (
  loadingStore: LoadingStore,
): Middleware<{}, LoadingStoreState> => {
  return () => (dispatch) => (action) => {
    if (!isLoadingAction(action)) {
      return dispatch(action);
    }

    const {
      model,
      method,
      payload: { category, loading },
    } = action;

    if (loadingStore.isModelInitializing(model)) {
      loadingStore.activate(model, method);
    } else if (!loadingStore.isActive(model, method)) {
      return;
    }

    const record = loadingStore.getItem(model, method);

    if (!record || record.loadings.data[category] !== loading) {
      return dispatch(action);
    }

    return action;
  };
};


================================================
FILE: src/middleware/model.interceptor.ts
================================================
import type { Middleware } from 'redux';
import { deepEqual } from '../utils/deep-equal';
import { isPreModelAction, PostModelAction } from '../actions/model';
import { immer } from '../utils/immer';

export const modelInterceptor: Middleware<{}, Record<string, object>> =
  (api) => (dispatch) => (action) => {
    if (!isPreModelAction(action)) {
      return dispatch(action);
    }

    const prev = api.getState()[action.model]!;
    const next = immer.produce(prev, (draft) => {
      return action.consumer(draft, action);
    });

    action.actionInActionGuard && action.actionInActionGuard();

    if (deepEqual(prev, next)) return action;

    return dispatch({
      type: action.type,
      model: action.model,
      postModel: true,
      next: next,
    } satisfies PostModelAction);
  };


================================================
FILE: src/model/clone-model.ts
================================================
import { isFunction } from '../utils/is-type';
import { defineModel } from './define-model';
import type { DefineModelOptions, InternalModel, Model } from './types';

const editableKeys = [
  'initialState',
  'events',
  'persist',
  'skipRefresh',
] as const;

type EditableKeys = (typeof editableKeys)[number];

type OverrideOptions<
  State extends object,
  Action extends object,
  Effect extends object,
  Computed extends object,
  PersistDump,
> = Pick<
  DefineModelOptions<State, Action, Effect, Computed, PersistDump>,
  EditableKeys
>;

export const cloneModel = <
  Name extends string,
  State extends object,
  Action extends object,
  Effect extends object,
  Computed extends object,
  PersistDump,
>(
  uniqueName: Name,
  model: Model<string, State, Action, Effect, Computed>,
  options?:
    | Partial<OverrideOptions<State, Action, Effect, Computed, PersistDump>>
    | ((
        prev: OverrideOptions<State, Action, Effect, Computed, PersistDump>,
      ) => Partial<
        OverrideOptions<State, Action, Effect, Computed, PersistDump>
      >),
): Model<Name, State, Action, Effect, Computed> => {
  const realModel = model as unknown as InternalModel<
    string,
    State,
    Action,
    Effect,
    Computed
  >;

  const prevOpts = realModel._$opts;
  const nextOpts = Object.assign({}, prevOpts);

  if (options) {
    Object.assign(nextOpts, isFunction(options) ? options(nextOpts) : options);

    /* istanbul ignore else -- @preserve  */
    if (process.env.NODE_ENV !== 'production') {
      (Object.keys(nextOpts) as EditableKeys[]).forEach((key) => {
        if (
          nextOpts[key] !== prevOpts[key] &&
          editableKeys.indexOf(key) === -1
        ) {
          throw new Error(
            `[model:${uniqueName}] 复制模型时禁止重写属性'${key}'`,
          );
        }
      });
    }
  }

  return defineModel(uniqueName, nextOpts);
};


================================================
FILE: src/model/define-model.ts
================================================
import { parseState, stringifyState } from '../utils/serialize';
import { deepEqual } from '../utils/deep-equal';
import { EnhancedAction, enhanceAction } from './enhance-action';
import { EnhancedEffect, enhanceEffect } from './enhance-effect';
import { modelStore } from '../store/model-store';
import { createReducer } from '../redux/create-reducer';
import { composeGetter, defineGetter } from '../utils/getter';
import { getMethodCategory } from '../utils/get-method-category';
import { guard } from './guard';
import { depsCollector } from '../reactive/deps-collector';
import { ObjectDeps } from '../reactive/object-deps';
import type {
  ActionCtx,
  EffectCtx,
  DefineModelOptions,
  GetInitialState,
  GetState,
  Model,
  ComputedCtx,
  EventCtx,
  InternalModel,
  SetStateCallback,
  ComputedFlag,
} from './types';
import { isFunction } from '../utils/is-type';
import { Unsubscribe } from 'redux';
import { freeze, original, isDraft } from 'immer';
import { isPromise } from '../utils/is-promise';
import { enhanceComputed } from './enhance-computed';

export const defineModel = <
  Name extends string,
  State extends object,
  Action extends object,
  Effect extends object,
  Computed extends object,
  PersistDump,
>(
  uniqueName: Name,
  options: DefineModelOptions<State, Action, Effect, Computed, PersistDump>,
): Model<Name, State, Action, Effect, Computed> => {
  guard(uniqueName);

  const { reducers, methods, computed, skipRefresh, events } = options;
  /**
   * 防止初始化数据在外面被修改从而影响到store,
   * 这属于小概率事件,所以仅需要在开发环境处理,
   * 而且在严格模式下,runtime修改冻结数据会直接报错,可以提醒开发者修正
   */
  const initialState =
    process.env.NODE_ENV !== 'production'
      ? freeze(options.initialState, true)
      : options.initialState;

  /* istanbul ignore else -- @preserve  */
  if (process.env.NODE_ENV !== 'production') {
    const items = [
      { name: 'reducers', value: reducers },
      { name: 'methods', value: methods },
      { name: 'computed', value: computed },
    ];
    const validateUniqueMethod = (index1: number, index2: number) => {
      const item1 = items[index1]!;
      const item2 = items[index2]!;
      if (item1.value && item2.value) {
        Object.keys(item1.value).forEach((key) => {
          if (item2.value!.hasOwnProperty(key)) {
            throw new Error(
              `[model:${uniqueName}] 属性'${key}'在${item1.name}和${item2.name}中重复使用`,
            );
          }
        });
      }
    };
    validateUniqueMethod(0, 1);
    validateUniqueMethod(0, 2);
    validateUniqueMethod(1, 2);
  }

  /* istanbul ignore else -- @preserve  */
  if (process.env.NODE_ENV !== 'production') {
    if (!deepEqual(parseState(stringifyState(initialState)), initialState)) {
      throw new Error(
        `[model:${uniqueName}] initialState 包含了不可系列化的数据,允许的类型为:Object, Array, Number, String, Undefined 和 Null`,
      );
    }
  }

  const getState = <T extends object>(obj: T): T & GetState<State> => {
    return defineGetter(obj, 'state', () => {
      const state = modelStore.getState()[uniqueName];
      return depsCollector.active
        ? new ObjectDeps(modelStore, uniqueName).start(state)
        : state;
    });
  };

  const getInitialState = <T extends object>(
    obj: T,
  ): T & GetInitialState<State> => {
    return defineGetter(obj, 'initialState', () =>
      parseState(stringifyState(initialState)),
    );
  };

  const actionCtx: ActionCtx<State> = composeGetter(
    {
      name: uniqueName,
    },
    getInitialState,
  );

  const createEffectCtx = (methodName: string): EffectCtx<State> => {
    const isArrayState = Array.isArray(initialState);
    const obj: Pick<EffectCtx<State>, 'setState'> = {
      // @ts-expect-error
      setState: enhanceAction(
        actionCtx,
        `${methodName}.setState`,
        <K extends keyof State>(
          state: State,
          fn_state: SetStateCallback<State, K> | State | Pick<State, K>,
        ) => {
          const nextState = isFunction<SetStateCallback<State, K>>(fn_state)
            ? fn_state(state)
            : fn_state;

          if (nextState === void 0) return;

          return isArrayState || isDraft(nextState)
            ? nextState
            : Object.assign({}, original(state), nextState);
        },
      ),
    };
    return composeGetter(
      Object.assign(obj, { name: uniqueName }),
      getState,
      getInitialState,
    );
  };

  const enhancedMethods: {
    [K in ReturnType<typeof getMethodCategory>]: Record<
      string,
      EnhancedAction<State> | EnhancedEffect | ComputedFlag
    >;
  } = {
    external: {},
    internal: {},
  };

  if (reducers) {
    const reducerKeys = Object.keys(reducers);
    for (let i = reducerKeys.length; i-- > 0; ) {
      const key = reducerKeys[i]!;
      enhancedMethods[getMethodCategory(key)][key] = enhanceAction(
        actionCtx,
        key,
        reducers[key]!,
      );
    }
  }

  if (computed) {
    const computedCtx: ComputedCtx<State> & {
      [K in string]?: ComputedFlag;
    } = composeGetter({ name: uniqueName }, getState);
    const computedKeys = Object.keys(computed);

    for (let i = computedKeys.length; i-- > 0; ) {
      const key = computedKeys[i]!;
      computedCtx[key] = enhancedMethods[getMethodCategory(key)][key] =
        enhanceComputed(
          computedCtx,
          uniqueName,
          key,
          // @ts-expect-error
          computed[key],
        );
    }
  }

  if (methods) {
    let ctx: EffectCtx<State>;
    const ctxs: EffectCtx<State>[] = [(ctx = createEffectCtx(''))];
    const methodKeys = Object.keys(methods);

    for (let i = methodKeys.length; i-- > 0; ) {
      const key = methodKeys[i]!;
      if (process.env.NODE_ENV !== 'production') {
        ctxs.push((ctx = createEffectCtx(key)));
      }

      enhancedMethods[getMethodCategory(key)][key] = enhanceEffect(
        ctx,
        key,
        // @ts-expect-error
        methods[key],
      );
    }

    for (let i = ctxs.length; i-- > 0; ) {
      Object.assign(
        ctxs[i]!,
        enhancedMethods.external,
        enhancedMethods.internal,
      );
    }
  }

  if (events) {
    const { onInit, onChange, onDestroy } = events;
    const eventCtx: EventCtx<State> = Object.assign(
      composeGetter({ name: uniqueName }, getState),
      enhancedMethods.external,
      enhancedMethods.internal,
    );

    modelStore.onInitialized().then(() => {
      const subscriptions: Unsubscribe[] = [];

      if (onChange) {
        let prevState = eventCtx.state;
        subscriptions.push(
          modelStore.subscribe(() => {
            const nextState = eventCtx.state;
            if (
              modelStore.isReady &&
              prevState !== nextState &&
              nextState !== void 0
            ) {
              onChange.call(eventCtx, prevState, nextState);
            }
            prevState = nextState;
          }),
        );
      }

      if (onDestroy) {
        subscriptions.push(
          modelStore.subscribe(() => {
            if (eventCtx.state === void 0) {
              for (let i = 0; i < subscriptions.length; ++i) {
                subscriptions[i]!();
              }
              onDestroy.call(null as never, uniqueName);
            }
          }),
        );
      }

      if (onInit) {
        /**
         * 初始化时,用到它的React组件可能还没加载,所以执行async-method时无法判断是否需要保存loading。因此需要一个钩子来处理事件周期
         * @see https://github.com/foca-js/foca/issues/38
         */
        modelStore.topic.publish('modelPreInit', uniqueName);
        const promiseOrVoid = onInit.call(eventCtx);
        const postInit = () => {
          modelStore.topic.publish('modelPostInit', uniqueName);
        };
        if (isPromise(promiseOrVoid)) {
          promiseOrVoid.then(postInit, postInit);
        } else {
          postInit();
        }
      }
    });
  }

  modelStore['appendReducer'](
    uniqueName,
    createReducer({
      name: uniqueName,
      initialState,
      allowRefresh: !skipRefresh,
    }),
  );

  const model: InternalModel<Name, State, Action, Effect, Computed> =
    Object.assign(
      composeGetter(
        {
          name: uniqueName,
          _$opts: options,
          _$persistCtx: getInitialState({}),
        },
        getState,
      ),
      enhancedMethods.external,
    );

  return model as any;
};


================================================
FILE: src/model/enhance-action.ts
================================================
import type { PreModelAction } from '../actions/model';
import { modelStore } from '../store/model-store';
import { toArgs } from '../utils/to-args';
import type { ActionCtx } from './types';

export interface EnhancedAction<State extends object> {
  (payload: any): PreModelAction<State>;
}

export const enhanceAction = <State extends object>(
  ctx: ActionCtx<State>,
  actionName: string,
  consumer: (state: State, ...args: any[]) => any,
): EnhancedAction<State> => {
  const modelName = ctx.name;
  const actionType = modelName + '.' + actionName;

  const enhancedConsumer: PreModelAction<State, any[]>['consumer'] = (
    state,
    action,
  ) => {
    return consumer.apply(
      ctx,
      [state].concat(action.payload) as [state: State, ...args: any[]],
    );
  };

  const fn: EnhancedAction<State> = function () {
    return modelStore.dispatch<PreModelAction<State, any[]>>({
      type: actionType,
      model: modelName,
      preModel: true,
      payload: toArgs(arguments),
      consumer: enhancedConsumer,
    });
  };

  return fn;
};


================================================
FILE: src/model/enhance-computed.ts
================================================
import { ComputedValue } from '../reactive/computed-value';
import { modelStore } from '../store/model-store';
import { toArgs } from '../utils/to-args';
import { ComputedCtx, ComputedFlag } from './types';

export const enhanceComputed = <State extends object>(
  ctx: ComputedCtx<State>,
  modelName: string,
  computedName: string,
  fn: (...args: any[]) => any,
): ComputedFlag => {
  let caches: {
    deps: any[];
    skipCount: number;
    ref: ComputedValue;
  }[] = [];

  function anonymousFn() {
    const args = toArgs(arguments);
    let hitCache: (typeof caches)[number] | undefined;

    searchCache: for (let i = 0; i < caches.length; ++i) {
      const cache = caches[i]!;
      if (hitCache) {
        ++cache.skipCount;
        continue;
      }
      for (let j = 0; j < cache.deps.length; ++j) {
        if (args[j] !== cache.deps[j]) {
          ++cache.skipCount;
          continue searchCache;
        }
      }
      cache.skipCount = 0;
      hitCache = cache;
    }

    if (hitCache) return hitCache.ref.value;

    if (caches.length > 10) {
      caches = caches.filter((cache) => cache.skipCount < 15);
    }

    hitCache = {
      deps: args,
      skipCount: 0,
      ref: new ComputedValue(modelStore, modelName, computedName, () =>
        fn.apply(ctx, args),
      ),
    };
    caches.push(hitCache);
    return hitCache.ref.value;
  }

  return anonymousFn as any;
};


================================================
FILE: src/model/enhance-effect.ts
================================================
import {
  LoadingAction,
  LOADING_CATEGORY,
  TYPE_SET_LOADING,
} from '../actions/loading';
import type { EffectCtx } from './types';
import { isPromise } from '../utils/is-promise';
import { toArgs } from '../utils/to-args';
import { loadingStore } from '../store/loading-store';

interface RoomFunc<P extends any[] = any[], R = Promise<any>> {
  (category: number | string): {
    execute(...args: P): R;
  };
}

interface AsyncRoomEffect<P extends any[] = any[], R = Promise<any>>
  extends RoomFunc<P, R> {
  readonly _: {
    readonly model: string;
    readonly method: string;
    readonly hasRoom: true;
  };
}

interface AsyncEffect<P extends any[] = any[], R = Promise<any>>
  extends EffectFunc<P, R> {
  readonly _: {
    readonly model: string;
    readonly method: string;
    readonly hasRoom: '';
  };
  /**
   * 对同一effect函数的执行状态进行分类以实现独立保存。好处有:
   *
   * 1. 并发请求同一个请求时不会互相覆盖执行状态。
   * <br>
   * 2. 可以精确地判断业务中是哪个控件或者逻辑正在执行。
   *
   * ```typescript
   * model.effect.room(CATEGORY).execute(...);
   * ```
   *
   * @see useLoading(effect.room)
   * @see getLoading(effect.room)
   * @since 0.11.4
   *
   */
  readonly room: AsyncRoomEffect<P, R>;
}

export type PromiseEffect = AsyncEffect;
export type PromiseRoomEffect = AsyncRoomEffect;

interface EffectFunc<P extends any[] = any[], R = Promise<any>> {
  (...args: P): R;
}

export type EnhancedEffect<P extends any[] = any[], R = Promise<any>> =
  R extends Promise<any> ? AsyncEffect<P, R> : EffectFunc<P, R>;

type NonReadonly<T extends object> = {
  -readonly [K in keyof T]: T[K];
};

export const enhanceEffect = <State extends object>(
  ctx: EffectCtx<State>,
  methodName: string,
  effect: (...args: any[]) => any,
): EnhancedEffect => {
  const fn: NonReadonly<EnhancedEffect> & EffectFunc = function () {
    return execute(ctx, methodName, effect, toArgs(arguments));
  };

  fn._ = {
    model: ctx.name,
    method: methodName,
    hasRoom: '',
  };

  const room: NonReadonly<AsyncRoomEffect> & RoomFunc = (
    category: number | string,
  ) => ({
    execute() {
      return execute(ctx, methodName, effect, toArgs(arguments), category);
    },
  });

  room._ = Object.assign({}, fn._, {
    hasRoom: true as const,
  });

  fn.room = room;

  return fn;
};

const dispatchLoading = (
  modelName: string,
  methodName: string,
  loading: boolean,
  category?: number | string,
) => {
  loadingStore.dispatch<LoadingAction>({
    type: TYPE_SET_LOADING,
    model: modelName,
    method: methodName,
    payload: {
      category: category === void 0 ? LOADING_CATEGORY : category,
      loading,
    },
  });
};

const execute = <State extends object>(
  ctx: EffectCtx<State>,
  methodName: string,
  effect: (...args: any[]) => any,
  args: any[],
  category?: number | string,
) => {
  const modelName = ctx.name;
  const resultOrPromise = effect.apply(ctx, args);

  if (!isPromise(resultOrPromise)) return resultOrPromise;

  dispatchLoading(modelName, methodName, true, category);

  return resultOrPromise.then(
    (result) => {
      return dispatchLoading(modelName, methodName, false, category), result;
    },
    (e: unknown) => {
      dispatchLoading(modelName, methodName, false, category);
      throw e;
    },
  );
};


================================================
FILE: src/model/guard.ts
================================================
const counter: Record<string, number> = {};

export const guard = (modelName: string) => {
  counter[modelName] ||= 0;

  if (process.env.NODE_ENV !== 'production') {
    setTimeout(() => {
      --counter[modelName]!;
    });
  }

  if (++counter[modelName] > 1) {
    throw new Error(`模型名称'${modelName}'被重复使用`);
  }
};


================================================
FILE: src/model/types.ts
================================================
import type { UnknownAction } from 'redux';
import type { EnhancedEffect } from './enhance-effect';
import type { PersistMergeMode } from '../persist/persist-item';

export interface ComputedFlag {
  readonly _computedFlag: never;
}

export interface GetName<Name extends string> {
  /**
   * 模型名称。请在定义模型时确保是唯一的字符串
   */
  readonly name: Name;
}

export interface GetState<State extends object> {
  /**
   * 模型的实时状态
   */
  readonly state: State;
}

export interface GetInitialState<State extends object> {
  /**
   * 模型的初始状态,每次获取该属性都会执行深拷贝操作
   */
  readonly initialState: State;
}

export type ModelPersist<State extends object, PersistDump> = {
  /**
   * 持久化版本号,数据结构变化后建议立即升级该版本。默认值:`0`
   */
  version?: number | string;

  /**
   * 持久化数据与初始数据的合并方式。默认值以全局配置为准
   *
   * - replace - 覆盖模式。直接用持久化数据替换初始数据
   * - merge - 合并模式。持久化数据与初始数据新增的key进行合并,可理解为`Object.assign`
   * - deep-merge - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
   *
   * 注意:当数据为数组格式时该配置无效。
   * @since 3.0.0
   */
  merge?: PersistMergeMode;
} & (
  | {
      /**
       * 模型数据从内存存储到持久化引擎时的过滤函数,允许你只持久化部分数据。
       * ```typescript
       *
       * // state = { firstName: 'tick', lastName: 'tock' }
       * dump: (state) => state
       * dump: (state) => state.firstName
       * dump: (state) => ({ name: state.lastName })
       * ```
       *
       * @since 3.0.0
       */
      dump: (state: State) => PersistDump;
      /**
       * 持久化数据恢复到模型内存时的过滤函数,参数为`dump`返回的值。
       * ```typescript
       * // state = { firstName: 'tick', lastName: 'tock' }
       * {
       *   dump(state) {
       *     return state.firstName
       *   },
       *   load(firstName) {
       *     return { ...this.initialState, firstName: firstName };
       *   }
       * }
       * ```
       *
       * @since 3.0.0
       */
      load: (this: GetInitialState<State>, dumpData: PersistDump) => State;
    }
  | {
      dump?: never;
      load?: never;
    }
);

export interface ActionCtx<State extends object>
  extends GetName<string>,
    GetInitialState<State> {}

export interface EffectCtx<State extends object>
  extends ActionCtx<State>,
    GetState<State> {
  /**
   * 立即更改状态,支持**immer**操作
   *
   * ```typescript
   * this.setState((state) => {
   *   state.count += 1;
   * });
   * ```
   *
   * 对于object类型,你可以直接传递 **全部** 或者 **部分** 数据
   * ```typescript
   * interface State { id: number; name: string };
   *
   * this.setState({}); // 什么也没修改
   * this.setState({ id: 10 }); // 只修改id
   * this.setState({ id: 10, name: 'foo' }); // 修改全部
   *
   * this.setState((state) => {
   *   return {}; // 什么也没修改
   * });
   * this.setState((state) => {
   *   return { id: 10 }; // 只修改id
   * });
   * this.setState((state) => {
   *   return { id: 10, name: 'foo' }; // 修改全部
   * });
   * ```
   *
   * 对于array类型,直接传递数组就行了
   * ```typescript
   * this.setState(['a', 'b', 'c']);
   * ```
   */
  readonly setState: State extends any[]
    ? (state: State | ((state: State) => State | void)) => UnknownAction
    : <K extends keyof State>(
        state: SetStateCallback<State, K> | (Pick<State, K> | State),
      ) => UnknownAction;
}

export interface SetStateCallback<State extends object, K extends keyof State> {
  (state: State): Pick<State, K> | State | void;
}

export interface ComputedCtx<State extends object>
  extends GetName<string>,
    GetState<State> {}

export interface BaseModel<Name extends string, State extends object>
  extends GetState<State>,
    GetName<Name> {}

type ModelActionItem<
  State extends object,
  Action extends object,
  K extends keyof Action,
> = Action[K] extends (state: State, ...args: infer P) => State | void
  ? (...args: P) => UnknownAction
  : never;

type ModelAction<State extends object, Action extends object> = {
  readonly [K in keyof Action]: ModelActionItem<State, Action, K>;
};

type GetPrivateMethodKeys<Method extends object> = {
  [K in keyof Method]: K extends `_${string}` ? K : never;
}[keyof Method];

type ModelEffect<Effect extends object> = {
  readonly [K in keyof Effect]: Effect[K] extends (...args: infer P) => infer R
    ? EnhancedEffect<P, R>
    : never;
};

type ModelComputed<Computed extends object> = {
  readonly [K in keyof Computed]: Computed[K] & ComputedFlag;
};

export type Model<
  Name extends string = string,
  State extends object = object,
  Action extends object = object,
  Effect extends object = object,
  Computed extends object = object,
> = BaseModel<Name, State> &
  // [K in keyof Action as K extends `_${string}` ? never : K]
  // 上面这种看起来简洁,业务代码提示也正常,但是业务代码那边无法点击跳转进模型了。
  // 所以需要先转换所有的属性,再把私有属性去除。
  Omit<ModelAction<State, Action>, GetPrivateMethodKeys<Action>> &
  Omit<ModelEffect<Effect>, GetPrivateMethodKeys<Effect>> &
  Omit<ModelComputed<Computed>, GetPrivateMethodKeys<Computed>>;

export type InternalModel<
  Name extends string = string,
  State extends object = object,
  Action extends object = object,
  Effect extends object = object,
  Computed extends object = object,
> = BaseModel<Name, State> & {
  readonly _$opts: DefineModelOptions<State, Action, Effect, Computed, any>;
  readonly _$persistCtx: GetInitialState<State>;
};

export type InternalAction<State extends object> = {
  [key: string]: (state: State, ...args: any[]) => State | void;
};

export interface Event<State> {
  /**
   * store初始化完成,并且持久化(如果有)的数据也已经恢复。
   *
   * 上下文 **this** 可以直接调用actions和effects的函数以及computed计算属性。
   */
  onInit?: () => void;
  /**
   * 每当state有变化时的回调通知。
   *
   * 初始化(onInit)执行之前不会触发该回调。如果在onInit中做了修改state的操作,则会触发该回调。
   *
   * 上下文 **this** 可以直接调用actions和effects的函数以及computed计算属性,请谨慎执行修改数据的操作以防止死循环。
   */
  onChange?: (prevState: State, nextState: State) => void;
  /**
   * 销毁模型时的回调通知,此时模型已经被销毁。
   * 该事件仅在局部模型生效
   * @see useIsolate
   */
  onDestroy?: (this: never, modelName: string) => void;
}

export interface EventCtx<State extends object>
  extends GetName<string>,
    GetState<State> {}

export interface DefineModelOptions<
  State extends object,
  Action extends object,
  Effect extends object,
  Computed extends object,
  PersistDump,
> {
  /**
   * 初始状态
   *
   * ```typescript
   * cosnt initialState: {
   *   count: number;
   * } = {
   *   count: 0,
   * }
   *
   * const model = defineModel('model1', {
   *   initialState
   * });
   * ```
   */
  initialState: State;
  /**
   * 定义修改状态的方法。参数一自动推断为state类型。支持**immer**操作。支持多参数。
   *
   * ```typescript
   * const model = defineModel('model1', {
   *   initialState,
   *   reducers: {
   *     plus(state, step: number) {
   *       state.count += step;
   *     },
   *     minus(state, step: number, scale: 1 | 2) {
   *       state.count -= step * scale;
   *     }
   *   },
   * });
   * ```
   */
  reducers?: Action & InternalAction<State> & ThisType<ActionCtx<State>>;
  /**
   * 定义普通方法,异步方法等。
   * 调用effect方法时,一般会伴随异步操作(请求数据、耗时任务),框架会自动收集当前方法的调用状态。
   *
   * ```typescript
   * const model = defineModel('model1', {
   *   initialState,
   *   methods: {
   *     async foo(p1: string, p2: number) {
   *       const result = await Promise.resolve();
   *       this.setState({ x: result });
   *       return 'OK';
   *     }
   *   },
   * });
   *
   * useLoading(model.foo); // 返回值类型: boolean
   * ```
   */
  methods?: Effect &
    ThisType<ModelAction<State, Action> & Effect & Computed & EffectCtx<State>>;
  /**
   * 定义计算属性。针对需要复杂的计算才能得出结果的场景而设计。如果只是简单的返回,建议使用`methods`
   *
   * ```typescript
   * const initialState = { firstName: 'tick', lastName: 'tock' };
   *
   * const model = defineModel('model1', {
   *   initialState,
   *   computed: {
   *     fullname() {
   *       return this.state.firstName + '.' + this.state.lastName;
   *     },
   *     names() {
   *       return this.fullName.value.split('').map((item) => `[${item}]`);
   *     }
   *   },
   * });
   * ```
   *
   * 可以单独使用:
   * ```typescript
   * model.fullname; // ComputedRef<string>;
   * model.fullname.value; // string;
   * ```
   *
   * 可以配合react hooks使用:
   *
   * ```typescript
   * const fullname = useComputed(model.fullname); // string
   * ```
   */
  computed?: Computed & ThisType<Computed & ComputedCtx<State>>;
  /**
   * 是否阻止刷新数据时跳过当前模型,默认即不跳过。
   *
   * 如果是强制刷新,则该参数无效。
   *
   * @see store.refresh(force: boolean = false)
   */
  skipRefresh?: boolean;
  /**
   * 定制持久化,请确保已经在初始化store的时候把当前模型加入persist配置,否则当前设置无效
   *
   * @see store.init()
   */
  persist?: ModelPersist<State, PersistDump> & ThisType<null>;
  /**
   * 生命周期
   * @since 0.11.1
   */
  events?: Event<State> &
    ThisType<ModelAction<State, Action> & Computed & Effect & EventCtx<State>>;
}


================================================
FILE: src/persist/persist-gate.tsx
================================================
import { ReactNode, FC, useState, useEffect } from 'react';
import { modelStore } from '../store/model-store';
import { isFunction } from '../utils/is-type';

export interface PersistGateProps {
  loading?: ReactNode;
  children?: ReactNode | ((isReady: boolean) => ReactNode);
}

export const PersistGate: FC<PersistGateProps> = (props) => {
  const state = useState(() => modelStore.isReady),
    isReady = state[0],
    setIsReady = state[1];
  const { loading = null, children } = props;

  useEffect(() => {
    isReady ||
      modelStore.onInitialized().then(() => {
        setIsReady(true);
      });
  }, []);

  /* istanbul ignore else -- @preserve */
  if (process.env.NODE_ENV !== 'production') {
    if (loading && isFunction(children)) {
      console.error('[PersistGate] 当前children为函数类型,loading属性无效');
    }
  }

  return (
    <>
      {isFunction(children) ? children(isReady) : isReady ? children : loading}
    </>
  );
};


================================================
FILE: src/persist/persist-item.ts
================================================
import type { StorageEngine } from '../engines/storage-engine';
import type {
  GetInitialState,
  InternalModel,
  Model,
  ModelPersist,
} from '../model/types';
import { isObject, isPlainObject, isString } from '../utils/is-type';
import { toPromise } from '../utils/to-promise';
import { parseState, stringifyState } from '../utils/serialize';

export interface PersistSchema {
  /**
   * 版本
   */
  v: number | string;
  /**
   * 数据
   */
  d: {
    [key: string]: PersistItemSchema;
  };
}

export interface PersistItemSchema {
  /**
   * 版本
   */
  v: number | string;
  /**
   * 数据
   */
  d: string;
}

export type PersistMergeMode = 'replace' | 'merge' | 'deep-merge';

export interface PersistOptions {
  /**
   * 存储唯一标识名称
   */
  key: string;
  /**
   * 存储名称前缀,默认值:`@@foca.persist:`
   */
  keyPrefix?: string;
  /**
   * 持久化数据与初始数据的合并方式。默认值:`merge`
   *
   * - replace - 覆盖模式。数据从存储引擎取出后直接覆盖初始数据
   * - merge - 合并模式。数据从存储引擎取出后,与初始数据多余部分进行合并,可以理解为`Object.assign()`操作
   * - deep-merge - 二级合并模式。在合并模式的基础上,如果某个key的值为对象,则该对象也会执行合并操作
   *
   * 注意:当数据为数组格式时该配置无效。
   * @since 3.0.0
   */
  merge?: PersistMergeMode;
  /**
   * 版本号
   */
  version: string | number;
  /**
   * 存储引擎
   */
  engine: StorageEngine;
  /**
   * 允许持久化的模型列表
   */
  models: Model[];
}

type CustomModelPersistOptions = Required<ModelPersist<object, any>> & {
  ctx: GetInitialState<object>;
};

const defaultDumpOrLoadFn = (value: any) => value;

interface PersistRecord {
  model: Model;
  /**
   * 模型的persist参数
   */
  opts: CustomModelPersistOptions;
  /**
   * 已经存储的模型内容,data是字符串。
   * 存储时,如果各项属性符合条件,则会当作最终值,从而省去了系列化的过程。
   */
  schema?: PersistItemSchema;
  /**
   * 已经存储的模型内容,data是对象。
   * 主要用于和store变化后的state对比。
   */
  prev?: object;
}

export class PersistItem {
  readonly key: string;

  protected readonly records: Record<string, PersistRecord> = {};

  constructor(protected readonly options: PersistOptions) {
    const {
      models,
      keyPrefix = '@@foca.persist:',
      key,
      merge = 'merge' satisfies PersistMergeMode,
    } = options;

    this.key = keyPrefix + key;

    for (let i = models.length; i-- > 0; ) {
      const model = models[i]!;
      const {
        load = defaultDumpOrLoadFn,
        dump = defaultDumpOrLoadFn,
        version: customVersion = 0,
        merge: customMerge = merge,
      } = (model as unknown as InternalModel)._$opts.persist || {};

      this.records[model.name] = {
        model,
        opts: {
          version: customVersion,
          merge: customMerge,
          load,
          dump,
          ctx: (model as unknown as InternalModel)._$persistCtx,
        },
      };
    }
  }

  init(): Promise<void> {
    return toPromise(() => this.options.engine.getItem(this.key)).then(
      (data) => {
        if (!data) {
          this.loadMissingState();
          return this.dump();
        }

        try {
          const schema = JSON.parse(data);

          if (!this.validateSchema(schema)) {
            this.loadMissingState();
            return this.dump();
          }

          const schemaKeys = Object.keys(schema.d);
          for (let i = schemaKeys.length; i-- > 0; ) {
            const key = schemaKeys[i]!;
            const record = this.records[key];

            if (record) {
              const { opts } = record;
              const itemSchema = schema.d[key]!;
              if (this.validateItemSchema(itemSchema, opts)) {
                const dumpData = parseState(itemSchema.d);
                record.prev = this.merge(
                  opts.load.call(opts.ctx, dumpData),
                  opts.ctx.initialState,
                  opts.merge,
                );
                record.schema = itemSchema;
              }
            }
          }

          this.loadMissingState();
          return this.dump();
        } catch (e) {
          this.dump();
          throw e;
        }
      },
    );
  }

  loadMissingState() {
    this.loop((record) => {
      const { prev, opts, schema } = record;
      if (!schema || !prev) {
        const dumpData = opts.dump.call(null, opts.ctx.initialState);
        record.prev = this.merge(
          opts.load.call(opts.ctx, dumpData),
          opts.ctx.initialState,
          opts.merge,
        );
        record.schema = {
          v: opts.version,
          d: stringifyState(dumpData),
        };
      }
    });
  }

  merge(persistState: any, initialState: any, mode: PersistMergeMode) {
    const isStateArray = Array.isArray(persistState);
    const isInitialStateArray = Array.isArray(initialState);
    if (isStateArray && isInitialStateArray) return persistState;
    if (isStateArray || isInitialStateArray) return initialState;

    if (mode === 'replace') return persistState;

    const state = Object.assign({}, initialState, persistState);

    if (mode === 'deep-merge') {
      const keys = Object.keys(persistState);
      for (let i = 0; i < keys.length; ++i) {
        const key = keys[i]!;
        if (
          Object.prototype.hasOwnProperty.call(initialState, key) &&
          isPlainObject(state[key]) &&
          isPlainObject(initialState[key])
        ) {
          state[key] = Object.assign({}, initialState[key], state[key]);
        }
      }
    }

    return state;
  }

  collect(): Record<string, object> {
    const stateMaps: Record<string, object> = {};

    this.loop(({ prev: state }, key) => {
      state && (stateMaps[key] = state);
    });

    return stateMaps;
  }

  update(nextState: Record<string, object>) {
    let changed = false;

    this.loop((record) => {
      const { model, prev, opts, schema } = record;
      const nextStateForKey = nextState[model.name]!;

      // 状态不变的情况下,即使过期了也无所谓,下次初始化时会自动剔除。
      // 版本号改动的话一定会触发页面刷新。
      if (nextStateForKey !== prev) {
        record.prev = nextStateForKey;
        const nextSchema: PersistItemSchema = {
          v: opts.version,
          d: stringifyState(opts.dump.call(null, nextStateForKey)),
        };

        if (!schema || nextSchema.d !== schema.d) {
          record.schema = nextSchema;
          changed ||= true;
        }
      }
    });

    changed && this.dump();
  }

  protected loop(callback: (record: PersistRecord, key: string) => void) {
    const records = this.records;
    const recordKeys = Object.keys(records);
    for (let i = recordKeys.length; i-- > 0; ) {
      const key = recordKeys[i]!;
      callback(records[key]!, key);
    }
  }

  protected dump() {
    this.options.engine.setItem(this.key, JSON.stringify(this.toJSON()));
  }

  protected validateSchema(schema: any): schema is PersistSchema {
    return (
      isObject<PersistSchema>(schema) &&
      isObject<PersistSchema['d']>(schema.d) &&
      schema.v === this.options.version
    );
  }

  protected validateItemSchema(
    schema: PersistItemSchema | undefined,
    options: CustomModelPersistOptions,
  ) {
    return schema && schema.v === options.version && isString(schema.d);
  }

  protected toJSON(): PersistSchema {
    const states: PersistSchema['d'] = {};

    this.loop(({ schema }, key) => {
      schema && (states[key] = schema);
    });

    return { v: this.options.version, d: states };
  }
}


================================================
FILE: src/persist/persist-manager.ts
================================================
import type { Reducer, Store, Unsubscribe } from 'redux';
import { actionHydrate, isHydrateAction } from '../actions/persist';
import { PersistItem, PersistOptions } from './persist-item';

export class PersistManager {
  protected initialized: boolean = false;
  protected readonly list: PersistItem[];
  protected timer?: ReturnType<typeof setTimeout>;
  protected unsubscribeStore!: Unsubscribe;

  constructor(options: PersistOptions[]) {
    this.list = options.map((option) => new PersistItem(option));
  }

  init(store: Store, hydrate: boolean) {
    this.unsubscribeStore = store.subscribe(() => {
      this.initialized && this.update(store);
    });

    return Promise.all(this.list.map((item) => item.init())).then(() => {
      hydrate && store.dispatch(actionHydrate(this.collect()));
      this.initialized = true;
    });
  }

  destroy() {
    this.unsubscribeStore();
    this.initialized = false;
  }

  collect(): Record<string, object> {
    return this.list.reduce<Record<string, object>>((stateMaps, item) => {
      return Object.assign(stateMaps, item.collect());
    }, {});
  }

  combineReducer(original: Reducer): Reducer<Record<string, object>> {
    return (state, action) => {
      if (state === void 0) state = {};

      if (isHydrateAction(action)) {
        return Object.assign({}, state, action.payload);
      }

      return original(state, action);
    };
  }

  protected update(store: Store) {
    this.timer ||= setTimeout(() => {
      const nextState = store.getState();
      this.timer = void 0;
      for (let i = this.list.length; i-- > 0; ) {
        this.list[i]!.update(nextState);
      }
    }, 50);
  }
}


================================================
FILE: src/reactive/computed-value.ts
================================================
import type { Store } from 'redux';
import { depsCollector } from './deps-collector';
import { createComputedDeps } from './create-computed-deps';
import type { Deps } from './object-deps';

export class ComputedValue<T = any> {
  public deps: Deps[] = [];
  public snapshot: any;

  protected active?: boolean;
  protected root?: any;

  constructor(
    protected readonly store: Pick<Store<Record<string, any>>, 'getState'>,
    public readonly model: string,
    public readonly property: string,
    protected readonly fn: () => any,
  ) {}

  public get value(): T {
    if (this.active) {
      throw new Error(
        `[model:${this.model}] 计算属性"${this.property}"正在被循环引用`,
      );
    }

    this.active = true;
    this.isDirty() && this.updateSnapshot();
    this.active = false;

    if (depsCollector.active) {
      // 作为其他computed的依赖
      depsCollector.prepend(createComputedDeps(this));
    }

    return this.snapshot;
  }

  isDirty(): boolean {
    if (!this.root) return true;

    const rootState = this.store.getState();

    if (this.root !== rootState) {
      const deps = this.deps;
      // 前置的元素是createComputedDeps()生成的对象,执行isDirty()会触发ref.value
      // 后置的元素是state,所以需要从后往前判断
      for (let i = deps.length; i-- > 0; ) {
        if (deps[i]!.isDirty()) return true;
      }
    }

    this.root = rootState;
    return false;
  }

  protected updateSnapshot() {
    this.deps = depsCollector.produce(() => {
      this.snapshot = this.fn();
      this.root = this.store.getState();
    });
  }
}


================================================
FILE: src/reactive/create-computed-deps.ts
================================================
import { shallowEqual } from 'react-redux';
import type { ComputedValue } from './computed-value';
import type { Deps } from './object-deps';

export const createComputedDeps = (body: ComputedValue): Deps => {
  let snapshot: any;

  return {
    id: `c-${body.model}-${body.property}`,
    end(): void {
      snapshot = body.snapshot;
    },
    isDirty(): boolean {
      return !shallowEqual(snapshot, body.value);
    },
  };
};


================================================
FILE: src/reactive/deps-collector.ts
================================================
import type { Deps } from './object-deps';

const deps: Deps[][] = [];
let level = -1;

export const depsCollector = {
  get active(): boolean {
    return level >= 0;
  },
  produce(callback: Function): Deps[] {
    const current: Deps[] = (deps[++level] = []);
    callback();
    deps.length = level--;

    const uniqueDeps: Deps[] = [];
    const uniqueID: string[] = [];

    for (let i = 0; i < current.length; ++i) {
      const dep = current[i]!;
      const id = dep.id;
      if (uniqueID.indexOf(id) === -1) {
        uniqueID.push(id);
        uniqueDeps.push(dep);
        dep.end();
      }
    }

    return uniqueDeps;
  },
  append(dep: Deps) {
    deps[level]!.push(dep);
  },
  prepend(dep: Deps) {
    deps[level]!.unshift(dep);
  },
};


================================================
FILE: src/reactive/object-deps.ts
================================================
import type { Store } from 'redux';
import { isObject } from '../utils/is-type';
import { depsCollector } from './deps-collector';

export interface Deps {
  id: string;
  end(): void;
  isDirty(): boolean;
}

export class ObjectDeps<T = any> implements Deps {
  protected active: boolean = true;
  protected snapshot: any;
  protected root: any;

  constructor(
    protected readonly store: Pick<Store<Record<string, any>>, 'getState'>,
    protected readonly model: string,
    protected readonly deps: string[] = [],
  ) {
    this.root = this.getState();
  }

  isDirty(): boolean {
    const rootState = this.getState();
    if (this.root === rootState) return false;
    const { pathChanged, snapshot: nextSnapshot } = this.getSnapshot(rootState);
    if (pathChanged || this.snapshot !== nextSnapshot) return true;
    this.root = rootState;
    return false;
  }

  get id(): string {
    return this.model + '.' + this.deps.join('.');
  }

  start<T extends Record<string, any>>(startState: T): T {
    depsCollector.append(this);
    return this.proxy(startState);
  }

  end(): void {
    this.active = false;
  }

  protected getState(): T {
    return this.store.getState()[this.model];
  }

  protected getSnapshot(state: any): { pathChanged: boolean; snapshot: any } {
    const deps = this.deps;
    let snapshot = state;
    for (let i = 0; i < deps.length; ++i) {
      if (!isObject<Record<string, any>>(snapshot)) {
        return { pathChanged: true, snapshot };
      }
      snapshot = snapshot[deps[i]!];
    }

    return { pathChanged: false, snapshot };
  }

  protected proxy(currentState: Record<string, any>): any {
    if (
      currentState === null ||
      !isObject<Record<string, any>>(currentState) ||
      Array.isArray(currentState)
    ) {
      return currentState;
    }

    const nextState = {};
    const keys = Object.keys(currentState);
    const currentDeps = this.deps.slice();
    let visited = false;

    for (let i = keys.length; i-- > 0; ) {
      const key = keys[i]!;

      Object.defineProperty(nextState, key, {
        enumerable: true,
        get: () => {
          if (!this.active) return currentState[key];

          if (visited) {
            return new ObjectDeps(
              this.store,
              this.model,
              currentDeps.slice(),
            ).start(currentState)[key];
          }

          visited = true;
          this.deps.push(key);
          return this.proxy((this.snapshot = currentState[key]));
        },
      });
    }

    /* istanbul ignore else -- @preserve */
    if (process.env.NODE_ENV !== 'production') {
      Object.freeze(nextState);
    }

    return nextState;
  }
}


================================================
FILE: src/redux/connect.ts
================================================
import { Connect, connect as originalConnect } from 'react-redux';
import { ProxyContext } from './contexts';
import { toArgs } from '../utils/to-args';

export const connect: Connect = function () {
  const args = toArgs<Parameters<Connect>>(arguments);
  (args[3] ||= {}).context = ProxyContext;

  return originalConnect.apply(null, args);
};


================================================
FILE: src/redux/contexts.ts
================================================
import { createContext } from 'react';
import type { ReactReduxContextValue } from 'react-redux';

export const ModelContext = createContext<ReactReduxContextValue | null>(null);

export const LoadingContext = createContext<ReactReduxContextValue | null>(
  null,
);

export const ProxyContext = createContext<ReactReduxContextValue | null>(null);


================================================
FILE: src/redux/create-reducer.ts
================================================
import type { Reducer } from 'redux';
import { isPostModelAction } from '../actions/model';
import { isRefreshAction } from '../actions/refresh';

interface Options<State extends object> {
  readonly name: string;
  readonly initialState: State;
  readonly allowRefresh: boolean;
}

export const createReducer = <State extends object>(
  options: Options<State>,
): Reducer<State> => {
  const allowRefresh = options.allowRefresh;
  const reducerName = options.name;
  const initialState = options.initialState;

  return function reducer(state, action) {
    if (state === void 0) return initialState;

    if (isPostModelAction<State>(action) && action.model === reducerName) {
      return action.next;
    }

    if (isRefreshAction(action) && (allowRefresh || action.payload.force)) {
      return initialState;
    }

    return state;
  };
};


================================================
FILE: src/redux/foca-provider.tsx
================================================
import { FC } from 'react';
import { Provider } from 'react-redux';
import { ProxyContext, ModelContext, LoadingContext } from './contexts';
import { modelStore } from '../store/model-store';
import { PersistGate, PersistGateProps } from '../persist/persist-gate';
import { proxyStore } from '../store/proxy-store';
import { loadingStore } from '../store/loading-store';
import { isFunction } from '../utils/is-type';

interface OwnProps extends PersistGateProps {}

/**
 * 状态上下文组件,请挂载到入口文件。
 * 请确保您已经初始化了store仓库。
 *
 * @see store.init()
 *
 * ```typescript
 * ReactDOM.render(
 *   <FocaProvider>
 *     <App />
 *   </FocaProvider>
 * );
 * ```
 */
export const FocaProvider: FC<OwnProps> = ({ children, loading }) => {
  return (
    <Provider context={ProxyContext} store={proxyStore}>
      <Provider context={LoadingContext} store={loadingStore}>
        <Provider context={ModelContext} store={modelStore}>
          {modelStore['persister'] ? (
            <PersistGate loading={loading} children={children} />
          ) : isFunction(children) ? (
            children(true)
          ) : (
            children
          )}
        </Provider>
      </Provider>
    </Provider>
  );
};


================================================
FILE: src/redux/use-selector.ts
================================================
import { createSelectorHook } from 'react-redux';
import { ModelContext, LoadingContext } from './contexts';

export const useModelSelector = createSelectorHook(ModelContext);

export const useLoadingSelector = createSelectorHook(LoadingContext);


================================================
FILE: src/store/loading-store.ts
================================================
import {
  UnknownAction,
  applyMiddleware,
  legacy_createStore as createStore,
  Middleware,
} from 'redux';
import type { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';
import { loadingInterceptor } from '../middleware/loading.interceptor';
import { isDestroyLoadingAction, isLoadingAction } from '../actions/loading';
import { actionRefresh, isRefreshAction } from '../actions/refresh';
import { combine } from './proxy-store';
import { destroyLoadingInterceptor } from '../middleware/destroy-loading.interceptor';
import { immer } from '../utils/immer';
import { StoreBasic } from './store-basic';
import { modelStore } from './model-store';
import { freeze } from 'immer';
import { freezeStateMiddleware } from '../middleware/freeze-state.middleware';

export interface FindLoading {
  find(category: number | string): boolean;
}

interface LoadingState extends FindLoading {
  data: {
    [category: string]: boolean;
  };
}

interface LoadingStoreStateItem {
  loadings: LoadingState;
}

export type LoadingStoreState = Partial<{
  [model: string]: Partial<{
    [method: string]: LoadingStoreStateItem;
  }>;
}>;

const findLoading: FindLoading['find'] = function (
  this: LoadingState,
  category,
) {
  return !!this.data[category];
};

const createDefaultRecord = (): LoadingStoreStateItem => {
  return {
    loadings: {
      find: findLoading,
      data: {},
    },
  };
};

export class LoadingStore extends StoreBasic<LoadingStoreState> {
  protected initializingModels: string[] = [];
  protected status: Partial<{
    [model: string]: Partial<{
      [method: string]: boolean;
    }>;
  }> = {};
  protected defaultRecord: LoadingStoreStateItem = freeze(
    createDefaultRecord(),
    true,
  );

  constructor() {
    super();
    const topic = modelStore.topic;
    topic.subscribe('init', this.init.bind(this));
    topic.subscribe('refresh', this.refresh.bind(this));
    topic.subscribe('unmount', this.unmount.bind(this));
    topic.subscribe('modelPreInit', (modelName) => {
      this.initializingModels.push(modelName);
    });
    topic.subscribe('modelPostInit', (modelName) => {
      this.initializingModels = this.initializingModels.filter(
        (item) => item !== modelName,
      );
    });
  }

  init() {
    const middleware: Middleware[] = [
      loadingInterceptor(this),
      destroyLoadingInterceptor,
    ];

    /* istanbul ignore else -- @preserve */
    if (process.env.NODE_ENV !== 'production') {
      middleware.push(freezeStateMiddleware);
    }

    this.origin = createStore(
      this.reducer.bind(this),
      applyMiddleware.apply(null, middleware),
    );

    combine(this.store);
  }

  unmount(): void {
    this.origin = null;
  }

  reducer(
    state: LoadingStoreState | undefined,
    action: UnknownAction,
  ): LoadingStoreState {
    if (state === void 0) {
      state = {};
    }

    if (isLoadingAction(action)) {
      const {
        model,
        method,
        payload: { category, loading },
      } = action;
      const next = immer.produce(state, (draft) => {
        draft[model] ||= {};
        const { loadings } = (draft[model]![method] ||= createDefaultRecord());
        loadings.data[category] = loading;
      });

      return next;
    }

    if (isDestroyLoadingAction(action)) {
      const next = Object.assign({}, state);
      delete next[action.model];
      delete this.status[action.model];
      return next;
    }

    if (isRefreshAction(action)) return {};

    return state;
  }

  get(effect: PromiseEffect | PromiseRoomEffect): LoadingStoreStateItem {
    const {
      _: { model, method },
    } = effect;
    let record: LoadingStoreStateItem | undefined;

    if (this.isActive(model, method)) {
      record = this.getItem(model, method);
    } else {
      this.activate(model, method);
    }

    return record || this.defaultRecord;
  }

  getItem(model: string, method: string): LoadingStoreStateItem | undefined {
    const level1 = this.getState()[model];
    return level1 && level1[method];
  }

  isModelInitializing(model: string): boolean {
    return (
      this.initializingModels.length > 0 &&
      this.initializingModels.includes(model)
    );
  }

  isActive(model: string, method: string): boolean {
    const level1 = this.status[model];
    return level1 !== void 0 && level1[method] === true;
  }

  activate(model: string, method: string) {
    (this.status[model] ||= {})[method] = true;
  }

  inactivate(model: string, method: string) {
    (this.status[model] ||= {})[method] = false;
  }

  refresh() {
    return this.dispatch(actionRefresh(true));
  }
}

export const loadingStore = new LoadingStore();


================================================
FILE: src/store/model-store.ts
================================================
import {
  applyMiddleware,
  compose,
  legacy_createStore as createStore,
  Middleware,
  Reducer,
  Store,
  StoreEnhancer,
} from 'redux';
import { Topic } from 'topic';
import { actionRefresh, RefreshAction } from '../actions/refresh';
import { modelInterceptor } from '../middleware/model.interceptor';
import type { PersistOptions } from '../persist/persist-item';
import { PersistManager } from '../persist/persist-manager';
import { combine } from './proxy-store';
import { OBJECT } from '../utils/is-type';
import { StoreBasic } from './store-basic';
import { actionInActionInterceptor } from '../middleware/action-in-action.interceptor';
import { freezeStateMiddleware } from '../middleware/freeze-state.middleware';

type Compose =
  | typeof compose
  | ((...funcs: StoreEnhancer<any>[]) => StoreEnhancer<any>);

interface CreateStoreOptions {
  preloadedState?: Record<string, any>;
  compose?: 'redux-devtools' | Compose;
  middleware?: Middleware[];
  persist?: PersistOptions[];
}

export class ModelStore extends StoreBasic<Record<string, any>> {
  public topic: Topic<{
    init: [];
    ready: [];
    refresh: [];
    unmount: [];
    modelPreInit: [modelName: string];
    modelPostInit: [modelName: string];
  }> = new Topic();
  protected _isReady: boolean = false;
  protected consumers: Record<string, Reducer> = {};
  protected reducerKeys: string[] = [];
  protected persister: PersistManager | null = null;

  protected reducer!: Reducer;

  constructor() {
    super();
    this.topic.keep('ready', () => this._isReady);
  }

  get isReady(): boolean {
    return this._isReady;
  }

  init(options: CreateStoreOptions = {}) {
    const prevStore = this.origin;
    const firstInitialize = !prevStore;

    if (!firstInitialize) {
      if (process.env.NODE_ENV === 'production') {
        throw new Error(`[store] 请勿多次执行'store.init()'`);
      }
    }

    this._isReady = false;
    this.reducer = this.combineReducers();

    const persistOptions = options.persist;
    let persister = this.persister;
    persister && persister.destroy();
    if (persistOptions && persistOptions.length) {
      persister = this.persister = new PersistManager(persistOptions);
      this.reducer = persister.combineReducer(this.reducer);
    } else {
      persister = this.persister = null;
    }

    let store: Store;

    if (firstInitialize) {
      const middleware = (options.middleware || []).concat(modelInterceptor);
      /* istanbul ignore else -- @preserve */
      if (process.env.NODE_ENV !== 'production') {
        middleware.unshift(actionInActionInterceptor);
        middleware.push(freezeStateMiddleware);
      }

      const enhancer = applyMiddleware.apply(null, middleware);

      store = this.origin = createStore(
        this.reducer,
        options.preloadedState,
        this.getCompose(options.compose)(enhancer),
      );
      this.topic.publish('init');

      combine(store);
    } else {
      // 重新创建store会导致组件里的subscription都失效
      store = prevStore;
      store.replaceReducer(this.reducer);
    }

    if (persister) {
      persister.init(store, firstInitialize).then(() => {
        this.ready();
      });
    } else {
      this.ready();
    }

    return this;
  }

  refresh(force: boolean = false): RefreshAction {
    const action = this.dispatch(actionRefresh(force));
    this.topic.publish('refresh');
    return action;
  }

  unmount() {
    this.origin = null;
    this._isReady = false;
    this.topic.publish('unmount');
  }

  onInitialized(maybeSync?: () => void): Promise<void> {
    return new Promise((resolve) => {
      if (this._isReady) {
        maybeSync && maybeSync();
        resolve();
      } else {
        this.topic.subscribeOnce('ready', () => {
          maybeSync && maybeSync();
          resolve();
        });
      }
    });
  }

  protected ready() {
    this._isReady = true;
    this.topic.publish('ready');
  }

  protected getCompose(customCompose: CreateStoreOptions['compose']): Compose {
    if (customCompose === 'redux-devtools') {
      /* istanbul ignore if -- @preserve */
      if (process.env.NODE_ENV !== 'production') {
        return (
          /** @ts-expect-error */
          (typeof window === OBJECT
            ? window
            : typeof global === OBJECT
              ? global
              : {})['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose
        );
      }

      return compose;
    }

    return customCompose || compose;
  }

  protected combineReducers(): Reducer<Record<string, object>> {
    return (state, action) => {
      if (state === void 0) {
        state = {};
      }

      const reducerKeys = this.reducerKeys;
      const consumers = this.consumers;
      const keyLength = reducerKeys.length;
      const nextState: Record<string, any> = {};
      let hasChanged = false;
      let i = keyLength;

      while (i-- > 0) {
        const key = reducerKeys[i]!;
        const prevForKey = state[key];
        const nextForKey = (nextState[key] = consumers[key]!(
          prevForKey,
          action,
        ));
        hasChanged ||= nextForKey !== prevForKey;
      }

      return hasChanged || keyLength !== Object.keys(state).length
        ? nextState
        : state;
    };
  }

  protected appendReducer(key: string, consumer: Reducer): void {
    const store = this.origin;
    const consumers = this.consumers;
    const exists = store && consumers.hasOwnProperty(key);

    consumers[key] = consumer;
    this.reducerKeys = Object.keys(consumers);
    store && !exists && store.replaceReducer(this.reducer);
  }

  protected removeReducer(key: string): void {
    const store = this.origin;
    const consumers = this.consumers;

    if (consumers.hasOwnProperty(key)) {
      delete consumers[key];
      this.reducerKeys = Object.keys(consumers);
      store && store.replaceReducer(this.reducer);
    }
  }
}

export const modelStore = new ModelStore();


================================================
FILE: src/store/proxy-store.ts
================================================
import { legacy_createStore as createStore, Store } from 'redux';

export const proxyStore = createStore(() => ({}));

const dispatch = () => {
  proxyStore.dispatch({
    type: '-',
  });
};

/**
 * 为了触发connect(),需要将实体store都注册到代理的store。
 */
export const combine = (otherStore: Store) => {
  otherStore.subscribe(dispatch);
};


================================================
FILE: src/store/store-basic.ts
================================================
import { Store } from 'redux';
import { $$observable } from '../utils/symbol-observable';

export abstract class StoreBasic<T> implements Store<T> {
  protected origin: Store<T> | null = null;

  /**
   * @deprecated 请勿使用该方法,因为它其实没有被实现
   */
  declare replaceReducer: Store<T>['replaceReducer'];

  dispatch: Store<T>['dispatch'] = (action) => {
    return this.store.dispatch(action);
  };

  getState: Store<T>['getState'] = () => {
    return this.store.getState();
  };

  subscribe: Store<T>['subscribe'] = (listener) => {
    return this.store.subscribe(listener);
  };

  [$$observable]: Store<T>[typeof $$observable] = () => {
    return this.store[$$observable]();
  };

  protected get store(): Store<T> {
    if (!this.origin) {
      throw new Error(`[store] 当前无实例,忘记执行'store.init()'了吗?`);
    }
    return this.origin;
  }

  abstract init(): void;
  abstract unmount(): void;
}


================================================
FILE: src/utils/deep-equal.ts
================================================
import { OBJECT } from './is-type';

export const deepEqual = (a: any, b: any): boolean => {
  if (a === b) return true;

  if (a && b && typeof a == OBJECT && typeof b == OBJECT) {
    if (a.constructor !== b.constructor) return false;

    let i: number;
    let len: number;
    let key: string;

    if (Array.isArray(a)) {
      len = a.length;

      if (len != b.length) return false;

      for (i = len; i-- > 0; ) {
        if (!deepEqual(a[i], b[i])) return false;
      }

      return true;
    }

    const keys = Object.keys(a);
    len = keys.length;

    if (len !== Object.keys(b).length) return false;

    for (i = len; i-- > 0; ) {
      if (!hasOwn.call(b, keys[i]!)) return false;
    }

    for (i = len; i-- > 0; ) {
      key = keys[i]!;
      if (!deepEqual(a[key], b[key])) return false;
    }

    return true;
  }

  return a !== a && b !== b;
};

const hasOwn = Object.prototype.hasOwnProperty;


================================================
FILE: src/utils/get-method-category.ts
================================================
export const getMethodCategory = (methodName: string) =>
  methodName.indexOf('_') === 0 ? 'internal' : 'external';


================================================
FILE: src/utils/getter.ts
================================================
import { toArgs } from './to-args';

export function composeGetter<
  T extends object,
  U1 extends (...args: any[]) => any,
>(obj: T, getter1: U1): T & ReturnType<U1>;

export function composeGetter<
  T extends object,
  U1 extends (...args: any[]) => any,
  U2 extends (...args: any[]) => any,
>(obj: T, getter1: U1, getter2: U2): T & ReturnType<U1> & ReturnType<U2>;

export function composeGetter<
  T extends object,
  U1 extends (...args: any[]) => any,
  U2 extends (...args: any[]) => any,
  U3 extends (...args: any[]) => any,
>(
  obj: T,
  getter1: U1,
  getter2: U2,
  getter3: U3,
): T & ReturnType<U1> & ReturnType<U2> & ReturnType<U3>;

export function composeGetter<
  T extends object,
  U1 extends (...args: any[]) => any,
  U2 extends (...args: any[]) => any,
  U3 extends (...args: any[]) => any,
  U4 extends (...args: any[]) => any,
>(
  obj: T,
  getter1: U1,
  getter2: U2,
  getter3: U3,
  getter4: U4,
): T & ReturnType<U1> & ReturnType<U2> & ReturnType<U3> & ReturnType<U4>;

export function composeGetter() {
  const args = toArgs<Function[]>(arguments);

  return args.reduce((carry, getter) => getter(carry), args.shift() as object);
}

export const defineGetter = (obj: object, key: string, get: () => any): any => {
  Object.defineProperty(obj, key, {
    get,
  });
  return obj;
};


================================================
FILE: src/utils/immer.ts
================================================
import { Immer, enableES5 } from 'immer';

/**
 * 支持ES5,毕竟Proxy无法polyfill。有些用户手机可以10年不换!!
 * @link https://immerjs.github.io/immer/docs/installation#pick-your-immer-version
 * @since immer@6.0
 */
enableES5();

export const immer = new Immer({
  autoFreeze: false,
});


================================================
FILE: src/utils/is-promise.ts
================================================
import { FUNCTION, isFunction, isObject } from './is-type';

const hasPromise = typeof Promise === FUNCTION;

export const isPromise = <T>(value: any): value is Promise<T> => {
  return (
    (hasPromise && value instanceof Promise) ||
    ((isObject(value) || isFunction(value)) && isFunction(value.then))
  );
};


================================================
FILE: src/utils/is-type.ts
================================================
export const OBJECT = 'object';
export const FUNCTION = 'function';

export const isFunction = <T extends Function>(value: any): value is T =>
  !!value && typeof value === FUNCTION;

export const isObject = <T extends object>(value: any): value is T =>
  !!value && typeof value === OBJECT;

export const isPlainObject = <T extends object>(value: any): value is T =>
  !!value && Object.prototype.toString.call(value) === '[object Object]';

export const isString = <T extends string>(value: any): value is T =>
  typeof value === 'string';


================================================
FILE: src/utils/serialize.ts
================================================
import { isObject } from './is-type';

const JSON_UNDEFINED = '__JSON_UNDEFINED__';

const replacer = (_key: string, value: any) => {
  return value === void 0 ? JSON_UNDEFINED : value;
};

const reviver = (_key: string, value: any) => {
  if (isObject<Record<string, any>>(value)) {
    const keys = Object.keys(value);
    for (let i = keys.length; i-- > 0; ) {
      const key = keys[i]!;
      if (value[key] === JSON_UNDEFINED) {
        value[key] = void 0;
      }
    }
  }

  return value;
};

export const stringifyState = (value: any) => {
  return JSON.stringify(value, replacer);
};

export const parseState = (value: string) => {
  return JSON.parse(
    value,
    value.indexOf(JSON_UNDEFINED) >= 0 ? reviver : void 0,
  );
};


================================================
FILE: src/utils/symbol-observable.ts
================================================
import { FUNCTION } from './is-type';

/**
 * Inlined version of the `symbol-observable` polyfill
 * @link https://github.com/reduxjs/redux/blob/master/src/utils/symbol-observable.ts
 */
export const $$observable: typeof Symbol.observable =
  (typeof Symbol === FUNCTION && Symbol.observable) ||
  ('@@observable' as unknown as typeof Symbol.observable);


================================================
FILE: src/utils/to-args.ts
================================================
const slice = Array.prototype.slice;

export const toArgs = <T = any[]>(args: IArguments, start?: number): T =>
  slice.call(args, start) as unknown as T;


================================================
FILE: src/utils/to-promise.ts
================================================
export const toPromise = <T>(fn: () => T | Promise<T>): Promise<T> => {
  return Promise.resolve().then(fn);
};


================================================
FILE: test/__snapshots__/serialize.test.ts.snap
================================================
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`can clone null and undefined 1`] = `"{"x":["__JSON_UNDEFINED__",1,{"test":"__JSON_UNDEFINED__","test1":"hello"},null,"__JSON_UNDEFINED__"],"y":null,"z":"__JSON_UNDEFINED__"}"`;

exports[`can clone null and undefined 2`] = `
{
  "x": [
    undefined,
    1,
    {
      "test": undefined,
      "test1": "hello",
    },
    null,
    undefined,
  ],
  "y": null,
  "z": undefined,
}
`;


================================================
FILE: test/action-in-action.test.tsx
================================================
import { act, render } from '@testing-library/react';
import { FC, useEffect, version } from 'react';
import sleep from 'sleep-promise';
import { defineModel, FocaProvider, store, useModel } from '../src';

const model = defineModel('aia' + Math.random(), {
  initialState: {
    open: false,
    count: 1,
  },
  reducers: {
    plus(state) {
      state.count += 1;
    },
    toggle(state) {
      state.open = !state.open;
    },
  },
});

const OtherComponent: FC = () => {
  useEffect(() => {
    model.plus();
  }, []);
  return null;
};

const App: FC = () => {
  const { open } = useModel(model);
  return <>{open && <OtherComponent />}</>;
};

test.runIf(version.split('.')[0] === '18').each([true, false])(
  `[legacy: %s] forceUpdate should not cause action in action error`,
  async (legacy) => {
    store.init();

    render(
      <FocaProvider>
        <App />
      </FocaProvider>,
      {
        legacyRoot: legacy,
      },
    );

    // console.error
    // Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.
    const spy = vitest.spyOn(console, 'error').mockImplementation(() => {});

    await expect(
      Promise.all(
        Array(3)
          .fill('')
          .map((_, i) =>
            act(async () => {
              await sleep(i * 2);
              model.toggle();
            }),
          ),
      ),
    ).resolves.toStrictEqual(Array(3).fill(void 0));

    // 等待useEffect
    await sleep(1000);
    store.unmount();
    spy.mockRestore();
  },
);


================================================
FILE: test/build.test.ts
================================================
import { writeFileSync } from 'fs';
import { execSync, exec } from 'child_process';

function testFile(filename: string, expectCode: number) {
  return new Promise((resolve) => {
    const child = exec(`node ${filename}`);
    child.on('exit', (code) => {
      try {
        expect(code).toBe(expectCode);
      } finally {
        resolve(code);
      }
    });
  });
}

beforeEach(() => {
  execSync('npx tsup');
}, 10000);

test('ESM with type=module', async () => {
  await testFile('dist/esm/index.js', 0);
});

test('ESM with type=commonjs', async () => {
  writeFileSync('dist/esm/package.json', '{"type": "commonjs"}');
  await testFile('dist/esm/index.js', 1);
});

test('pure commonjs', async () => {
  await testFile('dist/index.js', 0);
});


================================================
FILE: test/clone.test.ts
================================================
import { defineModel, cloneModel, store } from '../src';
import type { InternalModel } from '../src/model/types';
import { basicModel } from './models/basic.model';

let modelIndex = 0;

beforeEach(() => {
  store.init();
});

afterEach(() => {
  store.unmount();
});

test('Model can be cloned', async () => {
  const model = cloneModel('model' + ++modelIndex, basicModel);

  expect(model.state.hello).toBe('world');
  expect(model.state.count).toBe(0);

  model.plus(11);
  expect(model.state.count).toBe(11);

  await expect(model.foo('earth', 5)).resolves.toBe('OK');
  expect(model.state.hello).toBe('earth');
  expect(model.state.count).toBe(16);
});

test('Cloned model name', () => {
  const model = cloneModel('model' + ++modelIndex, basicModel);
  expect(model.name).toBe('model' + modelIndex);
});

test('Clone model with same name is invalid', () => {
  const model = defineModel(Date.now().toString(), { initialState: {} });
  expect(() => cloneModel(model.name, model)).toThrowError();
});

test('Override state', () => {
  const model = cloneModel('model' + ++modelIndex, basicModel, {
    initialState: {
      count: 20,
      hello: 'cat',
    },
  }) as unknown as InternalModel;

  expect(model._$opts.initialState).toStrictEqual({
    count: 20,
    hello: 'cat',
  });
});

test('Override persist', () => {
  const model1 = defineModel('model' + ++modelIndex, {
    initialState: {},
    persist: {
      dump: (state) => state,
      load: (state) => state,
    },
    methods: {
      cc() {
        return 3;
      },
    },
  });

  const model2 = cloneModel('model' + ++modelIndex, model1, {
    persist: {},
  }) as unknown as InternalModel;

  expect(model2._$opts.persist).not.toHaveProperty('maxAge');

  const model3 = cloneModel('model' + ++modelIndex, model1, (prev) => {
    return {
      persist: {
        ...prev.persist,
        maxAge: 30,
      },
    };
  }) as unknown as InternalModel;

  expect(model3._$opts.persist).toHaveProperty('maxAge');
  expect(model3._$opts.persist).toHaveProperty('dump');
  expect(model3._$opts.persist).toHaveProperty('load');
});

test('override methods or unknown option can cause error', () => {
  const model = defineModel('model' + ++modelIndex, { initialState: {} });

  expect(() =>
    cloneModel('a', model, {
      // @ts-expect-error
      reducers: {},
    }),
  ).toThrowError();

  expect(() =>
    cloneModel('b', model, {
      // @ts-expect-error
      methods: {},
    }),
  ).toThrowError();

  expect(() =>
    cloneModel('c', model, {
      // @ts-expect-error
      computed: {},
    }),
  ).toThrowError();

  expect(() =>
    cloneModel('d', model, {
      // @ts-expect-error
      whateverblabla: {},
    }),
  ).toThrowError();
});


================================================
FILE: test/computed.test.ts
================================================
import { defineModel, store } from '../src';
import { ComputedValue } from '../src/reactive/computed-value';
import { depsCollector } from '../src/reactive/deps-collector';
import { ObjectDeps } from '../src/reactive/object-deps';
import { computedModel } from './models/computed.model';

beforeEach(() => {
  store.init();
});

afterEach(() => {
  store.unmount();
});

test('Get computed value', () => {
  expect(computedModel.fullName()).toBe('ticktock');
  computedModel.changeFirstName('hello');
  expect(computedModel.fullName()).toBe('hellotock');
  computedModel.changeLastName('world');
  expect(computedModel.fullName()).toBe('helloworld');
});

test('Get computed value in computed function', () => {
  expect(computedModel.testDependentOtherComputed()).toBe('ticktock [online]');
});

test('Can return correct result from Object.keys', () => {
  expect(computedModel.testObjectKeys()).toMatchObject(
    expect.arrayContaining(['online', 'offline']),
  );
});

test('can enum all items for find method', () => {
  expect(computedModel.testFind()).toBe('offline');
});

test('can visit array item', () => {
  expect(computedModel.testVisitArray()[0]).toBe('online');
});

test('can return correct length from array', () => {
  expect(computedModel.testArrayLength()).toBe(2);
});

test('Can throw error with circularly reference', () => {
  const model = defineModel('computed-cycle-usage', {
    initialState: {},
    computed: {
      a() {
        this.b();
      },
      b() {
        this.c();
      },
      c() {
        this.a();
      },
    },
  });

  expect(() => model.a()).toThrowError('循环引用');
  expect(() => model.b()).toThrowError('循环引用');
  expect(() => model.c()).toThrowError('循环引用');
});

test('Can visit compute value from methods', () => {
  expect(computedModel.effectsGetFullName()).toBe('ticktock');
});

test('Split the deps for same getters', () => {
  let mockState = { a: { b: { c: 'd' } } };
  const mockStore = {
    getState() {
      return {
        x: mockState,
      };
    },
  };

  let deps = depsCollector.produce(() => {
    const proxy = new ObjectDeps(mockStore, 'x');
    const proxyState = proxy.start(mockState);
    proxyState.a.b;
    proxyState.a.b.c;
  });
  expect(deps).toHaveLength(2);

  deps = depsCollector.produce(() => {
    const proxy = new ObjectDeps(mockStore, 'x');
    const proxyState = proxy.start(mockState);
    proxyState.a.b;
  });
  expect(deps).toHaveLength(1);
});

test('Dirty deps never turn to clean', () => {
  let mockState = { a: { b: { c: 'd' } } };
  const mockStore = {
    getState() {
      return {
        x: mockState,
      };
    },
  };

  let deps = depsCollector.produce(() => {
    const proxy = new ObjectDeps(mockStore, 'x');
    const proxyState = proxy.start(mockState);
    proxyState.a.b;
    proxyState.a.b.c;
  });

  expect(deps[0]!.isDirty()).toBeFalsy();
  expect(deps[1]!.isDirty()).toBeFalsy();

  mockState = { a: { b: { c: 'd' } } };
  expect(deps[0]!.isDirty()).toBeTruthy();
  expect(deps[1]!.isDirty()).toBeFalsy();

  // @ts-expect-error
  mockState = { a: { b: 'm' } };
  expect(deps[0]!.isDirty()).toBeTruthy();
  expect(deps[1]!.isDirty()).toBeTruthy();

  mockState = { a: { b: { c: 'e' } } };
  expect(deps[0]!.isDirty()).toBeTruthy();
  expect(deps[1]!.isDirty()).toBeTruthy();

  mockState = { a: { b: { c: 'e' } } };
  expect(deps[0]!.isDirty()).toBeTruthy();
  expect(deps[1]!.isDirty()).toBeTruthy();
});

test('visit proxy state without collecting mode should not collect deps', () => {
  let mockState = { a: { b: { c: 'd' } } };
  const mockStore = {
    getState() {
      return {
        x: mockState,
      };
    },
  };

  const spy = vitest.spyOn(depsCollector, 'append');

  let proxyState!: typeof mockState;
  depsCollector.produce(() => {
    proxyState = new ObjectDeps(mockStore, 'x').start(mockState);
    proxyState.a;
    proxyState.a.b;
  });
  expect(spy).toHaveBeenCalledTimes(2);

  spy.mockClear();
  expect(proxyState.a).toMatchObject({
    b: {
      c: 'd',
    },
  });
  expect(proxyState.a.b).toMatchObject({
    c: 'd',
  });
  expect(spy).toHaveBeenCalledTimes(0);

  spy.mockClear();
  depsCollector.produce(() => {
    const custom = new ObjectDeps(mockStore, 'x').start(mockState);
    custom.a;

    proxyState.a;
    proxyState.a.b;
    proxyState.a.b.c;
  });
  expect(spy).toHaveBeenCalledTimes(1);
});

test('ComputedValue can remove duplicated deps', () => {
  const mockStore = {
    getState() {
      return {
        [computedModel.name]: store.getState()[computedModel.name],
      };
    },
  };

  const computedValue = new ComputedValue(
    mockStore,
    computedModel.name,
    'prop',
    () => {
      computedModel.state.firstName;
      computedModel.state.firstName;
      computedModel.state.lastName;
      computedModel.fullName();
      computedModel.fullName();
    },
  );

  // Collecting
  computedValue.value;

  expect(computedValue.deps).toHaveLength(3);
});

test('计算属性包含子计算属性时,子计算属性被作为一个整体', () => {
  const mockStore = {
    getState() {
      return {
        [computedModel.name]: store.getState()[computedModel.name],
      };
    },
  };

  const computedValue = new ComputedValue(
    mockStore,
    computedModel.name,
    'prop',
    () => {
      return computedModel.fullName() + 1;
    },
  );

  expect(computedValue.value).toBe('ticktock1');
  computedModel.changeFirstName('hello-');
  expect(computedValue.isDirty()).toBeTruthy();
  expect(computedValue.value).toBe('hello-tock1');
});

test('only execute computed function when deps changed', () => {
  const spy = vitest.fn().mockImplementation(() => {
    model.state.a;
  });

  const model = defineModel('x' + Math.random(), {
    initialState: {
      a: 0,
      b: 2,
    },
    reducers: {
      updateA(state) {
        state.a += 1;
      },
      updateB(state) {
        state.b += 1;
      },
    },
    computed: {
      testa: spy,
    },
  });

  expect(spy).toBeCalledTimes(0);

  model.testa();
  expect(spy).toBeCalledTimes(1);

  model.testa();
  expect(spy).toBeCalledTimes(1);

  model.updateB();
  model.testa();
  expect(spy).toBeCalledTimes(1);

  model.updateA();
  model.testa();
  expect(spy).toBeCalledTimes(2);

  model.testa();
  expect(spy).toBeCalledTimes(2);
});

test('Can handle JSON.stringify', () => {
  expect(computedModel.testJSON()).toBe(JSON.stringify(computedModel.state));
});

test('Fail to set value on proxy state', () => {
  expect(() => computedModel.testExtendObject()).toThrowError();
  expect(() => computedModel.testModifyValue()).toThrowError();
});

test('no private computed value', () => {
  // @ts-expect-error
  expect(computedModel._privateFullname).toBeUndefined();
});

test('support parameters', () => {
  expect(computedModel.withParameter(31)).toBe('tick-age-31');
  expect(computedModel.withDefaultParameter()).toBe('tick-age-20');
  expect(computedModel.withMultipleParameters(50, 'adddddr')).toBe(
    'tick-age-50-addr-adddddr',
  );
  expect(computedModel.withMultipleAndDefaultParameters(50)).toBe(
    'tick-age-50-addr-undefined',
  );
});

test('never re-calculate value with same parameters', () => {
  const spy = vitest.fn();
  const model = defineModel('computed-with-params', {
    initialState: { name: 'x' },
    computed: {
      myData(age: number, coding: boolean) {
        spy();
        return this.state.name + '-' + age + String(coding);
      },
    },
  });

  model.myData(20, true);
  expect(spy).toBeCalledTimes(1);
  model.myData(20, true);
  model.myData(20, true);
  model.myData(20, true);
  expect(spy).toBeCalledTimes(1);
  model.myData(20, false);
  expect(spy).toBeCalledTimes(2);
  model.myData(20, false);
  expect(spy).toBeCalledTimes(2);
  model.myData(34, false);
  expect(spy).toBeCalledTimes(3);
  model.myData(20, false); // cache
  expect(spy).toBeCalledTimes(3);

  spy.mockRestore();
});

test('remove computed value for cache always be skipped', () => {
  const spy = vitest.fn();
  const model = defineModel('computed-with-remove-cache', {
    initialState: { name: 'x' },
    computed: {
      myData(age: number, coding: boolean) {
        spy();
        return this.state.name + '-' + age + String(coding);
      },
    },
  });

  for (let i = 0; i < 30; ++i) {
    model.myData(i, true);
  }
  expect(spy).toBeCalledTimes(30);
  spy.mockReset();
  model.myData(1, true);
  expect(spy).toBeCalledTimes(1);
  spy.mockRestore();
});

test('complex parameter can not hit cache', () => {
  const spy = vitest.fn();
  const model = defineModel('computed-with-complex-parameter', {
    initialState: { name: 'x' },
    computed: {
      myData(opts: object) {
        spy();
        return this.state.name + '-' + JSON.stringify(opts);
      },
    },
  });

  const obj = {};
  model.myData(obj);
  expect(spy).toBeCalledTimes(1);
  model.myData(obj);
  expect(spy).toBeCalledTimes(1);
  model.myData({});
  expect(spy).toBeCalledTimes(2);
  model.myData(obj);
  expect(spy).toBeCalledTimes(2);
  spy.mockRestore();
});

test('array should always be deps', () => {
  const spy = vitest.fn();

  const model = defineModel('computed-from-array', {
    initialState: {
      x: [{ foo: 'bar' } as { foo: string; other?: string }],
      y: {},
    },
    reducers: {
      update(state, other: string) {
        state.x = [{ foo: 'bar' }, { foo: 'baz', other }];
      },
    },
    computed: {
      myData() {
        spy();
        return this.state.x.filter((item) => item.foo === 'bar');
      },
    },
  });

  model.myData();
  expect(spy).toBeCalledTimes(1);
  model.update('baz');
  model.myData();
  expect(spy).toBeCalledTimes(2);
  model.update('x');
  model.myData();
  expect(spy).toBeCalledTimes(3);
  model.update('y');
  model.myData();
  expect(spy).toBeCalledTimes(4);
});

test('re-calculate when path changed', () => {
  const spy = vitest.fn();

  const model = defineModel('re-calculate-path-changed', {
    initialState: {
      foo: { bar: undefined } as undefined | { bar: string | undefined },
      baz: '123',
    },
    reducers: {
      updateFoo(state, foo: undefined | { bar: string | undefined }) {
        state.foo = foo;
      },
    },
    computed: {
      myData() {
        const foo = this.state.foo;
        spy();
        return foo ? foo.bar : this.state.baz;
      },
    },
  });

  expect(model.myData()).toBeUndefined();
  expect(spy).toBeCalledTimes(1);
  model.updateFoo(undefined);
  expect(model.myData()).toBe('123');
  expect(spy).toBeCalledTimes(2);
  model.updateFoo({ bar: undefined });
  expect(model.myData()).toBeUndefined();
  expect(spy).toBeCalledTimes(3);
  model.updateFoo({ bar: 'abc' });
  expect(model.myData()).toBe('abc');
  expect(spy).toBeCalledTimes(4);
  model.updateFoo({ bar: undefined });
  expect(model.myData()).toBeUndefined();
  expect(spy).toBeCalledTimes(5);
});


================================================
FILE: test/connect.test.tsx
================================================
import { FC } from 'react';
import { act, render, screen } from '@testing-library/react';
import { store, connect, FocaProvider, getLoading } from '../src';
import { basicModel } from './models/basic.model';
import { complexModel } from './models/complex.model';

let App: FC<ReturnType<typeof mapStateToProps>> = ({ count, loading }) => {
  return (
    <>
      <div data-testid="count">{count}</div>
      <div data-testid="loading">{loading.toString()}</div>
    </>
  );
};

const mapStateToProps = () => {
  return {
    count: basicModel.state.count + complexModel.state.ids.length,
    loading: getLoading(basicModel.pureAsync),
  };
};

const Wrapped = connect(mapStateToProps)(App);

const Root: FC = () => {
  return (
    <FocaProvider>
      <Wrapped />
    </FocaProvider>
  );
};

beforeEach(() => {
  store.init();
});

afterEach(() => {
  store.unmount();
});

test('Get state from connect', async () => {
  render(<Root />);
  const $count = screen.queryByTestId('count')!;
  const $loading = screen.queryByTestId('loading')!;

  expect($count.innerHTML).toBe('0');
  expect($loading.innerHTML).toBe('false');

  act(() => {
    basicModel.plus(0);
  });
  expect($count.innerHTML).toBe('0');

  act(() => {
    basicModel.plus(1);
  });
  expect($count.innerHTML).toBe('1');

  act(() => {
    basicModel.plus(20.5);
  });
  expect($count.innerHTML).toBe('21.5');

  act(() => {
    complexModel.addUser(40, '');
  });
  expect($count.innerHTML).toBe('22.5');

  let promise!: Promise<any>;

  act(() => {
    promise = basicModel.pureAsync();
  });
  expect($loading.innerHTML).toBe('true');

  await act(async () => {
    await promise;
  });
  expect($loading.innerHTML).toBe('false');
});


================================================
FILE: test/deep-equal.test.ts
================================================
import { deepEqual } from '../src/utils/deep-equal';
import { equals } from './fixtures/equals';
import { notEquals } from './fixtures/not-equals';

Object.entries(equals).map(([title, { a, b }]) => {
  test(`[equal] ${title}`, () => {
    expect(deepEqual(a, b)).toBeTruthy();
  });
});

Object.entries(notEquals).map(([title, { a, b }]) => {
  test(`[not equal] ${title}`, () => {
    expect(deepEqual(a, b)).toBeFalsy();
  });
});


================================================
FILE: test/engine.test.ts
================================================
import 'fake-indexeddb/auto';
import localforage from 'localforage';
import ReactNativeStorage from '@react-native-async-storage/async-storage';
import { toPromise } from '../src/utils/to-promise';
import { memoryStorage } from '../src';

const storages = [
  [localStorage, 'local'],
  [sessionStorage, 'session'],
  [memoryStorage, 'memory'],
  [
    localforage.createInstance({ driver: localforage.LOCALSTORAGE }),
    'localforage local',
  ],
  [
    localforage.createInstance({ driver: localforage.INDEXEDDB }),
    'localforage indexedDb',
  ],
  [ReactNativeStorage, 'react-native'],
] as const;

describe.each(storages)('storage io', (storage, name) => {
  beforeEach(() => storage.clear());
  afterEach(() => storage.clear());

  test(`[${name}] Get and set data`, async () => {
    await expect(toPromise(() => storage.getItem('test1'))).resolves.toBeNull();
    await storage.setItem('test1', 'yes');
    await expect(toPromise(() => storage.getItem('test1'))).resolves.toBe(
      'yes',
    );
  });

  test(`[${name}] Update data`, async () => {
    await storage.setItem('test2', 'yes');
    await expect(toPromise(() => storage.getItem('test2'))).resolves.toBe(
      'yes',
    );
    await storage.setItem('test2', 'no');
    await expect(toPromise(() => storage.getItem('test2'))).resolves.toBe('no');
  });

  test(`[${name}] Delete data`, async () => {
    await storage.setItem('test3', 'yes');
    await expect(toPromise(() => storage.getItem('test3'))).resolves.toBe(
      'yes',
    );
    await storage.removeItem('test3');
    await expect(toPromise(() => storage.getItem('test3'))).resolves.toBeNull();
  });

  test(`[${name}] Clear all data`, async () => {
    await storage.setItem('test4', 'yes');
    await storage.setItem('test5', 'yes');

    await storage.clear();

    await expect(toPromise(() => storage.getItem('test4'))).resolves.toBeNull();
    await expect(toPromise(() => storage.getItem('test5'))).resolves.toBeNull();
  });
});


================================================
FILE: test/fixtures/equals.ts
================================================
export const equals: Record<string, { a: any; b: any }> = {
  '0 and -0': {
    a: 0,
    b: -0,
  },
  'NaN': {
    a: NaN,
    b: NaN,
  },
  'number': {
    a: 1,
    b: 1,
  },
  'string': {
    a: 'x',
    b: 'x',
  },
  'empty array': {
    a: [],
    b: [],
  },
  'array': {
    a: [1, 2, 'x'],
    b: [1, 2, 'x'],
  },
  'complex array': {
    a: [1, { x: { y: 2, z: 3, x: [3, 6] } }, 5],
    b: [1, { x: { y: 2, z: 3, x: [3, 6] } }, 5],
  },
  'object': {
    a: { x: 1, y: 2 },
    b: { x: 1, y: 2 },
  },
  'complex object': {
    a: {
      x: {
        y: {
          z: [3, 6],
          p: [
            {
              m: 6,
            },
          ],
        },
      },
    },
    b: {
      x: {
        y: {
          z: [3, 6],
          p: [
            {
              m: 6,
            },
          ],
        },
      },
    },
  },
  'object without prototype': {
    a: Object.create(null),
    b: Object.create(null),
  },
};


================================================
FILE: test/fixtures/not-equals.ts
================================================
export const notEquals: Record<string, { a: any; b: any }> = {
  '0 and NaN': {
    a: 0,
    b: NaN,
  },
  '0 and null': {
    a: 0,
    b: null,
  },
  '0 and undefined': {
    a: 0,
    b: undefined,
  },
  '0 and false': {
    a: 0,
    b: false,
  },
  'null and undefined': {
    a: null,
    b: undefined,
  },
  'array and arrayLike': {
    a: [],
    b: { length: 0 },
  },
  'array and arrayLike with length in prototype': {
    a: [],
    b: Object.create({ length: 0 }),
  },
  'array and arrayLike with items': {
    a: [1, 'x'],
    b: { 0: 1, 1: 'x', length: 2 },
  },
  'number': {
    a: 1,
    b: 2,
  },
  'string': {
    a: 'x',
    b: 'y',
  },
  'number and object': {
    a: 1,
    b: {},
  },
  'number and array': {
    a: 3,
    b: [],
  },
  'object and array': {
    a: [],
    b: {},
  },
  'array': {
    a: [1],
    b: [2],
  },
  'complex array': {
    a: [1, { x: { y: 2, z: 3, x: [3] } }, 5],
    b: [1, { x: { y: 2, z: 3, x: [3, 6] } }, 5],
  },
  'array with different length': {
    a: [1],
    b: [1, 2, 3],
  },
  'object': {
    a: { x: 1 },
    b: { x: 2 },
  },
  'complex object': {
    a: {
      x: {
        y: {
          z: [3, 6],
        },
      },
    },
    b: {
      x: {
        y: {
          z: [3, 5],
        },
      },
    },
  },
  'object with different properties': {
    a: { x: 1 },
    b: { x: 1, y: 2 },
  },
  'with different constructor': {
    a: new (class {})(),
    b: new (class {})(),
  },
  'Object.keys() get same length but not own property': {
    a: { x: 3, y: 4 },
    b: (() => {
      const b = Object.create({ x: 3 });
      b.y = 4;
      b.z = 5;
      return b;
    })(),
  },
};


================================================
FILE: test/get-loading.test.ts
================================================
import sleep from 'sleep-promise';
import { defineModel, getLoading, store } from '../src';
import { basicModel } from './models/basic.model';

beforeEach(() => {
  store.init();
});

afterEach(() => {
  store.unmount();
});

test('Collect loading status for method', async () => {
  expect(getLoading(basicModel.bos)).toBeFalsy();
  const promise = basicModel.bos();
  expect(getLoading(basicModel.bos)).toBeTruthy();
  await promise;
  expect(getLoading(basicModel.bos)).toBeFalsy();
});

test('Collect error message for method', async () => {
  expect(getLoading(basicModel.hasError)).toBeFalsy();

  const promise = basicModel.hasError();
  expect(getLoading(basicModel.hasError)).toBeTruthy();

  await expect(promise).rejects.toThrowError('my-test');

  expect(getLoading(basicModel.hasError)).toBeFalsy();
});

test('Trace loadings', async () => {
  expect(getLoading(basicModel.bos.room, 'x')).toBeFalsy();
  expect(getLoading(basicModel.bos.room).find('x')).toBeFalsy();

  const promise = basicModel.bos.room('x').execute();
  expect(getLoading(basicModel.bos.room, 'x')).toBeTruthy();
  expect(getLoading(basicModel.bos.room).find('x')).toBeTruthy();
  expect(getLoading(basicModel.bos.room, 'y')).toBeFalsy();
  expect(getLoading(basicModel.bos.room).find('y')).toBeFalsy();
  expect(getLoading(basicModel.bos)).toBeFalsy();

  await promise;
  expect(getLoading(basicModel.bos.room, 'x')).toBeFalsy();
  expect(getLoading(basicModel.bos.room).find('x')).toBeFalsy();
});

test('async method in model.onInit should be activated automatically', async () => {
  const hookModel = defineModel('loading' + Math.random(), {
    initialState: {},
    methods: {
      async myMethod() {
        await sleep(200);
      },
      async myMethod2() {
        await sleep(200);
      },
    },
    events: {
      async onInit() {
        await this.myMethod();
        await this.myMethod2();
      },
    },
  });
  await store.onInitialized();
  expect(getLoading(hookModel.myMethod)).toBeTruthy();
  await sleep(220);
  expect(getLoading(hookModel.myMethod)).toBeFalsy();

  expect(getLoading(hookModel.myMethod2)).toBeTruthy();
  await sleep(220);
  expect(getLoading(hookModel.myMethod2)).toBeFalsy();
});


================================================
FILE: test/helpers/render-hook.tsx
================================================
import { renderHook as originRenderHook } from '@testing-library/react';
import { FocaProvider } from '../../src';

export const renderHook: typeof originRenderHook = (
  renderCallback,
  options,
) => {
  return originRenderHook(renderCallback, {
    wrapper: FocaProvider,
    ...options,
  });
};


================================================
FILE: test/helpers/slow-engine.ts
================================================
import type { StorageEngine } from '../../src';
import { toPromise } from '../../src/utils/to-promise';

let cache: Partial<Record<string, string>> = {};

export const slowEngine: StorageEngine = {
  getItem(key) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(cache[key] === void 0 ? null : cache[key]!);
      }, 300);
    });
  },
  setItem(key, value) {
    return toPromise(() => {
      cache[key] = value;
    });
  },
  removeItem(key) {
    return toPromise(() => {
      cache[key] = void 0;
    });
  },
  clear() {
    return toPromise(() => {
      cache = {};
    });
  },
};


================================================
FILE: test/lifecycle.test.ts
================================================
import sleep from 'sleep-promise';
import { cloneModel, defineModel, memoryStorage, store } from '../src';
import { PersistSchema } from '../src/persist/persist-item';

describe('onInit', () => {
  afterEach(() => {
    store.unmount();
  });

  const createModel = () => {
    return defineModel('events' + Math.random(), {
      initialState: { count: 0 },
      methods: {
        invokeByReadyHook() {
          this.setState((state) => {
            state.count += 101;
          });
        },
      },
      events: {
        onInit() {
          this.invokeByReadyHook();
        },
      },
    });
  };

  test('trigger ready events on store ready', async () => {
    const hookModel = createModel();
    store.init();

    const hook2Model = createModel();
    co
Download .txt
gitextract_3jrecnzc/

├── .github/
│   └── workflows/
│       ├── codeql.yml
│       ├── prerelease.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .husky/
│   ├── commit-msg
│   └── pre-commit
├── .prettierignore
├── .prettierrc.yml
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs/
│   ├── .nojekyll
│   ├── CNAME
│   ├── _sidebar.md
│   ├── advanced.md
│   ├── api.md
│   ├── changelog.md
│   ├── donate.md
│   ├── events.md
│   ├── home.md
│   ├── index.html
│   ├── initialize.md
│   ├── model.md
│   ├── persist.md
│   ├── redux-toolkit.md
│   ├── test.md
│   └── troubleshooting.md
├── package.json
├── src/
│   ├── actions/
│   │   ├── loading.ts
│   │   ├── model.ts
│   │   ├── persist.ts
│   │   └── refresh.ts
│   ├── api/
│   │   ├── get-loading.ts
│   │   ├── use-computed.ts
│   │   ├── use-isolate.ts
│   │   ├── use-loading.ts
│   │   └── use-model.ts
│   ├── engines/
│   │   ├── memory.ts
│   │   └── storage-engine.ts
│   ├── index.ts
│   ├── middleware/
│   │   ├── action-in-action.interceptor.ts
│   │   ├── destroy-loading.interceptor.ts
│   │   ├── freeze-state.middleware.ts
│   │   ├── loading.interceptor.ts
│   │   └── model.interceptor.ts
│   ├── model/
│   │   ├── clone-model.ts
│   │   ├── define-model.ts
│   │   ├── enhance-action.ts
│   │   ├── enhance-computed.ts
│   │   ├── enhance-effect.ts
│   │   ├── guard.ts
│   │   └── types.ts
│   ├── persist/
│   │   ├── persist-gate.tsx
│   │   ├── persist-item.ts
│   │   └── persist-manager.ts
│   ├── reactive/
│   │   ├── computed-value.ts
│   │   ├── create-computed-deps.ts
│   │   ├── deps-collector.ts
│   │   └── object-deps.ts
│   ├── redux/
│   │   ├── connect.ts
│   │   ├── contexts.ts
│   │   ├── create-reducer.ts
│   │   ├── foca-provider.tsx
│   │   └── use-selector.ts
│   ├── store/
│   │   ├── loading-store.ts
│   │   ├── model-store.ts
│   │   ├── proxy-store.ts
│   │   └── store-basic.ts
│   └── utils/
│       ├── deep-equal.ts
│       ├── get-method-category.ts
│       ├── getter.ts
│       ├── immer.ts
│       ├── is-promise.ts
│       ├── is-type.ts
│       ├── serialize.ts
│       ├── symbol-observable.ts
│       ├── to-args.ts
│       └── to-promise.ts
├── test/
│   ├── __snapshots__/
│   │   └── serialize.test.ts.snap
│   ├── action-in-action.test.tsx
│   ├── build.test.ts
│   ├── clone.test.ts
│   ├── computed.test.ts
│   ├── connect.test.tsx
│   ├── deep-equal.test.ts
│   ├── engine.test.ts
│   ├── fixtures/
│   │   ├── equals.ts
│   │   └── not-equals.ts
│   ├── get-loading.test.ts
│   ├── helpers/
│   │   ├── render-hook.tsx
│   │   └── slow-engine.ts
│   ├── lifecycle.test.ts
│   ├── middleware.test.ts
│   ├── model.test.ts
│   ├── models/
│   │   ├── basic.model.ts
│   │   ├── complex.model.ts
│   │   ├── computed.model.ts
│   │   └── persist.model.ts
│   ├── persist.gate.test.tsx
│   ├── persist.test.ts
│   ├── provider.test.tsx
│   ├── serialize.test.ts
│   ├── store.test.ts
│   ├── typescript/
│   │   ├── computed.check.ts
│   │   ├── define-model.check.ts
│   │   ├── get-loading.check.ts
│   │   ├── persist.check.ts
│   │   ├── use-isolate.check.ts
│   │   ├── use-loading.check.ts
│   │   └── use-model.check.ts
│   ├── use-computed.test.ts
│   ├── use-isolate.test.tsx
│   ├── use-loading.test.ts
│   └── use-model.test.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
Download .txt
SYMBOL INDEX (292 symbols across 50 files)

FILE: src/actions/loading.ts
  constant TYPE_SET_LOADING (line 3) | const TYPE_SET_LOADING = '@@store/loading';
  constant LOADING_CATEGORY (line 5) | const LOADING_CATEGORY = '##' + Math.random();
  constant DESTROY_LOADING (line 7) | const DESTROY_LOADING = TYPE_SET_LOADING + '/destroy';
  type LoadingAction (line 9) | interface LoadingAction extends UnknownAction {
  type DestroyLoadingAction (line 31) | interface DestroyLoadingAction extends UnknownAction {

FILE: src/actions/model.ts
  type PreModelAction (line 4) | interface PreModelAction<State extends object = object, Payload = object>
  type PostModelAction (line 14) | interface PostModelAction<State = object>

FILE: src/actions/persist.ts
  constant TYPE_PERSIST_HYDRATE (line 3) | const TYPE_PERSIST_HYDRATE = '@@persist/hydrate';
  type PersistHydrateAction (line 5) | interface PersistHydrateAction extends UnknownAction {

FILE: src/actions/refresh.ts
  constant TYPE_REFRESH_STORE (line 3) | const TYPE_REFRESH_STORE = '@@store/refresh';
  type RefreshAction (line 5) | interface RefreshAction extends UnknownAction {

FILE: src/api/get-loading.ts
  function getLoading (line 42) | function getLoading(

FILE: src/api/use-computed.ts
  type UseComputedFlag (line 5) | interface UseComputedFlag extends ComputedFlag {
  function useComputed (line 39) | function useComputed(ref: UseComputedFlag) {

FILE: src/api/use-loading.ts
  function useLoading (line 42) | function useLoading(): boolean | FindLoading {

FILE: src/api/use-model.ts
  type Algorithm (line 15) | type Algorithm = 'strictEqual' | 'shallowEqual' | 'deepEqual';
  function useModel (line 177) | function useModel(): any {

FILE: src/engines/memory.ts
  method getItem (line 6) | getItem(key) {
  method setItem (line 9) | setItem(key, value) {
  method removeItem (line 12) | removeItem(key) {
  method clear (line 15) | clear() {

FILE: src/engines/storage-engine.ts
  type StorageEngine (line 1) | interface StorageEngine {

FILE: src/model/clone-model.ts
  type EditableKeys (line 12) | type EditableKeys = (typeof editableKeys)[number];
  type OverrideOptions (line 14) | type OverrideOptions<

FILE: src/model/enhance-action.ts
  type EnhancedAction (line 6) | interface EnhancedAction<State extends object> {

FILE: src/model/enhance-computed.ts
  function anonymousFn (line 18) | function anonymousFn() {

FILE: src/model/enhance-effect.ts
  type RoomFunc (line 11) | interface RoomFunc<P extends any[] = any[], R = Promise<any>> {
  type AsyncRoomEffect (line 17) | interface AsyncRoomEffect<P extends any[] = any[], R = Promise<any>>
  type AsyncEffect (line 26) | interface AsyncEffect<P extends any[] = any[], R = Promise<any>>
  type PromiseEffect (line 52) | type PromiseEffect = AsyncEffect;
  type PromiseRoomEffect (line 53) | type PromiseRoomEffect = AsyncRoomEffect;
  type EffectFunc (line 55) | interface EffectFunc<P extends any[] = any[], R = Promise<any>> {
  type EnhancedEffect (line 59) | type EnhancedEffect<P extends any[] = any[], R = Promise<any>> =
  type NonReadonly (line 62) | type NonReadonly<T extends object> = {
  method execute (line 84) | execute() {

FILE: src/model/types.ts
  type ComputedFlag (line 5) | interface ComputedFlag {
  type GetName (line 9) | interface GetName<Name extends string> {
  type GetState (line 16) | interface GetState<State extends object> {
  type GetInitialState (line 23) | interface GetInitialState<State extends object> {
  type ModelPersist (line 30) | type ModelPersist<State extends object, PersistDump> = {
  type ActionCtx (line 86) | interface ActionCtx<State extends object>
  type EffectCtx (line 90) | interface EffectCtx<State extends object>
  type SetStateCallback (line 133) | interface SetStateCallback<State extends object, K extends keyof State> {
  type ComputedCtx (line 137) | interface ComputedCtx<State extends object>
  type BaseModel (line 141) | interface BaseModel<Name extends string, State extends object>
  type ModelActionItem (line 145) | type ModelActionItem<
  type ModelAction (line 153) | type ModelAction<State extends object, Action extends object> = {
  type GetPrivateMethodKeys (line 157) | type GetPrivateMethodKeys<Method extends object> = {
  type ModelEffect (line 161) | type ModelEffect<Effect extends object> = {
  type ModelComputed (line 167) | type ModelComputed<Computed extends object> = {
  type Model (line 171) | type Model<
  type InternalModel (line 185) | type InternalModel<
  type InternalAction (line 196) | type InternalAction<State extends object> = {
  type Event (line 200) | interface Event<State> {
  type EventCtx (line 223) | interface EventCtx<State extends object>
  type DefineModelOptions (line 227) | interface DefineModelOptions<

FILE: src/persist/persist-gate.tsx
  type PersistGateProps (line 5) | interface PersistGateProps {

FILE: src/persist/persist-item.ts
  type PersistSchema (line 12) | interface PersistSchema {
  type PersistItemSchema (line 25) | interface PersistItemSchema {
  type PersistMergeMode (line 36) | type PersistMergeMode = 'replace' | 'merge' | 'deep-merge';
  type PersistOptions (line 38) | interface PersistOptions {
  type CustomModelPersistOptions (line 72) | type CustomModelPersistOptions = Required<ModelPersist<object, any>> & {
  type PersistRecord (line 78) | interface PersistRecord {
  class PersistItem (line 96) | class PersistItem {
    method constructor (line 101) | constructor(protected readonly options: PersistOptions) {
    method init (line 133) | init(): Promise<void> {
    method loadMissingState (line 179) | loadMissingState() {
    method merge (line 197) | merge(persistState: any, initialState: any, mode: PersistMergeMode) {
    method collect (line 224) | collect(): Record<string, object> {
    method update (line 234) | update(nextState: Record<string, object>) {
    method loop (line 260) | protected loop(callback: (record: PersistRecord, key: string) => void) {
    method dump (line 269) | protected dump() {
    method validateSchema (line 273) | protected validateSchema(schema: any): schema is PersistSchema {
    method validateItemSchema (line 281) | protected validateItemSchema(
    method toJSON (line 288) | protected toJSON(): PersistSchema {

FILE: src/persist/persist-manager.ts
  class PersistManager (line 5) | class PersistManager {
    method constructor (line 11) | constructor(options: PersistOptions[]) {
    method init (line 15) | init(store: Store, hydrate: boolean) {
    method destroy (line 26) | destroy() {
    method collect (line 31) | collect(): Record<string, object> {
    method combineReducer (line 37) | combineReducer(original: Reducer): Reducer<Record<string, object>> {
    method update (line 49) | protected update(store: Store) {

FILE: src/reactive/computed-value.ts
  class ComputedValue (line 6) | class ComputedValue<T = any> {
    method constructor (line 13) | constructor(
    method value (line 20) | public get value(): T {
    method isDirty (line 39) | isDirty(): boolean {
    method updateSnapshot (line 57) | protected updateSnapshot() {

FILE: src/reactive/create-computed-deps.ts
  method end (line 10) | end(): void {
  method isDirty (line 13) | isDirty(): boolean {

FILE: src/reactive/deps-collector.ts
  method active (line 7) | get active(): boolean {
  method produce (line 10) | produce(callback: Function): Deps[] {
  method append (line 30) | append(dep: Deps) {
  method prepend (line 33) | prepend(dep: Deps) {

FILE: src/reactive/object-deps.ts
  type Deps (line 5) | interface Deps {
  class ObjectDeps (line 11) | class ObjectDeps<T = any> implements Deps {
    method constructor (line 16) | constructor(
    method isDirty (line 24) | isDirty(): boolean {
    method id (line 33) | get id(): string {
    method start (line 37) | start<T extends Record<string, any>>(startState: T): T {
    method end (line 42) | end(): void {
    method getState (line 46) | protected getState(): T {
    method getSnapshot (line 50) | protected getSnapshot(state: any): { pathChanged: boolean; snapshot: a...
    method proxy (line 63) | protected proxy(currentState: Record<string, any>): any {

FILE: src/redux/create-reducer.ts
  type Options (line 5) | interface Options<State extends object> {

FILE: src/redux/foca-provider.tsx
  type OwnProps (line 10) | interface OwnProps extends PersistGateProps {}

FILE: src/store/loading-store.ts
  type FindLoading (line 19) | interface FindLoading {
  type LoadingState (line 23) | interface LoadingState extends FindLoading {
  type LoadingStoreStateItem (line 29) | interface LoadingStoreStateItem {
  type LoadingStoreState (line 33) | type LoadingStoreState = Partial<{
  class LoadingStore (line 55) | class LoadingStore extends StoreBasic<LoadingStoreState> {
    method constructor (line 67) | constructor() {
    method init (line 83) | init() {
    method unmount (line 102) | unmount(): void {
    method reducer (line 106) | reducer(
    method get (line 141) | get(effect: PromiseEffect | PromiseRoomEffect): LoadingStoreStateItem {
    method getItem (line 156) | getItem(model: string, method: string): LoadingStoreStateItem | undefi...
    method isModelInitializing (line 161) | isModelInitializing(model: string): boolean {
    method isActive (line 168) | isActive(model: string, method: string): boolean {
    method activate (line 173) | activate(model: string, method: string) {
    method inactivate (line 177) | inactivate(model: string, method: string) {
    method refresh (line 181) | refresh() {

FILE: src/store/model-store.ts
  type Compose (line 21) | type Compose =
  type CreateStoreOptions (line 25) | interface CreateStoreOptions {
  class ModelStore (line 32) | class ModelStore extends StoreBasic<Record<string, any>> {
    method constructor (line 48) | constructor() {
    method isReady (line 53) | get isReady(): boolean {
    method init (line 57) | init(options: CreateStoreOptions = {}) {
    method refresh (line 117) | refresh(force: boolean = false): RefreshAction {
    method unmount (line 123) | unmount() {
    method onInitialized (line 129) | onInitialized(maybeSync?: () => void): Promise<void> {
    method ready (line 143) | protected ready() {
    method getCompose (line 148) | protected getCompose(customCompose: CreateStoreOptions['compose']): Co...
    method combineReducers (line 168) | protected combineReducers(): Reducer<Record<string, object>> {
    method appendReducer (line 197) | protected appendReducer(key: string, consumer: Reducer): void {
    method removeReducer (line 207) | protected removeReducer(key: string): void {

FILE: src/store/store-basic.ts
  method store (line 28) | protected get store(): Store<T> {

FILE: src/utils/getter.ts
  function composeGetter (line 40) | function composeGetter() {

FILE: src/utils/is-type.ts
  constant OBJECT (line 1) | const OBJECT = 'object';
  constant FUNCTION (line 2) | const FUNCTION = 'function';

FILE: src/utils/serialize.ts
  constant JSON_UNDEFINED (line 3) | const JSON_UNDEFINED = '__JSON_UNDEFINED__';

FILE: test/action-in-action.test.tsx
  method plus (line 12) | plus(state) {
  method toggle (line 15) | toggle(state) {

FILE: test/build.test.ts
  function testFile (line 4) | function testFile(filename: string, expectCode: number) {

FILE: test/clone.test.ts
  method cc (line 61) | cc() {

FILE: test/computed.test.ts
  method a (line 49) | a() {
  method b (line 52) | b() {
  method c (line 55) | c() {
  method getState (line 73) | getState() {
  method getState (line 99) | getState() {
  method getState (line 137) | getState() {
  method getState (line 179) | getState() {
  method getState (line 207) | getState() {
  method updateA (line 240) | updateA(state) {
  method updateB (line 243) | updateB(state) {
  method myData (line 302) | myData(age: number, coding: boolean) {
  method myData (line 332) | myData(age: number, coding: boolean) {
  method myData (line 354) | myData(opts: object) {
  method update (line 382) | update(state, other: string) {
  method myData (line 387) | myData() {
  method updateFoo (line 416) | updateFoo(state, foo: undefined | { bar: string | undefined }) {
  method myData (line 421) | myData() {

FILE: test/get-loading.test.ts
  method myMethod (line 52) | async myMethod() {
  method myMethod2 (line 55) | async myMethod2() {
  method onInit (line 60) | async onInit() {

FILE: test/helpers/slow-engine.ts
  method getItem (line 7) | getItem(key) {
  method setItem (line 14) | setItem(key, value) {
  method removeItem (line 19) | removeItem(key) {
  method clear (line 24) | clear() {

FILE: test/lifecycle.test.ts
  method invokeByReadyHook (line 14) | invokeByReadyHook() {
  method onInit (line 21) | onInit() {
  method onInit (line 109) | async onInit() {
  method plus (line 150) | plus(state) {
  method minus (line 153) | minus(state) {
  method _invokeByReadyHook (line 158) | _invokeByReadyHook() {
  method onInit (line 165) | onInit() {
  method onChange (line 169) | onChange(prevState, nextState) {
  method update (line 204) | update(state) {
  method update (line 228) | update(state) {

FILE: test/middleware.test.ts
  method test1 (line 95) | test1() {}
  method ok (line 98) | async ok() {}
  method notOk (line 99) | notOk() {
  method test2 (line 107) | test2() {
  method test3 (line 110) | test3() {
  method test4 (line 113) | test4() {

FILE: test/model.test.ts
  type State (line 102) | type State = {
  method setNothing (line 116) | setNothing() {
  method setCount (line 119) | setCount() {
  method setHello (line 126) | setHello() {
  method setHelloByFn (line 131) | setHelloByFn() {
  method setNothingByFn (line 138) | setNothingByFn() {
  method override (line 141) | override() {
  method set (line 188) | set() {
  method test1 (line 228) | test1() {}
  method test1 (line 231) | test1() {}
  method test2 (line 232) | test2() {}
  method test2 (line 241) | test2() {}
  method test1 (line 244) | test1() {}
  method test2 (line 245) | test2() {}
  method test2 (line 254) | test2() {}
  method test1 (line 257) | test1() {}
  method test2 (line 258) | test2() {}

FILE: test/models/basic.model.ts
  method plus (line 15) | plus(state, step: number) {
  method minus (line 18) | minus(state, step: number) {
  method moreParams (line 21) | moreParams(state, step: number, hello: string) {
  method set (line 25) | set(state, count: number) {
  method reset (line 28) | reset() {
  method _actionIsPrivate (line 31) | _actionIsPrivate() {}
  method ____alsoPrivateAction (line 32) | ____alsoPrivateAction() {}
  method foo (line 35) | async foo(hello: string, step: number) {
  method setWithoutFn (line 45) | setWithoutFn(step: number) {
  method setPartialState (line 51) | setPartialState(step: number) {
  method bar (line 56) | async bar() {
  method bos (line 59) | async bos() {
  method hasError (line 62) | async hasError(msg: string = 'my-test') {
  method pureAsync (line 65) | async pureAsync() {
  method normalMethod (line 69) | normalMethod() {
  method _effectIsPrivate (line 72) | async _effectIsPrivate() {}
  method ____alsoPrivateEffect (line 73) | ____alsoPrivateEffect() {}

FILE: test/models/complex.model.ts
  method addUser (line 14) | addUser(state, id: number, name: string) {
  method deleteUser (line 18) | deleteUser(state, id: number) {
  method updateUser (line 22) | updateUser(state, id: number, name: string) {

FILE: test/models/computed.model.ts
  method changeFirstName (line 21) | changeFirstName(state, value: string) {
  method changeLastName (line 24) | changeLastName(state, value) {
  method effectsGetFullName (line 29) | effectsGetFullName() {
  method fullName (line 34) | fullName() {
  method _privateFullname (line 37) | _privateFullname() {
  method testDependentOtherComputed (line 40) | testDependentOtherComputed() {
  method isOnline (line 47) | isOnline() {
  method testArrayLength (line 50) | testArrayLength() {
  method testObjectKeys (line 53) | testObjectKeys() {
  method testFind (line 56) | testFind() {
  method testVisitArray (line 59) | testVisitArray() {
  method testJSON (line 62) | testJSON() {
  method testExtendObject (line 65) | testExtendObject() {
  method testModifyValue (line 68) | testModifyValue() {
  method withParameter (line 71) | withParameter(age: number) {
  method withDefaultParameter (line 74) | withDefaultParameter(age: number = 20) {
  method withMultipleParameters (line 77) | withMultipleParameters(age: number = 20, address: string) {
  method withMultipleAndDefaultParameters (line 80) | withMultipleAndDefaultParameters(age: number = 20, address?: string) {

FILE: test/models/persist.model.ts
  method plus (line 12) | plus(state, step: number) {
  method minus (line 15) | minus(state, step: number) {
  method dump (line 33) | dump(state) {
  method load (line 36) | load(counter) {

FILE: test/persist.test.ts
  method dump (line 464) | dump(state) {
  method load (line 468) | load(hello) {

FILE: test/store.test.ts
  method foo (line 309) | foo(state) {
  method bar (line 312) | bar(state) {

FILE: test/typescript/computed.check.ts
  method fullName (line 11) | fullName() {
  method nickName (line 14) | nickName() {
  method _dirname (line 17) | _dirname() {
  method withAge (line 20) | withAge(age: number = 20) {
  method withRequiredParameter (line 23) | withRequiredParameter(address: string) {
  method withMultipleParameter (line 26) | withMultipleParameter(address: string, age: number, extra?: boolean) {

FILE: test/typescript/define-model.check.ts
  method returnNormal (line 20) | returnNormal(_) {
  method returnInitialize (line 23) | returnInitialize(_) {
  method returnNormal (line 28) | returnNormal() {
  method returnInitialize (line 59) | returnInitialize() {
  method returnNormal (line 75) | returnNormal(_state) {
  method returnInitialize (line 78) | returnInitialize() {
  method returnNormal (line 83) | returnNormal() {
  method returnPartial (line 125) | returnPartial() {
  method returnInitialize (line 160) | returnInitialize() {
  method returnUnexpected (line 173) | returnUnexpected(_) {
  method right (line 176) | right() {
  method returnUnexpected (line 186) | returnUnexpected(_) {
  method right (line 190) | right() {
  method returnUnexpected (line 200) | returnUnexpected(_) {
  method right (line 203) | right() {
  method _action1 (line 212) | _action1() {}
  method _action2 (line 213) | _action2() {}
  method action3 (line 214) | action3() {
  method _method1 (line 224) | _method1() {
  method _method2 (line 227) | async _method2() {}
  method method3 (line 228) | method3() {
  method xxx (line 235) | xxx() {
  method yyy (line 245) | yyy() {
  method _fullname (line 249) | _fullname() {
  method onInit (line 254) | onInit() {

FILE: test/typescript/persist.check.ts
  method dump (line 11) | dump() {
  method load (line 21) | load() {
  method dump (line 30) | dump() {
  method load (line 33) | load() {
  method dump (line 61) | dump(state) {
  method load (line 64) | load(s) {
  method dump (line 84) | dump(state) {
  method load (line 87) | load(s) {
  method dump (line 97) | dump() {
  method load (line 100) | load(s) {
  method dump (line 110) | dump() {
  method load (line 119) | load(dumpData) {

FILE: test/typescript/use-isolate.check.ts
  method onDestroy (line 42) | onDestroy() {

FILE: test/use-isolate.test.tsx
  function MyApp (line 135) | function MyApp() {
Condensed preview — 120 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (303K chars).
[
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 835,
    "preview": "name: 'CodeQL'\n\non:\n  push:\n    branches: ['master']\n  pull_request:\n    branches: ['master']\n  schedule:\n    - cron: '2"
  },
  {
    "path": ".github/workflows/prerelease.yml",
    "chars": 470,
    "preview": "name: Pre Release\n\non:\n  release:\n    types: [prereleased]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n     "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 432,
    "preview": "name: Release\n\non:\n  release:\n    types: [released]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 2672,
    "preview": "name: Test\n\non:\n  push:\n    branches:\n      - '*'\n    tags-ignore:\n      - '*'\n  pull_request:\n    branches:\n\njobs:\n  fo"
  },
  {
    "path": ".gitignore",
    "chars": 92,
    "preview": ".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",
    "chars": 38,
    "preview": "npx --no-install commitlint --edit $1\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 44,
    "preview": "npx --no-install prettier --cache --check .\n"
  },
  {
    "path": ".prettierignore",
    "chars": 50,
    "preview": "dist/\nbuild/\ncoverage/\npnpm-lock.yaml\n_sidebar.md\n"
  },
  {
    "path": ".prettierrc.yml",
    "chars": 458,
    "preview": "semi: true\nsingleQuote: true\n# Change when properties in objects are quoted.\n# If at least one property in an object req"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 52,
    "preview": "{\n  \"recommendations\": [\"esbenp.prettier-vscode\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 255,
    "preview": "{\n  \"typescript.preferences.quoteStyle\": \"single\",\n  \"typescript.suggest.autoImports\": true,\n  \"editor.tabSize\": 2,\n  \"t"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 12661,
    "preview": "## [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]("
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2021-2023 geekact\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 10373,
    "preview": "# FOCA\n\n流畅的 react 状态管理库,基于[redux](https://github.com/reduxjs/redux)和[react-redux](https://github.com/reduxjs/react-redux"
  },
  {
    "path": "docs/.nojekyll",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/CNAME",
    "chars": 11,
    "preview": "foca.js.org"
  },
  {
    "path": "docs/_sidebar.md",
    "chars": 281,
    "preview": "* [关于Foca](/)\n\n- [更新日志](/changelog.md)\n\n* [开始使用](/initialize.md)\n\n- [模型](/model.md)\n\n* [接口](/api.md)\n\n- [事件钩子](/events.m"
  },
  {
    "path": "docs/advanced.md",
    "chars": 4345,
    "preview": "# <!-- {docsify-ignore} -->\n\n# 克隆模型\n\n虽然比较不常用,但有时候为了同一个页面的不同模块能独立使用模型数据,你就得需要复制这个模型,并把名字改掉。其实也不用这么麻烦,foca 给你来个惊喜:\n\n```typ"
  },
  {
    "path": "docs/api.md",
    "chars": 3640,
    "preview": "# <!-- {docsify-ignore} -->\n\n# useModel\n\n使用频率::star2::star2::star2::star2::star2:\n\n你绝对想不到在 React 组件中获取一个模型的数据有多简单,试试:\n\n`"
  },
  {
    "path": "docs/changelog.md",
    "chars": 129,
    "preview": "# 更新日志 <!-- {docsify-ignore-all} -->\n\n[changelog](https://raw.githubusercontent.com/foca-js/foca/master/CHANGELOG.md ':i"
  },
  {
    "path": "docs/donate.md",
    "chars": 1021,
    "preview": "# 捐赠 <!-- {docsify-ignore} -->\n\n开源不易,升级维护框架和解决各种 issue 需要十分多的精力和时间。希望能得到你的支持,让项目处于良性发展的状态。\n\n> 捐赠时请备注你的 github 账号,对于每一个捐赠"
  },
  {
    "path": "docs/events.md",
    "chars": 1591,
    "preview": "每个模型都有针对自身的事件回调,在某些复杂的业务场景下,事件和其它属性的组合将变得十分灵活。\n\n## onInit\n\n当 store 初始化完成 并且持久化(如果有)数据已经恢复时,onInit 就会被自动触发。你可以调用 methods "
  },
  {
    "path": "docs/home.md",
    "chars": 7601,
    "preview": "# FOCA\n\n流畅的 react 状态管理库,基于[redux](https://github.com/reduxjs/redux)和[react-redux](https://github.com/reduxjs/react-redux"
  },
  {
    "path": "docs/index.html",
    "chars": 2985,
    "preview": "<!doctype html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Foca</title>\n    <meta http-equiv=\""
  },
  {
    "path": "docs/initialize.md",
    "chars": 4391,
    "preview": "# <!-- {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\nf"
  },
  {
    "path": "docs/model.md",
    "chars": 5235,
    "preview": "# <!-- {docsify-ignore} -->\n\n# Model\n\n原生的 redux 由 action/type/reducer 三个部分组成,大多数情况我们会分成 3 个文件分别存储。在实际使用中,这种模板式的书写方式不仅繁琐,"
  },
  {
    "path": "docs/persist.md",
    "chars": 4774,
    "preview": "# 持久化\n\n持久化是自动把数据通过引擎存储到某个空间的过程。\n\n如果你的某个 api 数据常年不变,那么建议你把它扔到本地做个缓存,这样用户下次再访问你的页面时,可以第一时间看到缓存的内容。如果你不想让用户每次刷新页面就重新登录,那么持久"
  },
  {
    "path": "docs/redux-toolkit.md",
    "chars": 7513,
    "preview": "<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<"
  },
  {
    "path": "docs/test.md",
    "chars": 1987,
    "preview": "前端的需求变化总是太快导致测试用例跟不上,甚至部分程序员根本就没想过为自己写的代码编写测试,他们心里总是想着`出错了再说`。对于要求拥有高质量体验的项目,测试是必不可少的,它能使得代码更加稳健,并且在新增功能和重构代码时,都无需太担心会破坏"
  },
  {
    "path": "docs/troubleshooting.md",
    "chars": 1377,
    "preview": "# <!-- {docsify-ignore} -->\n\n# 函数里 this 的类型是 any\n\n需要在文件 **tsconfig.json** 中开启`\"strict\": true`或者`\"noImplicitThis\": true`。"
  },
  {
    "path": "package.json",
    "chars": 2297,
    "preview": "{\n  \"name\": \"foca\",\n  \"version\": \"4.0.1\",\n  \"repository\": \"git@github.com:foca-js/foca.git\",\n  \"homepage\": \"https://foca"
  },
  {
    "path": "src/actions/loading.ts",
    "chars": 1020,
    "preview": "import type { UnknownAction } from 'redux';\n\nexport const TYPE_SET_LOADING = '@@store/loading';\n\nexport const LOADING_CA"
  },
  {
    "path": "src/actions/model.ts",
    "chars": 981,
    "preview": "import type { Action, UnknownAction } from 'redux';\nimport { isFunction } from '../utils/is-type';\n\nexport interface Pre"
  },
  {
    "path": "src/actions/persist.ts",
    "chars": 573,
    "preview": "import type { UnknownAction } from 'redux';\n\nconst TYPE_PERSIST_HYDRATE = '@@persist/hydrate';\n\nexport interface Persist"
  },
  {
    "path": "src/actions/refresh.ts",
    "chars": 531,
    "preview": "import type { UnknownAction } from 'redux';\n\nconst TYPE_REFRESH_STORE = '@@store/refresh';\n\nexport interface RefreshActi"
  },
  {
    "path": "src/api/get-loading.ts",
    "chars": 1418,
    "preview": "import { LOADING_CATEGORY } from '../actions/loading';\nimport { PromiseRoomEffect, PromiseEffect } from '../model/enhanc"
  },
  {
    "path": "src/api/use-computed.ts",
    "chars": 1095,
    "preview": "import { ComputedFlag } from '../model/types';\nimport { useModelSelector } from '../redux/use-selector';\nimport { toArgs"
  },
  {
    "path": "src/api/use-isolate.ts",
    "chars": 4027,
    "preview": "import { useEffect, useMemo, useRef, useState } from 'react';\nimport { DestroyLoadingAction, DESTROY_LOADING } from '../"
  },
  {
    "path": "src/api/use-loading.ts",
    "chars": 1106,
    "preview": "import { PromiseRoomEffect, PromiseEffect } from '../model/enhance-effect';\nimport { FindLoading } from '../store/loadin"
  },
  {
    "path": "src/api/use-model.ts",
    "chars": 6438,
    "preview": "import { shallowEqual } from 'react-redux';\nimport { deepEqual } from '../utils/deep-equal';\nimport type { Model } from "
  },
  {
    "path": "src/engines/memory.ts",
    "chars": 368,
    "preview": "import type { StorageEngine } from './storage-engine';\n\nlet cache: Partial<Record<string, string>> = {};\n\nexport const m"
  },
  {
    "path": "src/engines/storage-engine.ts",
    "chars": 191,
    "preview": "export interface StorageEngine {\n  getItem(key: string): string | null | Promise<string | null>;\n  setItem(key: string, "
  },
  {
    "path": "src/index.ts",
    "chars": 860,
    "preview": "// 模型中使用\nexport { defineModel } from './model/define-model';\nexport { cloneModel } from './model/clone-model';\n\n// 组件中使用"
  },
  {
    "path": "src/middleware/action-in-action.interceptor.ts",
    "chars": 1431,
    "preview": "import type { Middleware, UnknownAction } from 'redux';\nimport { isPreModelAction } from '../actions/model';\n\n// 开发者有可能在"
  },
  {
    "path": "src/middleware/destroy-loading.interceptor.ts",
    "chars": 365,
    "preview": "import type { Middleware } from 'redux';\nimport { isDestroyLoadingAction } from '../actions/loading';\n\nexport const dest"
  },
  {
    "path": "src/middleware/freeze-state.middleware.ts",
    "chars": 311,
    "preview": "import { freeze } from 'immer';\nimport type { Middleware } from 'redux';\n\nexport const freezeStateMiddleware: Middleware"
  },
  {
    "path": "src/middleware/loading.interceptor.ts",
    "chars": 853,
    "preview": "import type { Middleware } from 'redux';\nimport type { LoadingStore, LoadingStoreState } from '../store/loading-store';\n"
  },
  {
    "path": "src/middleware/model.interceptor.ts",
    "chars": 805,
    "preview": "import type { Middleware } from 'redux';\nimport { deepEqual } from '../utils/deep-equal';\nimport { isPreModelAction, Pos"
  },
  {
    "path": "src/model/clone-model.ts",
    "chars": 1879,
    "preview": "import { isFunction } from '../utils/is-type';\nimport { defineModel } from './define-model';\nimport type { DefineModelOp"
  },
  {
    "path": "src/model/define-model.ts",
    "chars": 8316,
    "preview": "import { parseState, stringifyState } from '../utils/serialize';\nimport { deepEqual } from '../utils/deep-equal';\nimport"
  },
  {
    "path": "src/model/enhance-action.ts",
    "chars": 1063,
    "preview": "import type { PreModelAction } from '../actions/model';\nimport { modelStore } from '../store/model-store';\nimport { toAr"
  },
  {
    "path": "src/model/enhance-computed.ts",
    "chars": 1408,
    "preview": "import { ComputedValue } from '../reactive/computed-value';\nimport { modelStore } from '../store/model-store';\nimport { "
  },
  {
    "path": "src/model/enhance-effect.ts",
    "chars": 3231,
    "preview": "import {\n  LoadingAction,\n  LOADING_CATEGORY,\n  TYPE_SET_LOADING,\n} from '../actions/loading';\nimport type { EffectCtx }"
  },
  {
    "path": "src/model/guard.ts",
    "chars": 321,
    "preview": "const counter: Record<string, number> = {};\n\nexport const guard = (modelName: string) => {\n  counter[modelName] ||= 0;\n\n"
  },
  {
    "path": "src/model/types.ts",
    "chars": 8546,
    "preview": "import type { UnknownAction } from 'redux';\nimport type { EnhancedEffect } from './enhance-effect';\nimport type { Persis"
  },
  {
    "path": "src/persist/persist-gate.tsx",
    "chars": 944,
    "preview": "import { ReactNode, FC, useState, useEffect } from 'react';\nimport { modelStore } from '../store/model-store';\nimport { "
  },
  {
    "path": "src/persist/persist-item.ts",
    "chars": 7182,
    "preview": "import type { StorageEngine } from '../engines/storage-engine';\nimport type {\n  GetInitialState,\n  InternalModel,\n  Mode"
  },
  {
    "path": "src/persist/persist-manager.ts",
    "chars": 1663,
    "preview": "import type { Reducer, Store, Unsubscribe } from 'redux';\nimport { actionHydrate, isHydrateAction } from '../actions/per"
  },
  {
    "path": "src/reactive/computed-value.ts",
    "chars": 1528,
    "preview": "import type { Store } from 'redux';\nimport { depsCollector } from './deps-collector';\nimport { createComputedDeps } from"
  },
  {
    "path": "src/reactive/create-computed-deps.ts",
    "chars": 434,
    "preview": "import { shallowEqual } from 'react-redux';\nimport type { ComputedValue } from './computed-value';\nimport type { Deps } "
  },
  {
    "path": "src/reactive/deps-collector.ts",
    "chars": 758,
    "preview": "import type { Deps } from './object-deps';\n\nconst deps: Deps[][] = [];\nlet level = -1;\n\nexport const depsCollector = {\n "
  },
  {
    "path": "src/reactive/object-deps.ts",
    "chars": 2687,
    "preview": "import type { Store } from 'redux';\nimport { isObject } from '../utils/is-type';\nimport { depsCollector } from './deps-c"
  },
  {
    "path": "src/redux/connect.ts",
    "chars": 346,
    "preview": "import { Connect, connect as originalConnect } from 'react-redux';\nimport { ProxyContext } from './contexts';\nimport { t"
  },
  {
    "path": "src/redux/contexts.ts",
    "chars": 348,
    "preview": "import { createContext } from 'react';\nimport type { ReactReduxContextValue } from 'react-redux';\n\nexport const ModelCon"
  },
  {
    "path": "src/redux/create-reducer.ts",
    "chars": 850,
    "preview": "import type { Reducer } from 'redux';\nimport { isPostModelAction } from '../actions/model';\nimport { isRefreshAction } f"
  },
  {
    "path": "src/redux/foca-provider.tsx",
    "chars": 1197,
    "preview": "import { FC } from 'react';\nimport { Provider } from 'react-redux';\nimport { ProxyContext, ModelContext, LoadingContext "
  },
  {
    "path": "src/redux/use-selector.ts",
    "chars": 247,
    "preview": "import { createSelectorHook } from 'react-redux';\nimport { ModelContext, LoadingContext } from './contexts';\n\nexport con"
  },
  {
    "path": "src/store/loading-store.ts",
    "chars": 4677,
    "preview": "import {\n  UnknownAction,\n  applyMiddleware,\n  legacy_createStore as createStore,\n  Middleware,\n} from 'redux';\nimport t"
  },
  {
    "path": "src/store/model-store.ts",
    "chars": 5933,
    "preview": "import {\n  applyMiddleware,\n  compose,\n  legacy_createStore as createStore,\n  Middleware,\n  Reducer,\n  Store,\n  StoreEnh"
  },
  {
    "path": "src/store/proxy-store.ts",
    "chars": 327,
    "preview": "import { legacy_createStore as createStore, Store } from 'redux';\n\nexport const proxyStore = createStore(() => ({}));\n\nc"
  },
  {
    "path": "src/store/store-basic.ts",
    "chars": 892,
    "preview": "import { Store } from 'redux';\nimport { $$observable } from '../utils/symbol-observable';\n\nexport abstract class StoreBa"
  },
  {
    "path": "src/utils/deep-equal.ts",
    "chars": 926,
    "preview": "import { OBJECT } from './is-type';\n\nexport const deepEqual = (a: any, b: any): boolean => {\n  if (a === b) return true;"
  },
  {
    "path": "src/utils/get-method-category.ts",
    "chars": 116,
    "preview": "export const getMethodCategory = (methodName: string) =>\n  methodName.indexOf('_') === 0 ? 'internal' : 'external';\n"
  },
  {
    "path": "src/utils/getter.ts",
    "chars": 1318,
    "preview": "import { toArgs } from './to-args';\n\nexport function composeGetter<\n  T extends object,\n  U1 extends (...args: any[]) =>"
  },
  {
    "path": "src/utils/immer.ts",
    "chars": 269,
    "preview": "import { Immer, enableES5 } from 'immer';\n\n/**\n * 支持ES5,毕竟Proxy无法polyfill。有些用户手机可以10年不换!!\n * @link https://immerjs.githu"
  },
  {
    "path": "src/utils/is-promise.ts",
    "chars": 315,
    "preview": "import { FUNCTION, isFunction, isObject } from './is-type';\n\nconst hasPromise = typeof Promise === FUNCTION;\n\nexport con"
  },
  {
    "path": "src/utils/is-type.ts",
    "chars": 542,
    "preview": "export const OBJECT = 'object';\nexport const FUNCTION = 'function';\n\nexport const isFunction = <T extends Function>(valu"
  },
  {
    "path": "src/utils/serialize.ts",
    "chars": 743,
    "preview": "import { isObject } from './is-type';\n\nconst JSON_UNDEFINED = '__JSON_UNDEFINED__';\n\nconst replacer = (_key: string, val"
  },
  {
    "path": "src/utils/symbol-observable.ts",
    "chars": 355,
    "preview": "import { FUNCTION } from './is-type';\n\n/**\n * Inlined version of the `symbol-observable` polyfill\n * @link https://githu"
  },
  {
    "path": "src/utils/to-args.ts",
    "chars": 155,
    "preview": "const slice = Array.prototype.slice;\n\nexport const toArgs = <T = any[]>(args: IArguments, start?: number): T =>\n  slice."
  },
  {
    "path": "src/utils/to-promise.ts",
    "chars": 112,
    "preview": "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",
    "chars": 457,
    "preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`can clone null and undefined 1`] = `\"{\"x\":[\"__JS"
  },
  {
    "path": "test/action-in-action.test.tsx",
    "chars": 1576,
    "preview": "import { act, render } from '@testing-library/react';\nimport { FC, useEffect, version } from 'react';\nimport sleep from "
  },
  {
    "path": "test/build.test.ts",
    "chars": 754,
    "preview": "import { writeFileSync } from 'fs';\nimport { execSync, exec } from 'child_process';\n\nfunction testFile(filename: string,"
  },
  {
    "path": "test/clone.test.ts",
    "chars": 2735,
    "preview": "import { defineModel, cloneModel, store } from '../src';\nimport type { InternalModel } from '../src/model/types';\nimport"
  },
  {
    "path": "test/computed.test.ts",
    "chars": 10832,
    "preview": "import { defineModel, store } from '../src';\nimport { ComputedValue } from '../src/reactive/computed-value';\nimport { de"
  },
  {
    "path": "test/connect.test.tsx",
    "chars": 1712,
    "preview": "import { FC } from 'react';\nimport { act, render, screen } from '@testing-library/react';\nimport { store, connect, FocaP"
  },
  {
    "path": "test/deep-equal.test.ts",
    "chars": 434,
    "preview": "import { deepEqual } from '../src/utils/deep-equal';\nimport { equals } from './fixtures/equals';\nimport { notEquals } fr"
  },
  {
    "path": "test/engine.test.ts",
    "chars": 1978,
    "preview": "import 'fake-indexeddb/auto';\nimport localforage from 'localforage';\nimport ReactNativeStorage from '@react-native-async"
  },
  {
    "path": "test/fixtures/equals.ts",
    "chars": 956,
    "preview": "export const equals: Record<string, { a: any; b: any }> = {\n  '0 and -0': {\n    a: 0,\n    b: -0,\n  },\n  'NaN': {\n    a: "
  },
  {
    "path": "test/fixtures/not-equals.ts",
    "chars": 1670,
    "preview": "export const notEquals: Record<string, { a: any; b: any }> = {\n  '0 and NaN': {\n    a: 0,\n    b: NaN,\n  },\n  '0 and null"
  },
  {
    "path": "test/get-loading.test.ts",
    "chars": 2214,
    "preview": "import sleep from 'sleep-promise';\nimport { defineModel, getLoading, store } from '../src';\nimport { basicModel } from '"
  },
  {
    "path": "test/helpers/render-hook.tsx",
    "chars": 301,
    "preview": "import { renderHook as originRenderHook } from '@testing-library/react';\nimport { FocaProvider } from '../../src';\n\nexpo"
  },
  {
    "path": "test/helpers/slow-engine.ts",
    "chars": 623,
    "preview": "import type { StorageEngine } from '../../src';\nimport { toPromise } from '../../src/utils/to-promise';\n\nlet cache: Part"
  },
  {
    "path": "test/lifecycle.test.ts",
    "chars": 6038,
    "preview": "import sleep from 'sleep-promise';\nimport { cloneModel, defineModel, memoryStorage, store } from '../src';\nimport { Pers"
  },
  {
    "path": "test/middleware.test.ts",
    "chars": 4213,
    "preview": "import { defineModel, getLoading, store } from '../src';\nimport { DestroyLoadingAction, DESTROY_LOADING } from '../src/a"
  },
  {
    "path": "test/model.test.ts",
    "chars": 5627,
    "preview": "import { defineModel, store } from '../src';\nimport { basicModel } from './models/basic.model';\n\nbeforeEach(() => {\n  st"
  },
  {
    "path": "test/models/basic.model.ts",
    "chars": 1632,
    "preview": "import sleep from 'sleep-promise';\nimport { cloneModel, defineModel } from '../../src';\n\nconst initialState: {\n  count: "
  },
  {
    "path": "test/models/complex.model.ts",
    "chars": 601,
    "preview": "import { defineModel } from '../../src';\n\nconst initialState: {\n  users: Record<number, string>;\n  ids: Array<number>;\n}"
  },
  {
    "path": "test/models/computed.model.ts",
    "chars": 2171,
    "preview": "import { defineModel } from '../../src';\n\nconst initialState: {\n  firstName: string;\n  lastName: string;\n  statusList: ["
  },
  {
    "path": "test/models/persist.model.ts",
    "chars": 760,
    "preview": "import { cloneModel, defineModel } from '../../src';\n\nconst initialState: {\n  counter: number;\n} = {\n  counter: 0,\n};\n\ne"
  },
  {
    "path": "test/persist.gate.test.tsx",
    "chars": 2366,
    "preview": "import { FC } from 'react';\nimport { act, render, screen } from '@testing-library/react';\nimport { FocaProvider, store }"
  },
  {
    "path": "test/persist.test.ts",
    "chars": 12904,
    "preview": "import sleep from 'sleep-promise';\nimport {\n  defineModel,\n  memoryStorage,\n  Model,\n  StorageEngine,\n  store,\n} from '."
  },
  {
    "path": "test/provider.test.tsx",
    "chars": 623,
    "preview": "import { render, screen } from '@testing-library/react';\nimport { FocaProvider, store } from '../src';\n\nbeforeEach(() =>"
  },
  {
    "path": "test/serialize.test.ts",
    "chars": 944,
    "preview": "import { parseState, stringifyState } from '../src/utils/serialize';\n\nit('can clone basic data', () => {\n  expect(\n    p"
  },
  {
    "path": "test/store.test.ts",
    "chars": 8507,
    "preview": "import { compose, StoreEnhancer } from 'redux';\nimport sleep from 'sleep-promise';\nimport { from, map } from 'rxjs';\nimp"
  },
  {
    "path": "test/typescript/computed.check.ts",
    "chars": 1953,
    "preview": "import { expectType } from 'ts-expect';\nimport { cloneModel, defineModel, useComputed } from '../../src';\nimport { Compu"
  },
  {
    "path": "test/typescript/define-model.check.ts",
    "chars": 6098,
    "preview": "import { expectType } from 'ts-expect';\nimport { UnknownAction, defineModel } from '../../src';\n\n// @ts-expect-error\ndef"
  },
  {
    "path": "test/typescript/get-loading.check.ts",
    "chars": 557,
    "preview": "import { expectType } from 'ts-expect';\nimport { getLoading } from '../../src';\nimport { basicModel } from '../models/ba"
  },
  {
    "path": "test/typescript/persist.check.ts",
    "chars": 2063,
    "preview": "import { TypeEqual, expectType } from 'ts-expect';\nimport { cloneModel, defineModel } from '../../src';\nimport { GetInit"
  },
  {
    "path": "test/typescript/use-isolate.check.ts",
    "chars": 1027,
    "preview": "import { TypeEqual, expectType } from 'ts-expect';\nimport { defineModel, useIsolate, useLoading, useModel } from '../../"
  },
  {
    "path": "test/typescript/use-loading.check.ts",
    "chars": 872,
    "preview": "import { expectType } from 'ts-expect';\nimport { useLoading } from '../../src';\nimport { basicModel } from '../models/ba"
  },
  {
    "path": "test/typescript/use-model.check.ts",
    "chars": 716,
    "preview": "import { expectType } from 'ts-expect';\nimport { useModel } from '../../src';\nimport { basicModel } from '../models/basi"
  },
  {
    "path": "test/use-computed.test.ts",
    "chars": 1128,
    "preview": "import { act } from '@testing-library/react';\nimport { renderHook } from './helpers/render-hook';\nimport { store, useCom"
  },
  {
    "path": "test/use-isolate.test.tsx",
    "chars": 5479,
    "preview": "import { act, cleanup, render } from '@testing-library/react';\nimport { useEffect, useState } from 'react';\nimport sleep"
  },
  {
    "path": "test/use-loading.test.ts",
    "chars": 2231,
    "preview": "import { act } from '@testing-library/react';\nimport { renderHook } from './helpers/render-hook';\nimport { store, useLoa"
  },
  {
    "path": "test/use-model.test.ts",
    "chars": 4702,
    "preview": "import { act } from '@testing-library/react';\nimport { renderHook } from './helpers/render-hook';\nimport { store, useMod"
  },
  {
    "path": "tsconfig.json",
    "chars": 991,
    "preview": "{\n  \"compilerOptions\": {\n    // https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping\n    \"target\": \"ES2015\","
  },
  {
    "path": "tsup.config.ts",
    "chars": 372,
    "preview": "import { defineConfig } from 'tsup';\n\nexport default defineConfig({\n  entry: ['src/index.ts'],\n  splitting: true,\n  sour"
  },
  {
    "path": "vitest.config.ts",
    "chars": 542,
    "preview": "/// <reference types='vitest/globals' />\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  "
  }
]

About this extraction

This page contains the full source code of the foca-js/foca GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 120 files (255.9 KB), approximately 77.9k tokens, and a symbol index with 292 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!