{"id":15415852,"url":"https://github.com/davidje13/shared-reducer-backend","last_synced_at":"2025-10-07T03:31:47.775Z","repository":{"id":57358008,"uuid":"227252655","full_name":"davidje13/shared-reducer-backend","owner":"davidje13","description":"Shared state management via websockets. This has moved to https://github.com/davidje13/shared-reducer","archived":true,"fork":false,"pushed_at":"2024-09-27T21:51:52.000Z","size":342,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-09T15:56:02.064Z","etag":null,"topics":["reducer","websocket"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":false,"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/davidje13.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-11T01:51:00.000Z","updated_at":"2024-09-27T21:54:45.000Z","dependencies_parsed_at":"2022-09-26T16:40:38.824Z","dependency_job_id":null,"html_url":"https://github.com/davidje13/shared-reducer-backend","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/davidje13/shared-reducer-backend","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fshared-reducer-backend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fshared-reducer-backend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fshared-reducer-backend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fshared-reducer-backend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davidje13","download_url":"https://codeload.github.com/davidje13/shared-reducer-backend/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidje13%2Fshared-reducer-backend/sbom","scorecard":{"id":326916,"data":{"date":"2025-08-11","repo":{"name":"github.com/davidje13/shared-reducer-backend","commit":"029c6ce3be56db8daf763f60ef7ad3a91b7a0f57"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":0,"reason":"project is archived","details":["Warn: Repository is archived."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-18T02:40:45.113Z","repository_id":57358008,"created_at":"2025-08-18T02:40:45.113Z","updated_at":"2025-08-18T02:40:45.113Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278715688,"owners_count":26033323,"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","status":"online","status_checked_at":"2025-10-07T02:00:06.786Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["reducer","websocket"],"created_at":"2024-10-01T17:10:02.039Z","updated_at":"2025-10-07T03:31:47.451Z","avatar_url":"https://github.com/davidje13.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shared Reducer Backend\n\n**This project has moved**\n\nThe new project is `shared-reducer`, and can be found on\n[GitHub](https://github.com/davidje13/shared-reducer) and\n[GitLab](https://gitlab.com/davidje13/shared-reducer)\n\n---\n\nShared state management via websockets.\n\nDesigned to work with\n[shared-reducer-frontend](https://github.com/davidje13/shared-reducer-frontend)\nand\n[json-immutability-helper](https://github.com/davidje13/json-immutability-helper).\n\n## Install dependency\n\n```bash\nnpm install --save shared-reducer-backend json-immutability-helper\n```\n\n(if you want to use an alternative reducer, see the instructions below).\n\nWhen using this with `shared-reducer-frontend`, ensure both dependencies are\nat the same major version (e.g. both are `2.x` or both are `3.x`). The API\nmay change between major versions.\n\n## Usage\n\nThis project is compatible with\n[websocket-express](https://github.com/davidje13/websocket-express),\nbut can also be used in isolation.\n\n### With websocket-express\n\n```js\nimport {\n  Broadcaster,\n  websocketHandler,\n  InMemoryModel,\n  ReadWrite,\n} from 'shared-reducer-backend';\nimport context from 'json-immutability-helper';\nimport WebSocketExpress from 'websocket-express';\n\nconst model = new InMemoryModel();\nconst broadcaster = Broadcaster.for(model)\n  .withReducer(context)\n  .build();\nmodel.set('a', { foo: 'v1' });\n\nconst app = new WebSocketExpress();\nconst server = app.listen(0, 'localhost');\n\nconst handler = websocketHandler(broadcaster);\napp.ws('/:id', handler((req) =\u003e req.params.id, () =\u003e ReadWrite));\n```\n\nFor real use-cases, you will probably want to add authentication middleware\nto the expressjs chain, and you may want to give some users read-only and\nothers read-write access, which can be achieved in the second lambda.\n\n### Alone\n\n```js\nimport { Broadcaster, InMemoryModel } from 'shared-reducer-backend';\nimport context from 'json-immutability-helper';\n\nconst model = new InMemoryModel();\nconst broadcaster = Broadcaster.for(model)\n  .withReducer(context)\n  .build();\nmodel.set('a', { foo: 'v1' });\n\n// ...\n\nconst subscription = await broadcaster.subscribe(\n  'a',\n  (change, meta) =\u003e { /*...*/ },\n);\n\nconst begin = subscription.getInitialData();\nawait subscription.send(['=', { foo: 'v2' }]);\n// callback provided earlier is invoked\n\nawait subscription.close();\n```\n\n## Persisting data\n\nA convenience wrapper is provided for use with\n[collection-storage](https://github.com/davidje13/collection-storage),\nor you can write your own implementation of the `Model` interface to\nlink any backend.\n\n```js\nimport {\n  Broadcaster,\n  CollectionStorageModel,\n} from 'shared-reducer-backend';\nimport context from 'json-immutability-helper';\nimport CollectionStorage from 'collection-storage';\n\nconst db = await CollectionStorage.connect('memory://something');\nconst model = new CollectionStorageModel(\n  db.getCollection('foo'),\n  'id',\n  // a function which takes in an object and returns it if valid,\n  // or throws if invalid (protects stored data from malicious changes)\n  MY_VALIDATOR,\n);\nconst broadcaster = Broadcaster.for(model)\n  .withReducer(context)\n  .build();\n```\n\nNote that the provided validator MUST verify structural integrity (e.g.\nensuring no unexpected fields are added or types are changed).\n\n## WebSocket protocol\n\nThe websocket protocol is minimal:\n\n### Messages received\n\n`P` (ping):\nCan be sent periodically to keep the connection alive. Sends a \"Pong\" message\nin response immediately.\n\n`{\"change\": \u003cspec\u003e, \"id\": \u003cid\u003e}`:\nDefines a delta. The ID will be reflected back once the change has been\napplied. Other clients will not receive the ID.\n\n### Messages sent\n\n`p` (pong):\nReponse to a ping. May also be sent unsolicited.\n\n`{\"init\": \u003cstate\u003e}`:\nThe first message sent by the server, in response to a successful\nconnection.\n\n`{\"change\": \u003cspec\u003e}`:\nSent whenever another client has changed the server state.\n\n`{\"change\": \u003cspec\u003e, \"id\": \u003cid\u003e}`:\nSent whenever the current client has changed the server state. Note that\nthe spec and ID will match the client-sent values.\n\n`{\"error\": \u003cmessage\u003e, \"id\": \u003cid\u003e}`:\nSent if the server rejects a client-initiated change.\n\nIf this is returned, the server state will not have changed (i.e. the\nentire spec failed).\n\n### Specs\n\nThe specs need to match whichever reducer you are using. In the examples\nabove, that is\n[json-immutability-helper](https://github.com/davidje13/json-immutability-helper).\n\n## Alternative reducer\n\nTo enable different features of `json-immutability-helper`, you can\ncustomise it before passing it to `withReducer`. For example, to\nenable list commands such as `updateWhere` and mathematical commands\nsuch as Reverse Polish Notation (`rpn`):\n\n```js\nimport { Broadcaster, InMemoryModel } from 'shared-reducer-backend';\nimport listCommands from 'json-immutability-helper/commands/list';\nimport mathCommands from 'json-immutability-helper/commands/math';\nimport context from 'json-immutability-helper';\n\nconst broadcaster = Broadcaster.for(new InMemoryModel())\n  .withReducer(context.with(listCommands, mathCommands))\n  .build();\n```\n\nIf you want to use an entirely different reducer, create a wrapper\nand pass it to `withReducer`:\n\n```js\nimport { Broadcaster, InMemoryModel } from 'shared-reducer-backend';\n\nconst myReducer = {\n  update: (value, spec) =\u003e {\n    // return a new value which is the result of applying\n    // the given spec to the given value (or throw an error)\n  },\n  combine: (specs) =\u003e {\n    // return a new spec which is equivalent to applying\n    // all the given specs in order\n    // (this is not used by the backend but is used by\n    // shared-reducer-frontend to reduce data transfers)\n  },\n};\n\nconst broadcaster = Broadcaster.for(new InMemoryModel())\n  .withReducer(myReducer)\n  .build();\n```\n\nBe careful when using your own reducer to avoid introducing\nsecurity vulnerabilities; the functions will be called with\nuntrusted input, so should be careful to avoid attacks such\nas code injection or prototype pollution.\n\n## Other customisations\n\nThe `Broadcaster` builder has other settable properties:\n\n- `withSubscribers`: specify a custom keyed broadcaster, used\n  for communicating changes to all consumers. Required interface:\n\n  ```js\n  {\n    add(key, listener) {\n      // add the listener function to key\n    },\n    remove(key, listener) {\n      // remove the listener function from key\n    },\n    broadcast(key, message) {\n      // call all current listener functions for key with\n      // the parameter message\n    },\n  }\n  ```\n\n  All functions can be asynchronous or synchronous.\n\n  The main use-case for overriding this would be to share\n  messages between multiple servers for load balancing, but\n  note that in most cases you probably want to load balance\n  _documents_ rather than _users_ for better scalability.\n\n- `withTaskQueues`: specify a custom task queue, used to ensure\n  operations happen in the correct order. Required interface:\n\n  ```js\n  {\n    push(key, task) {\n      // add the (possibly asynchronous) task to the queue\n      // for the given key\n    },\n  }\n  ```\n\n  The default implementation will execute the task if it is\n  the first task in a particular queue. If there is already\n  a task in the queue, it will be stored and executed once\n  the existing tasks have finished. Once all tasks for a\n  particular key have finished, it will remove the queue.\n\n  As with `withSubscribers`, the main reason to override\n  this is to provide consistency if multiple servers are\n  able to modify the same document simultaneously.\n\n- `withIdProvider`: specify a custom unique ID provider.\n  Required interface:\n\n  ```js\n  {\n    get() {\n      // return a unique string (must be synchronous)\n    },\n  }\n  ```\n\n  The returned ID is used internally and passed through\n  the configured `taskQueues` to identify the source of\n  a change. It is not revealed to users. The default\n  implementation uses a fixed random prefix followed by\n  an incrementing number, which should be sufficient for\n  most use cases.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidje13%2Fshared-reducer-backend","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidje13%2Fshared-reducer-backend","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidje13%2Fshared-reducer-backend/lists"}