{"id":23634791,"url":"https://github.com/filefoxper/agent-reducer","last_synced_at":"2025-08-31T10:30:34.055Z","repository":{"id":39588426,"uuid":"271449503","full_name":"filefoxper/agent-reducer","owner":"filefoxper","description":"This is a model container for javascript or typescript app ","archived":false,"fork":false,"pushed_at":"2022-12-07T03:12:27.000Z","size":719,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-08-13T06:03:07.279Z","etag":null,"topics":["agent","class","class-reducer","javascript","middleware","model","model-management","model-sharing","reducer","state","state-management","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/filefoxper.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGE_LOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-06-11T04:16:03.000Z","updated_at":"2023-12-20T06:53:54.000Z","dependencies_parsed_at":"2023-01-24T13:16:09.413Z","dependency_job_id":null,"html_url":"https://github.com/filefoxper/agent-reducer","commit_stats":null,"previous_names":[],"tags_count":44,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fagent-reducer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fagent-reducer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fagent-reducer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/filefoxper%2Fagent-reducer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/filefoxper","download_url":"https://codeload.github.com/filefoxper/agent-reducer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":231587568,"owners_count":18396514,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","class","class-reducer","javascript","middleware","model","model-management","model-sharing","reducer","state","state-management","typescript"],"created_at":"2024-12-28T05:19:48.452Z","updated_at":"2024-12-28T05:19:48.900Z","avatar_url":"https://github.com/filefoxper.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm][npm-image]][npm-url]\n[![NPM downloads][npm-downloads-image]][npm-url]\n[![standard][standard-image]][standard-url]\n\n[npm-image]: https://img.shields.io/npm/v/agent-reducer.svg?style=flat-square\n[npm-url]: https://www.npmjs.com/package/agent-reducer\n[standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square\n[standard-url]: http://npm.im/standard\n[npm-downloads-image]: https://img.shields.io/npm/dm/agent-reducer.svg?style=flat-square\n\n# agent-reducer\n\n`agent-reducer` is a model container for Javascript apps.\n\nIt helps you write applications with a micro `mvvm` pattern and provides a great developer experience, you can see details [here](https://filefoxper.github.io/agent-reducer/#/).\n\n## Other language\n\n[中文](https://github.com/filefoxper/agent-reducer/blob/master/README_zh.md)\n\n## Basic usage\n\nLet's have some examples to learn how to use it. \n\nThe example below is a counter, we can increase or decrease the state.\n\n```typescript\nimport { \n    effect, \n    Flows,\n    create, \n    act, \n    strict, \n    flow, \n    Model \n} from \"agent-reducer\";\n\ndescribe(\"basic\", () =\u003e {\n  // a class model template for managing a state\n  class Counter implements Model\u003cnumber\u003e {\n    // state of this model\n    state: number = 0;\n\n    // a method for generating a next state\n    increase() {\n      // keyword `this` represents model instance, like: new Counter()\n      return this.state + 1;\n    }\n\n    decrease() {\n      const nextState = this.state - 1;\n      if (nextState \u003c 0) {\n        // use another method for help\n        return this.reset();\n      }\n      return nextState;\n    }\n\n    reset() {\n      return 0;\n    }\n  }\n\n  test(\"call method from agent can change state\", () =\u003e {\n    // 'agent' is an avatar object from model class,\n    // call method from 'agent' can lead a state change\n    const { agent, connect, disconnect } = create(Counter);\n    connect();\n    // 'increase' method is from 'agent',\n    // and returns a new state for model.\n    agent.increase();\n    // model state is changed to 1\n    // We call these state change methods 'action methods'.\n    expect(agent.state).toBe(1);\n    disconnect();\n  });\n\n  test(\"only the method get from agent object directly, can change state\", () =\u003e {\n    const actionTypes: string[] = [];\n    const { agent, connect, disconnect } = create(Counter);\n    connect(({ type }) =\u003e {\n      // record action type, when state is changed\n      actionTypes.push(type);\n    });\n    // 'decrease' method is from 'agent',\n    // and returns a new state for model.\n    agent.decrease();\n    // model state is changed to 0\n    expect(agent.state).toBe(0);\n    // the 'reset' method called in 'decrease' method,\n    // it is not from 'agent',\n    // so, it can not lead a state change itself,\n    // and it is not an action method in this case.\n    expect(actionTypes).toEqual([\"decrease\"]);\n    disconnect();\n  });\n});\n    \n```\n\nThe operation is simple:\n\n1. create `agent` object\n2. connect\n3. call method from `agent` object\n4. the method called yet can use what it `returns` to change model state (this step is automatic)\n5. disconnect\n\nIt works like a redux reducer, that is why it names `agent-reducer`.\n\nLet's see a more complex example, and we will use it to manage a filterable list actions.\n\n```typescript\nimport { \n    effect, \n    Flows,\n    create, \n    act, \n    strict, \n    flow, \n    Model \n} from \"agent-reducer\";\n\ndescribe(\"use flow\", () =\u003e {\n  type State = {\n    sourceList: string[];\n    viewList: string[];\n    keyword: string;\n  };\n\n  const remoteSourceList = [\"1\", \"2\", \"3\", \"4\", \"5\"];\n\n  class List implements Model\u003cState\u003e {\n    state: State = {\n      sourceList: [],\n      viewList: [],\n      keyword: \"\",\n    };\n\n    // for changing sourceList,\n    // which will be used for filtering viewList\n    private changeSourceList(sourceList: string[]): State {\n      return { ...this.state, sourceList};\n    }\n\n    // for changing viewList\n    private changeViewList(viewList: string[]): State {\n      return { ...this.state, viewList };\n    }\n\n    // for changing keyword,\n    // which will be used for filtering viewList\n    changeKeyword(keyword: string): State {\n      return { ...this.state, keyword };\n    }\n\n    // fetch remote sourceList\n    // `flow` decorator can make a flow method,\n    // in flow method, keyword `this` is an agent object,\n    // so, you can call action method to change state.\n    @flow()\n    async fetchSourceList() {\n      // fetch remote sourceList\n      const sourceList = await Promise.resolve(remoteSourceList);\n      // keyword `this` represents an agent object in flow method,\n      // `changeSourceList` is from this agent object,\n      // and it is marked as an action method,\n      // so, it can change state.\n      this.changeSourceList(sourceList);\n    }\n\n    // effect of action methods: changeSourceList, changeKeyword for filtering viewList.\n    // `effect` decorator makes an effect method,\n    // the effect method can be used for listening the state change from action methods.\n    // effect method is a special flow method, it can not be called manually.\n    // We can add a flow mode by using `flow` decorator with effect,\n    // now, we have told the effect method works in a debounce mode with 100 ms\n    @flow(Flows.debounce(100))\n    @effect(() =\u003e [\n      // listen to action method `changeSourceList`\n      List.prototype.changeSourceList,\n      // listen to action method `changeKeyword`\n      List.prototype.changeKeyword,\n    ])\n    private effectForFilterViewList() {\n      const { sourceList, keyword } = this.state;\n      // filter out the viewList\n      const viewList = sourceList.filter((content) =\u003e\n        content.includes(keyword.trim())\n      );\n      // use action method `changeViewList` to change state\n      this.changeViewList(viewList);\n    }\n  }\n\n  test(\"flow method is used for composing action methods together to resolve more complex works\", async () =\u003e {\n    const { agent, connect, disconnect } = create(List);\n    connect();\n    // use flow to fetch remote sourceList\n    await agent.fetchSourceList();\n    expect(agent.state.sourceList).toEqual(remoteSourceList);\n    disconnect();\n  });\n\n  test('effect method can listen to the state change of action methods',async ()=\u003e{\n    const { agent, connect, disconnect } = create(List);\n    connect();\n    // use flow to fetch remote sourceList\n    await agent.fetchSourceList();\n    // change sourceList, the effect method `effectForFilterViewList` will start after 100 ms\n    expect(agent.state.sourceList).toEqual(remoteSourceList);\n    // change keyword,\n    // the effect method `effectForFilterViewList` will cancel itself,\n    // then start after 100 ms\n    agent.changeKeyword('1');\n    await new Promise((r)=\u003esetTimeout(r,110));\n    // effect `effectForFilterViewList` filter out the viewList\n    expect(agent.state.sourceList).toEqual(remoteSourceList);\n    expect(agent.state.viewList).toEqual(['1']);\n    disconnect();\n  })\n});\n```\n\nThe example above uses decorators like `@flow` and `@effect` to make a list manage model, which can fetch list from remote service and filter by keywords.\n\n## Share state change synchronously\n\n`agent-reducer` stores state, caches, listeners in the model instance, so you can share state change synchronously between two or more different agent objects from the same model instance.\n\n```typescript\nimport {\n    create,\n    Action,\n    Model\n} from 'agent-reducer';\n\ndescribe('update by observing another agent',()=\u003e{\n\n    // this is a counter model,\n    // we can increase or decrease its state\n    class Counter implements Model\u003cnumber\u003e {\n\n        state = 0;  // initial state\n\n        // consider what the method returns as a next state for model\n        stepUp = (): number =\u003e this.state + 1;\n\n        stepDown = (): number =\u003e this.state - 1;\n\n        step(isUp: boolean):number{\n            return isUp ? this.stepUp() : this.stepDown();\n        }\n\n    }\n\n    const counter = new Counter();\n\n    test('an agent can share state change with another one, if they share a same model instance',()=\u003e{\n        // we create two listeners `dispatch1` and `dispatch2` for different agent reducer function\n        const dispatch1 = jest.fn().mockImplementation((action:Action)=\u003e{\n            // the agent action contains a `state` property,\n            // this state is what the model state should be now.\n            expect(action.state).toBe(1);\n        });\n        const dispatch2 = jest.fn().mockImplementation((action:Action)=\u003e{\n            expect(action.state).toBe(1);\n        });\n        // use create api,\n        // you can create an `Agent` object from its `Model`\n        const reducer1 = create(counter);\n        const reducer2 = create(counter);\n        // before call the methods,\n        // you need to connect it first,\n        // you can add a listener to listen the agent action,\n        // by using connect function\n        reducer1.connect(dispatch1);\n        reducer2.connect(dispatch2);\n        // calling result which is returned by method `stepUp` will be next state.\n        // then reducer1.agent will notify state change to reducer2.agent.\n        reducer1.agent.stepUp();\n\n        expect(dispatch1).toBeCalled();     // dispatch1 work\n        expect(dispatch2).toBeCalled();     // dispatch2 work\n        expect(counter.state).toBe(1);\n    });\n\n});\n\n```\n\nThis example may not easy for understanding, but consider if we use this feature in a view library like React, we can update state synchronously between different components without `props` or `context`， and these components will rerender synchronously. You can use it easily with its React connnector [use-agent-reducer](https://www.npmjs.com/package/use-agent-reducer).\n\n## Connector\n\n* [use-agent-reducer](https://www.npmjs.com/package/use-agent-reducer)\n\n## Document\n\nIf you want to learn more, you can go into our [document](https://filefoxper.github.io/agent-reducer/#/) for more details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffilefoxper%2Fagent-reducer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffilefoxper%2Fagent-reducer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffilefoxper%2Fagent-reducer/lists"}