{"id":21107023,"url":"https://github.com/buildo/avenger","last_synced_at":"2025-07-08T16:31:29.799Z","repository":{"id":33431383,"uuid":"37076692","full_name":"buildo/avenger","owner":"buildo","description":"A CQRS-flavoured data fetching and caching layer in TypeScript.  Batching, caching, data-dependencies and manual invalidations in a declarative fashion for Node and the browser","archived":false,"fork":false,"pushed_at":"2021-11-05T11:51:25.000Z","size":1180,"stargazers_count":59,"open_issues_count":12,"forks_count":1,"subscribers_count":13,"default_branch":"master","last_synced_at":"2023-04-10T01:34:31.451Z","etag":null,"topics":[],"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/buildo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-06-08T15:50:57.000Z","updated_at":"2023-04-10T01:34:31.453Z","dependencies_parsed_at":"2022-08-25T14:41:27.410Z","dependency_job_id":null,"html_url":"https://github.com/buildo/avenger","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buildo%2Favenger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buildo%2Favenger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buildo%2Favenger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buildo%2Favenger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/buildo","download_url":"https://codeload.github.com/buildo/avenger/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225449667,"owners_count":17476094,"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":[],"created_at":"2024-11-20T00:35:21.895Z","updated_at":"2024-11-20T00:35:22.538Z","avatar_url":"https://github.com/buildo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Avenger is a data fetching and caching layer written in TypeScript. Its API is designed to mirror the principles of **Command Query Responsibility Segregation** and facilitate their adoption (if you are new to the concept you can get a grasp of its foundations in [this nice article](https://martinfowler.com/bliki/CQRS.html) by Martin Fowler).\n\nBuilding user interfaces is a complex task, mainly because of its `IO` intensive nature. Reads (**queries**) and updates (**commands**) toward \"external\" data sources are ubiquitous and difficult to orchestrate, but _orchestration_ is not the only challenge a UI developer faces, _performance_ and _scalability_ are also key aspects of good design.\n\nWe believe that an _effective and powerful abstraction to handle caching and synchronization of external data in a declarative way_ is of fundamental importance when designing a solid user interface.\n\nThis is what **Avenger** aims to be: an abstraction layer over external data that handles caching and synchronization for you:\n\n![\"cached flow\"](docs/Avenger.svg)\n\nBy separating how we fetch external data and how we update it we are able to state in a very declarative and _natural_ way the correct lifecycle of that data:\n\n```tsx\nimport { queryStrict, command } from 'avenger';\n\n// define a cached query, with strategy \"available\" (more about this later)\nconst user = queryStrict((id: string) =\u003e API.fetchUser(id), available);\n// define a command that invalidates the previous query\nconst updateUsername = command(\n  (patch: Partial\u003cUser\u003e) =\u003e API.updateUser(patch),\n  { user }\n);\n\n// declare it for usage in a React component\nimport { pipe } from 'fp-ts/lib/pipeable';\nimport * as QR from 'avenger/lib/QueryResult';\nimport { declareQueries } from 'avenger/lib/react';\n\nconst queries = declareQueries({ user });\nconst Username = queries(props =\u003e (\n  \u003cdiv\u003e\n    {pipe(\n      props.queries,\n      QR.fold(\n        () =\u003e 'loading...',\n        () =\u003e 'error while retrieving user',\n        queries =\u003e (\n          \u003cUserNameForm\n            value={queries.user.username}\n            onSubmit={updateUsername}\n          /\u003e\n        )\n      )\n    )}\n  \u003c/div\u003e\n));\n\n// render the component\n\u003cUsername queries={{ user: '42' }} /\u003e;\n```\n\n# Avenger\n\nAt the very heart of Avenger's DSL there are two constructors: **query** and **command**.\n\n## queries\n\nThe [**`query`**](#query) function allows you to query your data source and get an object of type [**`CachedQuery`**](#CachedQuery) in return.\nIt accepts two parameters: the first is a function with a [**`Fetch`**](#Fetch) signature that is used to retrieve data from your data source; the second is an object with the [**`Strategy`**](#Strategy) signature that will be used to decide if the data stored by **Avenger** is still relevant or needs to be refetched.\n\nAlthough important, `query` is a pretty low-level API and **Avenger** offers some convenient utils with a [**`StrategyBuilder`**](#StrategyBuilder) signature that you should prefer over it (unless you have very specific needs):\n\n- **refetch:** runs the fetch function every time the data is requested (unless there's an ongoing pending request, which is always reused).\n- **expire:** when the data is requested, the fetch function is run only if data in the `Cache` is older than the expiration defined, otherwise the cached value is used.\n- **available:** when the data is requested, if a cached value is available it is always returned, otherwise the fetch function is run and the result stored in the `Cache` accordingly.\n\nAll these utils ask you to pass custom [**`Eq`**](https://github.com/gcanti/fp-ts/blob/master/docs/modules/Eq.ts.md) instances as arguments; they will be used to check if a value for an input combination is already present in one of the `Cache`'s keys (if the check is successful `Avenger` will try to use that value, otherwise it will resort to the `Fetch` function).\nYou can (and should) use these utils together with one of the built-in implementations that automatically take care of passing by the needed `Eq`s:\n\n- **queryShallow:** will use an `Eq` instance that performs a shallow equality check to compare inputs.\n- **queryStrict:** will use an `Eq` instance that performs a strict equality check to compare inputs.\n\nSome examples will help clarify:\n\n```ts\n/*\n  this implementation will always re-run the `Fetch` function\n  even if valid cached data is already present\n  and use shallow equality to compare input\n*/\nconst myQuery = queryShallow(fetchFunction, refetch);\n\n/*\n  this implementation will never run the `Fetch` function\n  unless no valid data is present in the Cache\n  and use strict equality to compare input\n*/\nconst myQuery = queryStrict(fetchFunction, available);\n\n/*\n  this implementation will run the `Fetch` function only if no valid data is present in the Cache\n  or t \u003e 10000 ms passed till the last time data was fetched\n  and use strict equality to compare input\n*/\nconst myQuery = queryStrict(fetchFunction, expire(10000));\n```\n\nEach time the `Fetch` function is run with some `input`, those same `input` is used as a `key` to store the result obtained:\n\n```\n// usersCache is empty\nusersCache: {}\n\n//a user is fetched\ngetUser({ userId: 1 }) -\u003e { userName: \"Mario\" }\n\n// usersCache is now populated\nusersCache: {\n  [{ userId: 1 }]: { userName: Mario }\n}\n```\n\nFrom that moment onwards, when **Avenger** will need to decide if the data in our [**`Cache`**](#Cache) is present and still valid it will:\n\n1. attempt to retrieve data from the [**`Cache`**](#Cache)\n2. match the result against the cache strategy defined (for instance if we chose `refetch` the data will always be deemed invalid irrespective of the result).\n\nIf a valid result is found it is used without further actions, otherwise the `Fetch` function will be re-run in order to get valid data. The two flows are relatively simple:\n\n##### Valid CacheValue\n\n![\"cached flow\"](docs/CachedValue.svg)\n\n##### Invalid CacheValue\n\nwhen you call `run` or `subscribe` on a `query` with a combination of `inputs` that was never used before (or whose last use ended up with a `Failure`), avenger will try to run the `Fetch` function resulting in a more complex flow:\n![\"cached flow\"](docs/UncachedOrErrorValue.svg)\n\n## listening to queries\n\nThere are two ways to get a query result:\n\n```ts\ntype Error = '500' | '404';\ntype User = { userName: String };\n\ndeclare function getUser(userId: number): TaskEither\u003cError, User\u003e;\n\nconst userQuery: CachedQuery\u003cnumber, Error, User\u003e = query(getUser)(refetch);\n\ndeclare function dispatchError(e: Error): void;\ndeclare function setCurrentUser(e: User): void;\n\n// feeding your query to `observe` will give you an observable on the query\n// N.B. until now no fetch is yet attempted, avenger will wait until the first subscription is issued\nconst observable: Observable\u003cQueryResult\u003cError, User\u003e\u003e = observe(userQuery);\n\n// this will trigger the fetch function\nobservable.subscribe(dispatchError, setCurrentUser);\n\n// alternatively you can call `run` on your query and it will return a TaskEither\u003cError, User\u003e\n// you can then use it imperatively\nconst task: TaskEither\u003cError, User\u003e = userQuery.run(1);\nconst result: Either\u003cError, User\u003e = await task();\n```\n\nalthough the `run` method is available to check a query result imperatively, it is highly suggested the use of the `observe` utility in order to be notified in real time of when data changes.\n\nEither way, whenever you ask for a query result you will end up with an object with the [**`QueryResult`**](#QueryResult) signature that conveniently lets you `fold` to decide the best way to handle the result. The `fold` method takes three functions as parameters: the first is used to handle a `Loading` result; the second is used in case a `Failure` occurs; the last one handles `Success` values.\n\n## composing queries\n\nYou can build bigger queries from smaller ones in two ways:\n\n- by composing them with [**`compose`**](#compose): when you need your queries to be sequentially run with the results of one feeding the other, you can use `compose`.\n- by grouping them with [**`product`**](#product): when you don't need to run the queries sequentially but would like to conveniently group them and treat them as if they were one you can use `product`\\*.\n\n\\*Internally `product` uses the `Applicative` nature of `QueryResults` to group them using the following hierarchical logic:\n\n1. If any of the queries returned a `Failure` then the whole composition is a `Failure`.\n2. If any of the queries is `Loading` then the whole composition is `Loading`.\n3. If all the queries ended with a `Success` then the composition is a `Success` with a record of results that mirrors the key/value result of the single queries as value.\n\nHere are a couple of simple examples on how to use them:\n\n```ts\n/* N.B. each value defined is explicitly annotated for clarity, although the annotations are not strictly required */\n\nimport { compose } from 'avenger/lib/Query';\n\ntype UserPreferences = { color: string };\n\n// note that the two ends of the composed functions must have compatible types\ndeclare function getUser(userId: number): TaskEither\u003cError, User\u003e;\ndeclare function getUserPreferences(\n  user: User\n): TaskEither\u003cError, UserPreferences\u003e;\n\nconst userQuery: CachedQuery\u003cnumber, Error, User\u003e = queryStrict(\n  getUser,\n  refetch\n);\n\nconst preferencesQuery: CachedQuery\u003c\n  User,\n  Error,\n  UserPreferences\n\u003e = queryShallow(getUserPreferences, refetch);\n\n// this is a query composition\nconst composition: Composition\u003cnumber, Error, UserPreferences\u003e = compose(\n  userQuery,\n  preferencesQuery\n);\n\n// this is a query product\nconst group: Product\u003cnumber, Error, UserPreferences\u003e = product({\n  myQuery,\n  myQuery2\n});\n```\n\n# commands\n\nUp to now we only described how to fetch data. When you need to update or insert data remotely you can make use of [**`command`**](#command):\n\n```ts\ndeclare function updateUserPreferences({\n  color: string\n}): TaskEither\u003cError, void\u003e;\n\nconst updatePreferencesCommand = command(updateUserPreferences, {\n  preferencesQuery\n});\n```\n\n`command` accepts a `Fetch` function that will be used to modify the remote data source and, as a second optional parameter, a record of `query`es that will be invalidated once the `command` is successfully run:\n\n```ts\n/* when you call the command you can specify the input value corresponding\nto the Cache key that should be invalidated as a second parameter */\nupdatePreferencesCommand({ color: 'acquamarine' }, { preferencesQuery: 1 });\n```\n\n# React\n\nAvenger also exports some utilities to use with `React`.\n\n## declareQueries\n\n`declareQueries` is a `HOC` (Higher-Order Component) builder. It lets you define the queries that you want to inject into a component and then creates a simple `HOC` to wrap it:\n\n```tsx\nimport { pipe } from 'fp-ts/lib/pipeable';\nimport { declareQueries } from 'avenger/lib/react';\nimport * as QR from 'avenger/lib/QueryResult';\nimport { userPreferences } from './queries';\n\nconst queries = declareQueries({ userPreferences });\n\nclass MyComponent extends React.PureComponent\u003cProps, State\u003e {\n  render() {\n    return pipe(\n      this.props.queries,\n      QR.fold(\n        () =\u003e \u003cp\u003eloading\u003c/p\u003e,\n        () =\u003e \u003cp\u003ethere was a problem when fetching preferences\u003c/p\u003e,\n        ({ userPreferences }) =\u003e \u003cp\u003emy favourite color is {userPreferences.color}\u003c/p\u003e\n      )\n    )\n  }\n}\n\nexport queries(MyComponent)\n```\n\nWhen using this component from outside you will have to pass it the correct query parameters inside the `queries` prop in order for it to load the declared queries:\n\n```ts\nclass MyOtherComponent extends React.PureComponent\u003cProps, State\u003e {\n  render() {\n    return (\n      \u003cMyComponent\n        queries={{\n          userPreferences: { userName: 'Mario' }\n        }}\n      /\u003e\n    );\n  }\n}\n```\n\n## WithQueries\n\nalternatively, to avoid unecessary boilerplate, you can use the `WithQueries` component:\n\n```tsx\nimport * as QR from 'avenger/lib/QueryResult';\nimport { WithQueries } from 'avenger/lib/react';\nimport { userPreferences } from './queries';\n\nclass MyComponent extends React.PureComponent\u003cProps, State\u003e {\n  render() {\n    return (\n      \u003cWithQueries\n        queries={{ userPreferences }}\n        params={{ userPreferences: { userName: 'Mario' } }}\n        render={QR.fold(\n          () =\u003e (\n            \u003cp\u003eloading\u003c/p\u003e\n          ),\n          () =\u003e (\n            \u003cp\u003ethere was a problem when fetching preferences\u003c/p\u003e\n          ),\n          ({ userPreferences }) =\u003e (\n            \u003cp\u003eMario's favourite color is {userPreferences.color}\u003c/p\u003e\n          )\n        )}\n      /\u003e\n    );\n  }\n}\n```\n\n**NB** both `declareQueries` and `WithQueries` do not support dynamic queries definition (e.g. `declareQueries(someCondition ? { queryA } : { queryA, queryB }` will not work).\n\n## useQuery\n\nalternatively, to avoid unecessary boilerplate, you can use the `useQuery` and `useQueries` hooks:\n\n```tsx\nimport { pipe } from 'fp-ts/lib/pipeable';\nimport * as QR from 'avenger/lib/QueryResult';\nimport { useQuery } from 'avenger/lib/react';\nimport { userPreferences } from './queries';\n\nconst MyComponent: React.FC\u003c{ userName: string }\u003e = props =\u003e {\n  return pipe(\n    useQuery(userPreferences, { userName: props.userName }),\n    QR.fold(\n      () =\u003e \u003cp\u003eloading\u003c/p\u003e,\n      () =\u003e \u003cp\u003ethere was a problem when fetching preferences\u003c/p\u003e,\n      userPreferences =\u003e (\n        \u003cp\u003e\n          {props.userName}'s favourite color is {userPreferences.color}\n        \u003c/p\u003e\n      )\n    )\n  );\n};\n```\n\n## useQueries\n\n```tsx\nimport { pipe } from 'fp-ts/lib/pipeable';\nimport * as QR from 'avenger/lib/QueryResult';\nimport { useQueries } from 'avenger/lib/react';\n\ndeclare const query1: ObservableQuery\u003cstring, unknown, number\u003e;\ndeclare const query2: ObservableQuery\u003cvoid, unknown, string\u003e;\n\nconst MyComponent: React.FC = props =\u003e {\n  return pipe(\n    useQueries({ query1, query2 }, { query1: 'query-1-input' }),\n    QR.fold(\n      () =\u003e \u003cp\u003estill loading query1 or query2 (or both)\u003c/p\u003e,\n      () =\u003e \u003cp\u003ethere was a problem when fetching either query1 or query2\u003c/p\u003e,\n      ({ query1, query2 }) =\u003e (\n        \u003cp\u003e\n          {query2}: {query1}\n        \u003c/p\u003e\n      )\n    )\n  );\n};\n```\n\n**NB** both `useQuery` and `useQueries` support dynamic queries definition (e.g. `useQueries(someCondition ? { queryA } : { queryA, queryB }` will work as expected).\n\n# Navigation\n\nAnother useful set of utilities is the one used to handle client navigation in the browser. Following you can find a simple but exhaustive example of how it is used:\n\n```ts\nimport { getCurrentView, getDoUpdateCurrentView } from \"avenger/lib/browser\";\n\nexport type CurrentView =\n  | { view: 'itemView'; itemId: String }\n  | { view: 'items' };\n  | { view: 'home' };\n\nconst itemViewRegex = /^\\/items\\/([^\\/]+)$/;\nconst itemsRegex = /^\\/items$/;\n\nexport function locationToView(location: HistoryLocation): CurrentView {\n  const itemViewMatch = location.pathname.match(itemViewRegex);\n  const itemsMatch = location.pathname.match(itemsRegex);\n\n  if (itemViewMatch) {\n    return { view: 'itemView'; itemId: itemViewMatch[1] };\n  } else if (itemsMatch) {\n    return { view: 'items' };\n  } else {\n    return { view: 'home' };\n  }\n}\n\nexport function viewToLocation(view: CurrentView): HistoryLocation {\n  switch (view.view) {\n    case 'itemView':\n      return { pathname: `/items/${view.itemId}`, search: {} };\n    case 'items':\n      return { pathname: '/items', search: {} };\n    case 'home':\n      return { pathname: '/home', search: {} };\n  }\n}\n\nexport const currentView = getCurrentView(locationToView); // ObservableQuery\nexport const doUpdateCurrentView = getDoUpdateCurrentView(viewToLocation); // Command\n```\n\nonce you instantiated all the boilerplate needed to instruct Avenger on how to navigate, you can use `currentView` and `doUpdateCurrentView` like they were normal queries and commands (and, in fact, they are..).\n\n```tsx\n// ./App.ts\nimport { pipe } from 'fp-ts/lib/pipeable';\nimport * as QR from 'avenger/lib/QueryResult';\nimport { declareQueries } from 'avenger/lib/react';\n\nconst queries = declareQueries({ currentView });\n\n// usually at the top level of your app there will be a sort of index of your navigation\nclass Navigation extends React.PureComponent\u003cProps, State\u003e {\n  render() {\n    return pipe(\n      this.props.queries,\n      QR.fold(\n        () =\u003e \u003cp\u003eloading\u003c/p\u003e,\n        () =\u003e null,\n        ({ currentView }) =\u003e {\n          switch(currentView.view) {\n            case 'itemView':\n              return \u003cItemView id={view.itemId} /\u003e\n            case 'items':\n              return \u003cItems /\u003e\n            case 'home':\n              return \u003cHome /\u003e\n          }\n        }\n      )\n    )\n  }\n}\n\nexport queries(MyComponent)\n```\n\n```tsx\n// ./Components/ItemView.ts\n\nclass ItemView extends React.PureComponent\u003cProps, State\u003e {\n  goToItems: () =\u003e doUpdateCurrentView({ view: 'items' })()\n\n  render() {\n    return \u003cBackButton onClick={this.goToItems}\u003e\n  }\n}\n```\n\n## Signatures\n\n\u003e N.B. all the following signatures reference the abstractions in [`fp-ts`](https://github.com/gcanti/fp-ts)\n\n### `query`\n\n```ts\ndeclare function query\u003cA = void, L = unknown, P = unknown\u003e(\n  fetch: Fetch\u003cA, L, P\u003e\n): (strategy: Strategy\u003cA, L, P\u003e) =\u003e CachedQuery\u003cA, L, P\u003e;\n```\n\n### `Fetch`\n\n```ts\ntype Fetch\u003cA, L, P\u003e = (input: A) =\u003e TaskEither\u003cL, P\u003e;\n```\n\n### `StrategyBuilder`\n\n```ts\ntype StrategyBuilder\u003cA, L, P\u003e = (\n  inputEq: Eq\u003cA\u003e,\n  cacheValueEq: Eq\u003cCacheValue\u003cL, P\u003e\u003e\n) =\u003e Strategy\u003cA, L, P\u003e;\n```\n\n### `Strategy`\n\n```ts\nexport class Strategy\u003cA, L, P\u003e {\n  constructor(\n    readonly inputEq: Eq\u003cA\u003e,\n    readonly filter: Function1\u003cCacheValue\u003cL, P\u003e, boolean\u003e,\n    readonly cacheValueEq: Eq\u003cCacheValue\u003cL, P\u003e\u003e\n  ) {}\n}\n```\n\n### `CachedQuery`\n\n```ts\ninterface CachedQuery\u003cA, L, P\u003e {\n  type: 'cached';\n  inputEq: Eq\u003cA\u003e;\n  run: Fetch\u003cA, L, P\u003e;\n  invalidate: Fetch\u003cA, L, P\u003e;\n  cache: Cache\u003cA, L, P\u003e;\n}\n```\n\n### `Composition`\n\n```ts\ninterface Composition\u003cA, L, P\u003e {\n  type: 'composition';\n  inputEq: Eq\u003cA\u003e;\n  run: Fetch\u003cA, L, P\u003e;\n  invalidate: Fetch\u003cA, L, P\u003e;\n  master: ObservableQuery\u003cA, L, unknown\u003e;\n  slave: ObservableQuery\u003cunknown, L, P\u003e;\n}\n```\n\n### `Product`\n\n```ts\ninterface Product\u003cA, L, P\u003e {\n  type: 'product';\n  inputEq: Eq\u003cA\u003e;\n  run: Fetch\u003cA, L, P\u003e;\n  invalidate: Fetch\u003cA, L, P\u003e;\n  queries: Record\u003cstring, ObservableQuery\u003cA[keyof A], L, P[keyof P]\u003e\u003e;\n}\n```\n\n### `ObservableQuery`\n\n```ts\ntype ObservableQuery\u003cA, L, P\u003e =\n  | CachedQuery\u003cA, L, P\u003e\n  | Composition\u003cA, L, P\u003e\n  | Product\u003cA, L, P\u003e;\n```\n\n### `QueryResult`\n\n```ts\n// instance of Bifunctor2\u003cURI\u003e \u0026 Monad2\u003cURI\u003e\ntype QueryResult\u003cL, A\u003e = Loading\u003cL, A\u003e | Failure\u003cL, A\u003e | Success\u003cL, A\u003e;\n```\n\n### `compose`\n\n```ts\nfunction compose\u003cA1, L1, P1, L2, P2\u003e(\n  master: ObservableQuery\u003cA1, L1, P1\u003e,\n  slave: ObservableQuery\u003cP1, L2, P2\u003e\n): Composition\u003cA1, L1 | L2, P2\u003e;\n```\n\n### `product`\n\n```ts\nfunction product\u003cR extends ObservableQueries\u003e(\n  queries: EnforceNonEmptyRecord\u003cR\u003e\n): Product\u003cProductA\u003cR\u003e, ProductL\u003cR\u003e, ProductP\u003cR\u003e\u003e;\n```\n\n### `command`\n\n```ts\nfunction command\u003cA, L, P, I extends ObservableQueries, IL extends ProductL\u003cI\u003e\u003e(\n  cmd: Fetch\u003cA, L, P\u003e,\n  queries?: EnforceNonEmptyRecord\u003cI\u003e\n): (a: A, ia?: ProductA\u003cI\u003e) =\u003e TaskEither\u003cL | IL, P\u003e;\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbuildo%2Favenger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbuildo%2Favenger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbuildo%2Favenger/lists"}