{"id":35711407,"url":"https://github.com/dudko-dev/protoobject","last_synced_at":"2026-04-01T20:52:38.590Z","repository":{"id":258246231,"uuid":"871319417","full_name":"dudko-dev/protoobject","owner":"dudko-dev","description":"A universal class for creating any JSON objects and simple manipulations with them.","archived":false,"fork":false,"pushed_at":"2026-03-16T07:32:28.000Z","size":1012,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-16T20:00:08.553Z","etag":null,"topics":["base-class","class-conversion","class-transformer","converter","data-processing","data-protocol","javascript","json","jsonstream","object","protocol","protocol-buffers","protoobject","prototype","transformer","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/protoobject","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/dudko-dev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":"FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"patreon":"dudko_dev","custom":["https://dudko.dev/donate","https://paypal.me/dudkodev","https://www.buymeacoffee.com/dudko.dev"]}},"created_at":"2024-10-11T17:54:40.000Z","updated_at":"2026-03-16T07:32:28.000Z","dependencies_parsed_at":"2025-12-13T06:05:18.104Z","dependency_job_id":null,"html_url":"https://github.com/dudko-dev/protoobject","commit_stats":null,"previous_names":["dudko-dev/protoobject"],"tags_count":145,"template":false,"template_full_name":null,"purl":"pkg:github/dudko-dev/protoobject","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dudko-dev%2Fprotoobject","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dudko-dev%2Fprotoobject/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dudko-dev%2Fprotoobject/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dudko-dev%2Fprotoobject/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dudko-dev","download_url":"https://codeload.github.com/dudko-dev/protoobject/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dudko-dev%2Fprotoobject/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291858,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["base-class","class-conversion","class-transformer","converter","data-processing","data-protocol","javascript","json","jsonstream","object","protocol","protocol-buffers","protoobject","prototype","transformer","typescript"],"created_at":"2026-01-06T04:14:39.231Z","updated_at":"2026-04-01T20:52:38.570Z","avatar_url":"https://github.com/dudko-dev.png","language":"TypeScript","readme":"﻿# ProtoObject\n\nA universal class for creating any JSON objects and simple manipulations with them.\n\nJust inherit from this class and easily organize your work with data. This can be data storage in an SQL/NoSQL database, data storage in localStorage/sessionStorage, simple transfer of data serialization on one side and the same idle deserialization of data on the other side. You can write your own ProtoBuffer protocol on top of this library and use it for RPC or to write a universal data library for FrontEnd and BackEnd.\n\nThink of it as a data framework.\n\nInspired by gRPC and Firebase.\n\n[![npm](https://img.shields.io/npm/v/protoobject.svg)](https://www.npmjs.com/package/protoobject)\n[![npm](https://img.shields.io/npm/dy/protoobject.svg)](https://www.npmjs.com/package/protoobject)\n[![NpmLicense](https://img.shields.io/npm/l/protoobject.svg)](https://www.npmjs.com/package/protoobject)\n![GitHub last commit](https://img.shields.io/github/last-commit/dudko-dev/protoobject.svg)\n![GitHub release](https://img.shields.io/github/release/dudko-dev/protoobject.svg)\n\n## INSTALL\n\n```bash\n npm i protoobject --save\n```\n\n## MODULE FORMATS\n\nThis library supports both **CommonJS** and **ESM** (ES Modules) formats:\n\n### CommonJS\n\n```javascript\nconst { ProtoObject, StaticImplements, protoObjectFactory } = require('protoobject');\n```\n\n### ES Modules (ESM)\n\n```javascript\nimport { ProtoObject, StaticImplements, protoObjectFactory } from 'protoobject';\n```\n\nThe library automatically provides the correct format based on your project configuration. No additional setup required!\n\n## 🌐 BROWSER COMPATIBILITY\n\nProtoObject works in **both Node.js and Browser environments**:\n\n### Browser Usage\n\nFor browser compatibility, use the browser-specific import:\n\n```javascript\n// Browser-compatible imports only (excludes Node.js-specific modules)\nimport { \n  ProtoObject, \n  StaticImplements,\n  protoObjectFactory \n} from 'protoobject/browser';\n```\n\n### Node.js Usage\n\nFor Node.js, use the full import with all modules:\n\n```javascript\n// Full import with all modules including Node.js-specific\nimport { \n  ProtoObject,\n  ProtoObjectSQLite,\n  ProtoObjectTCP,\n  ProtoObjectCrypto,\n  ProtoObjectFS,\n  ProtoObjectStream\n} from 'protoobject';\n```\n\n### Browser Support\n\n- ✅ Chrome 80+\n- ✅ Firefox 75+\n- ✅ Safari 13+\n- ✅ Edge 80+\n- ✅ ES6+ and ES Modules support\n\n### What Works in Browser\n\n- ✅ Core ProtoObject functionality\n- ✅ JSON serialization/deserialization\n- ✅ localStorage/sessionStorage integration\n- ✅ Fetch API integration\n- ✅ IndexedDB integration\n- ✅ WebSocket integration\n\n### What's Node.js Only\n\n- ❌ ProtoObjectSQLite (use IndexedDB in browser)\n- ❌ ProtoObjectTCP (use WebSockets/Fetch in browser)\n- ❌ ProtoObjectCrypto (use Web Crypto API in browser)\n- ❌ ProtoObjectFS (use localStorage/IndexedDB/Fetch in browser)\n- ❌ ProtoObjectStream (use browser streams or fetch in browser)\n\n## 📚 EXAMPLES\n\nSee the [`examples/`](./examples/) directory for comprehensive usage examples:\n\n- **[Browser Examples](./examples/browser/)** - Browser compatibility with interactive demo\n- **[JavaScript Examples](./examples/javascript/)** - Plain JavaScript usage with CommonJS\n- **[TypeScript Examples](./examples/typescript/)** - Type-safe usage with decorators and interfaces  \n- **[SQL Database Examples](./examples/sql-database/)** - Database integration patterns\n\n## 🧪 TESTING\n\nRun all tests:\n\n```bash\nnpm test\n```\n\nRun specific test suites:\n\n```bash\nnpm run test:ts    # TypeScript tests\nnpm run test:js    # JavaScript tests  \nnpm run test:sql   # SQL database tests\n```\n\n## DOCS\n\n### The main methods of the ProtoObject class\n\nThese methods ensure that the class and its heirs interact with the external system and will not contain backward incompatible changes.\n\n| type of the property | name of the property | description |\n| --- | --- | --- |\n| static | `fromJSON` | A method for converting a simple json to ProtoObject class or its heir |\n| dynamic | `toJSON` | A method for converting a ProtoObject class or its heir to simple json |\n| dynamic | `toString` | A method for converting a ProtoObject class or its heir to a string |\n| dynamic | `copy` | Copying a ProtoObject class or its heirs |\n| dynamic | `assign` | Deep assign data to an instance of the ProtoObject class or its heir |\n| static | `recordTransformer` | Factory for creating a data transformer for the ProtoObject class or its heir |\n| static | `collectionTransformer` | Factory for creating a data transformer for the array of ProtoObject classes or its heirs |\n\n### The auxiliary methods of the ProtoObject class\n\nThese methods ensure the operation of the class itself and can change significantly over time.\n\n| type of the property | name of the property | description |\n| --- | --- | --- |\n| static | `getProperties` | Get all properties of an object and its prototypes |\n| static | `getEnumerableProperties` | Get all enumerable properties of an object and its prototypes |\n| static | `recursiveAssign` | A recursive function for assigning properties to an object or returning a property if it is not interchangeable |\n| static | `deepAssign` | Deep assign data to an instance of the ProtoObject class or its heir |\n| static | `valueToJSON` | The converter of values into simple types |\n| static | `valueFromJSON` | The converter of simple types into values |\n\n### JavaScript\n\nCreating an heir class\n\n#### JavaScript - Creating an heir class using inheritance\n\nNote that you cannot use static method validation using a decorator in JavaScript, TypeScript provides more features.\n\nNote the call to `this.assign(data);` in the constructor of your own class. This is due to the code build, which causes your class to first call `super(data);` and then apply the value of the properties specified in the class (if the value is not specified, `undefined` will be applied). This is the reason why `super(data);` will not make an assignment for the properties specified in your class.\n\n```javascript\nclass UserAddress extends ProtoObject {\n  constructor(data) {\n    if (data) this.assign(data);\n    return this;\n  }\n\n  country;\n\n  postCode;\n}\n```\n\n#### JavaScript - Creating an heir class using a factory\n\nYou can skip fields with standard types `String`, `Number`, `Boolean` and use a superclass converters (`UserRights?.prototype?.toJSON?.call(this)` and `ProtoObject.fromJSON(data)`) for these types, but you must implement the conversion of the remaining types manually.\n\n```javascript\nconst UserRights = protoObjectFactory({\n  fromJSON(data) {\n    return new this({\n      ...ProtoObject.fromJSON(data),\n      updatedAt: new Date(data?.updatedAt),\n    });\n  },\n  toJSON() {\n    return {\n      ...UserRights?.prototype?.toJSON?.call(this),\n      updatedAt: this.updatedAt?.toJSON(),\n    };\n  },\n});\n```\n\n#### JavaScript - Creating an heir class using inheritance with conversion of additional data types\n\nNote that you cannot use static method validation using a decorator in JavaScript, TypeScript provides more features.\n\nNote the call to `this.assign(data);` in the constructor of your own class. This is due to the code build, which causes your class to first call `super(data);` and then apply the value of the properties specified in the class (if the value is not specified, `undefined` will be applied). This is the reason why `super(data);` will not make an assignment for the properties specified in your class.\n\nYou can skip fields with standard types `String`, `Number`, `Boolean` and use a superclass converters (`super.toJSON.call(this)` and `super.fromJSON(data)`) for these types, but you must implement the conversion of the remaining types manually.\n\n```javascript\nclass User extends ProtoObject {\n  constructor(data) {\n    super(data);\n    if (data) this.assign(data);\n    return this;\n  }\n\n  id;\n\n  email;\n\n  createdAt;\n\n  photo;\n\n  address;\n\n  toJSON() {\n    return {\n      ...super.toJSON.call(this),\n      createdAt: this.createdAt.toJSON(),\n      photo:\n        this.photo instanceof Buffer ? this.photo.toString(\"hex\") : undefined,\n      address:\n        this.address instanceof UserAddress ? this.address.toJSON() : undefined,\n      rights:\n        this.rights instanceof UserRights ? this.rights?.toJSON() : undefined,\n    };\n  }\n\n  static fromJSON(data) {\n    return new User({\n      ...super.fromJSON(data),\n      createdAt:\n        typeof data.createdAt === \"string\"\n          ? new Date(data.createdAt)\n          : undefined,\n      photo:\n        typeof data.photo === \"string\"\n          ? Buffer.from(data.photo, \"hex\")\n          : undefined,\n      address: data.address ? UserAddress.fromJSON(data.address) : undefined,\n      rights: data.rights ? UserRights.fromJSON(data.rights) : undefined,\n    });\n  }\n}\n```\n\n### TypeScript\n\nCreating an heir class\n\n#### TypeScript - Creating an heir class using inheritance\n\nNote that to check the static properties of a class, you can use the decorator `@StaticImplements\u003cProtoObjectStaticMethods\u003cUser\u003e\u003e()`.\n\nNote the call to `this.assign(data);` in the constructor of your own class. This is due to the code build, which causes your class to first call `super(data);` and then apply the value of the properties specified in the class (if the value is not specified, `undefined` will be applied). This is the reason why `super(data);` will not make an assignment for the properties specified in your class.\n\n```typescript\n@StaticImplements\u003cProtoObjectStaticMethods\u003cUserAddress\u003e\u003e()\nexport class UserAddress extends ProtoObject\u003cUserAddress\u003e {\n  constructor(data?: Partial\u003cUserAddress\u003e) {\n    super(data);\n    if (data) this.assign(data);\n    return this;\n  }\n\n  country!: string;\n\n  postCode!: string;\n}\n```\n\n#### TypeScript - Creating an heir class using a factory\n\nYou can skip fields with standard types `String`, `Number`, `Boolean` and use a superclass converters (`UserRights?.prototype?.toJSON?.call(this)` and `ProtoObject.fromJSON(data)`) for these types, but you must implement the conversion of the remaining types manually.\n\n```typescript\ninterface IUserRights extends ProtoObject\u003cIUserRights\u003e {\n  isAdmin: boolean;\n  updatedAt: Date;\n}\nconst UserRights = protoObjectFactory\u003cIUserRights\u003e({\n  fromJSON(data) {\n    return new this({\n      ...ProtoObject.fromJSON(data),\n      updatedAt: new Date(data?.updatedAt),\n    });\n  },\n  toJSON() {\n    return {\n      ...UserRights?.prototype?.toJSON?.call(this),\n      updatedAt: this.updatedAt?.toJSON(),\n    };\n  },\n});\n```\n\n#### TypeScript - Creating an heir class using inheritance with conversion of additional data types\n\nNote that to check the static properties of a class, you can use the decorator `@StaticImplements\u003cProtoObjectStaticMethods\u003cUser\u003e\u003e()`.\n\nNote the call to `this.assign(data);` in the constructor of your own class. This is due to the code build, which causes your class to first call `super(data);` and then apply the value of the properties specified in the class (if the value is not specified, `undefined` will be applied). This is the reason why `super(data);` will not make an assignment for the properties specified in your class.\n\nYou can skip fields with standard types `String`, `Number`, `Boolean` and use a superclass converters (`super.toJSON.call(this)` and `super.fromJSON(data)`) for these types, but you must implement the conversion of the remaining types manually.\n\n```typescript\n@StaticImplements\u003cProtoObjectStaticMethods\u003cUser\u003e\u003e()\nclass User extends ProtoObject\u003cUser\u003e {\n  constructor(data?: Partial\u003cUser\u003e) {\n    super(data);\n    if (data) this.assign(data);\n    return this;\n  }\n\n  id!: string;\n\n  email!: string;\n\n  createdAt!: Date;\n\n  photo?: Buffer;\n\n  address?: UserAddress;\n\n  rights?: IUserRights;\n\n  public toJSON(): { [key: string]: any } {\n    return {\n      ...super.toJSON.call(this),\n      createdAt: this.createdAt.toJSON(),\n      photo:\n        this.photo instanceof Buffer ? this.photo.toString(\"hex\") : undefined,\n      address:\n        this.address instanceof UserAddress ? this.address.toJSON() : undefined,\n      rights:\n        this.rights instanceof UserRights ? this.rights?.toJSON() : undefined,\n    };\n  }\n\n  public static fromJSON\u003cUser\u003e(data: { [key: string]: unknown }): User {\n    return new User({\n      ...(super.fromJSON\u003cany\u003e(data) as User),\n      createdAt:\n        typeof data.createdAt === \"string\"\n          ? new Date(data.createdAt)\n          : undefined,\n      photo:\n        typeof data.photo === \"string\"\n          ? Buffer.from(data.photo, \"hex\")\n          : undefined,\n      address: data.address\n        ? UserAddress.fromJSON\u003cUserAddress\u003e(\n            data.address as { [key: string]: unknown }\n          )\n        : undefined,\n      rights: data.rights ? UserRights.fromJSON(data.rights) : undefined,\n    }) as unknown as User;\n  }\n}\n```\n\n### An example of the implementation of the SQL database base class\n\nThis is an example of creating a basic (abstract) class for working with an SQLite database. Below is an example of an heir of this class, which is a table entry.\n\n```typescript\nimport { randomUUID } from \"node:crypto\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport {\n  ProtoObject,\n  ProtoObjectStaticMethods,\n  StaticImplements,\n} from \"protoobject\";\n\nexport enum RecordState {\n  ACTIVE,\n  DELETED,\n}\n\nexport interface BaseRecordStaticMethods\u003cT extends BaseRecord\u003cT\u003e\u003e\n  extends ProtoObjectStaticMethods\u003cT\u003e {\n  table: string;\n  getById\u003cT extends BaseRecord\u003cT\u003e\u003e(\n    db: DatabaseSync,\n    id: BaseRecord\u003cT\u003e[\"id\"]\n  ): Promise\u003cT | undefined\u003e;\n}\n\n@StaticImplements\u003cBaseRecordStaticMethods\u003cBaseRecord\u003cany\u003e\u003e\u003e()\nexport class BaseRecord\u003cT extends BaseRecord\u003cT\u003e\u003e extends ProtoObject\u003cT\u003e {\n  constructor(data?: Partial\u003cT\u003e) {\n    super(data);\n    if (data) this.assign(data);\n    if (typeof this.record_state === \"undefined\")\n      this.record_state = RecordState.ACTIVE;\n    if (!this.id) this.id = randomUUID();\n    const dt = new Date();\n    if (!this.created_at) this.created_at = dt;\n    if (!this.updated_at) this.updated_at = dt;\n    return this;\n  }\n\n  public static table = `base`;\n\n  public id!: string;\n\n  public created_at!: Date;\n\n  public updated_at!: Date;\n\n  public record_state!: RecordState;\n\n  public static async getById\u003cT extends BaseRecord\u003cT\u003e\u003e(\n    db: DatabaseSync,\n    id: BaseRecord\u003cT\u003e[\"id\"]\n  ): Promise\u003cT | undefined\u003e {\n    const dbRecord = (await db\n      .prepare(`SELECT * FROM ${this.table} WHERE id = ?`)\n      .get(id)) as Partial\u003cT\u003e;\n    return dbRecord ? this.fromJSON\u003cT\u003e(dbRecord) : undefined;\n  }\n\n  public async reload(db: DatabaseSync): Promise\u003cT\u003e {\n    const classNode = this.constructor as BaseRecordStaticMethods\u003cT\u003e;\n    const dbRecord = await classNode.getById\u003cT\u003e(db, this.id);\n    if (!dbRecord) throw new Error(\"Not Found\");\n    return this.assign(classNode.fromJSON(dbRecord));\n  }\n\n  public async insert(db: DatabaseSync): Promise\u003cvoid\u003e {\n    const classNode = this.constructor as BaseRecordStaticMethods\u003cT\u003e;\n    this.created_at = new Date();\n    this.updated_at = new Date();\n    const jsonData = this.toJSON();\n    await Promise.resolve(\n      db\n        .prepare(\n          `INSERT INTO ${classNode.table} (${Object.keys(jsonData).join(\", \")}) VALUES (${Object.keys(\n            jsonData\n          )\n            .map((e, i) =\u003e `?`)\n            .join(\", \")})`\n        )\n        .run(...Object.values(jsonData))\n    );\n    return;\n  }\n\n  public async update(db: DatabaseSync): Promise\u003cvoid\u003e {\n    const classNode = this.constructor as BaseRecordStaticMethods\u003cT\u003e;\n    this.updated_at = new Date();\n    const jsonData = this.toJSON();\n    delete jsonData.id;\n    await Promise.resolve(\n      db\n        .prepare(\n          `UPDATE ${classNode.table} SET ${Object.keys(jsonData)\n            .map((e, i) =\u003e `${e} = ?`)\n            .join(\", \")} WHERE id = ?`\n        )\n        .run(...Object.values(jsonData), this.id)\n    );\n    return;\n  }\n\n  public async softDelete(db: DatabaseSync): Promise\u003cvoid\u003e {\n    this.record_state = RecordState.DELETED;\n    this.updated_at = new Date();\n    await this.update(db);\n    return;\n  }\n\n  public async delete(db: DatabaseSync): Promise\u003cvoid\u003e {\n    const classNode = this.constructor as BaseRecordStaticMethods\u003cT\u003e;\n    const jsonData = this.toJSON();\n    delete jsonData.id;\n    await Promise.resolve(\n      db.prepare(`DELETE FROM ${classNode.table} WHERE id = ?`).run(this.id)\n    );\n    return;\n  }\n\n  public static fromJSON\u003cT\u003e(data: { [key: string]: unknown }) {\n    return new this({\n      ...super.fromJSON(data),\n      created_at:\n        typeof data.created_at === \"string\"\n          ? new Date(data.created_at)\n          : undefined,\n      updated_at:\n        typeof data.updated_at === \"string\"\n          ? new Date(data.updated_at)\n          : undefined,\n    }) as T;\n  }\n\n  public toJSON(): { [key: string]: any } {\n    return {\n      ...super.toJSON.call(this),\n      created_at: this.created_at?.toJSON(),\n      updated_at: this.updated_at?.toJSON(),\n    };\n  }\n}\n```\n\n### An example of the implementation of an heir from the base class of the SQL database\n\nAn example of a simple implementation of a data class based on a base class.\n\n```typescript\nimport { randomUUID } from \"crypto\";\nimport { StaticImplements } from \"protoobject\";\nimport { BaseRecord, BaseRecordStaticMethods } from \"./example-base-class\";\n\n@StaticImplements\u003cBaseRecordStaticMethods\u003cApplicationRecord\u003e\u003e()\nexport class ApplicationRecord extends BaseRecord\u003cApplicationRecord\u003e {\n  constructor(data?: Partial\u003cApplicationRecord\u003e) {\n    super(data);\n    if (data) this.assign(data);\n    if (!this.api_key) this.api_key = randomUUID();\n  }\n\n  public static override table: string = `applications`;\n\n  public api_key!: string;\n\n  public app_name!: PublicKeyCredentialCreationOptions[\"rp\"][\"name\"];\n\n  public app_params!: { [key: string]: string | number | boolean | null };\n\n  public static fromJSON\u003cT\u003e(data: { [key: string]: unknown }) {\n    return new ApplicationRecord({\n      ...super.fromJSON(data),\n      app_params:\n        typeof data?.app_params === \"string\"\n          ? JSON.parse(data.app_params)\n          : undefined,\n    }) as T;\n  }\n\n  public toJSON(): { [key: string]: any } {\n    return {\n      ...super.toJSON.call(this),\n      app_params: this.app_params ? JSON.stringify(this.app_params) : undefined,\n    };\n  }\n}\n```\n\n## 🤝 Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n### Development Setup\n\n```bash\ngit clone https://github.com/siarheidudko/protoobject.git\ncd protoobject\nnpm install\nnpm run build\nnpm test\n```\n\n## 📄 License\n\nMIT License - see [LICENSE](./LICENSE) file for details.\n\n## 🆘 Support\n\n- 📝 **Issues**: [GitHub Issues](https://github.com/siarheidudko/protoobject/issues)\n- 💬 **Discussions**: [GitHub Discussions](https://github.com/siarheidudko/protoobject/discussions)\n- 📧 **Email**: [siarhei@dudko.dev](mailto:siarhei@dudko.dev)\n\n## 💝 Support This Project\n\nIf ProtoObject helps you build amazing applications, consider supporting its development:\n\n- ☕ **[Buy me a coffee](https://www.buymeacoffee.com/dudko.dev)**\n- 💳 **[PayPal](https://paypal.me/dudkodev)**\n- 🎯 **[Patreon](https://patreon.com/dudko_dev)**\n- 🌐 **[More options](http://dudko.dev/donate)**\n\nYour support helps maintain and improve Redux Cluster for the entire community!\n\n---\n\n**Made with ❤️ by [Siarhei Dudko](https://github.com/siarheidudko)**\n","funding_links":["https://patreon.com/dudko_dev","https://dudko.dev/donate","https://paypal.me/dudkodev","https://www.buymeacoffee.com/dudko.dev"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdudko-dev%2Fprotoobject","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdudko-dev%2Fprotoobject","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdudko-dev%2Fprotoobject/lists"}