{"id":13393738,"url":"https://github.com/lukeed/vegemite","last_synced_at":"2025-10-09T18:24:11.470Z","repository":{"id":46092428,"uuid":"235931850","full_name":"lukeed/vegemite","owner":"lukeed","description":"A Pub/Sub state manager you'll love... or hate","archived":false,"fork":false,"pushed_at":"2020-06-12T22:19:04.000Z","size":89,"stargazers_count":374,"open_issues_count":0,"forks_count":2,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-09-08T08:42:27.758Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/lukeed.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":"2020-01-24T03:12:44.000Z","updated_at":"2024-12-28T17:55:21.000Z","dependencies_parsed_at":"2022-09-01T17:43:33.911Z","dependency_job_id":null,"html_url":"https://github.com/lukeed/vegemite","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/lukeed/vegemite","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fvegemite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fvegemite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fvegemite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fvegemite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukeed","download_url":"https://codeload.github.com/lukeed/vegemite/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fvegemite/sbom","scorecard":{"id":604556,"data":{"date":"2025-08-11","repo":{"name":"github.com/lukeed/vegemite","commit":"c6a434454b5f9cbd5d0d9ff11aa04a8aa41ce3a4"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.4,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","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":"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":"Code-Review","score":0,"reason":"Found 1/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":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Info: no jobLevel write permissions found"],"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":"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":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/lukeed/vegemite/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/lukeed/vegemite/ci.yml/master?enable=pin","Warn: npmCommand not pinned by hash: .github/workflows/ci.yml:21","Warn: npmCommand not pinned by hash: .github/workflows/ci.yml:22","Warn: downloadThenRun not pinned by hash: .github/workflows/ci.yml:31","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 npmCommand dependencies pinned","Info:   0 out of   1 downloadThenRun dependencies pinned"],"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":"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":"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":"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"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 1 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-21T01:16:43.259Z","repository_id":46092428,"created_at":"2025-08-21T01:16:43.259Z","updated_at":"2025-08-21T01:16:43.259Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279001940,"owners_count":26083226,"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-09T02:00:07.460Z","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":[],"created_at":"2024-07-30T17:00:59.521Z","updated_at":"2025-10-09T18:24:11.418Z","avatar_url":"https://github.com/lukeed.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"logo.jpg\" alt=\"vegemite\" height=\"190\" /\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://npmjs.org/package/vegemite\"\u003e\n    \u003cimg src=\"https://badgen.now.sh/npm/v/vegemite\" alt=\"version\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/lukeed/vegemite/actions\"\u003e\n    \u003cimg src=\"https://badgen.net/github/status/lukeed/vegemite\" alt=\"status\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/lukeed/vegemite\"\u003e\n    \u003cimg src=\"https://badgen.now.sh/codecov/c/github/lukeed/vegemite\" alt=\"codecov\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://npmjs.org/package/vegemite\"\u003e\n    \u003cimg src=\"https://badgen.now.sh/npm/dm/vegemite\" alt=\"downloads\" /\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003eA Pub/Sub state manager you'll love... or hate\u003c/div\u003e\n\n## Features\n\n* **Familiar**\u003cbr\u003e_Inspired by [Immer](https://github.com/immerjs/immer) and [Redux](https://github.com/reduxjs/redux)_\n* **Composable**\u003cbr\u003e_Attach/Detach action handlers as needed. Components can bring their own handlers!_\n* **Immutability**\u003cbr\u003e_Modifications or mutations within a handler do not affect state until completion_\n* **Full [TypeScript Support](#typescript-support)**\u003cbr\u003e_Utilize TypeScript for in-action guards and validations_\n* **Extremely lightweight**\u003cbr\u003e_Weighs only 623 bytes, including dependencies!_\n\n\n## Install\n\n```\n$ npm install --save vegemite\n```\n\n\n## Usage\n\n\u003e Check out the [`/examples`](https://github.com/lukeed/vegemite/tree/master/examples) directory for more patterns.\n\n```js\nimport vegemite from 'vegemite';\n\nconst todomvc = vegemite({\n\tnext: 1,\n\ttodos: [],\n});\n\n// handler-type: mutate\ntodomvc.on('todo:add', (state, data) =\u003e {\n\tlet nxt = state.next++;\n\tstate.todos.push({\n\t\tid: nxt,\n\t\tdone: false,\n\t\ttitle: data.title,\n\t});\n});\n\n// handler-type: return object\ntodomvc.on('todo:toggle', (state, id) =\u003e {\n\tlet todos = state.todos.map(item =\u003e {\n\t\tif (item.id === id) item.done = !item.done;\n\t\treturn item;\n\t});\n\n\treturn { next: state.next, todos };\n});\n\n// handler-type: return Promise\ntodomvc.on('todo:assign', async (state, data) =\u003e {\n\tawait httpie.send('POST', `/api/todos/${data.id}/assign`, {\n\t\tbody: { user: data.userid }\n\t});\n\n\tstate.todos = state.todos.map(item =\u003e {\n\t\tif (item.id === data.id) {\n\t\t\titem.assigned = item.assigned || [];\n\t\t\titem.assigned.push(data.userid);\n\t\t}\n\t\treturn item;\n\t});\n});\n\n// Add a global \"listener\" (side effect)\n// This runs _after_ state has been updated\ntodomvc.listen((state, prev) =\u003e {\n\tconsole.log('~\u003e this is the NEW state:', state);\n\tconsole.log('~\u003e this was the OLD state:', prev);\n});\n```\n\n\n## API\n\n### vegemite(state?)\nReturns `Vegemite`\n\nReturns a Vegemite instance, pre-filled with an initial state object.\n\n#### state\nType: `State extends any`\n\nAny initial data of your choosing.\n\n### Vegemite.state\nReturns: `State`\n\nA getter function that returns a snapshot of the current state.\n\n### Vegemite.on(event, handler)\nReturns: `Function`\n\nAssign a new action handler for a specific event.\nReturns an unsubscriber function that, when called, will detach this `handler`.\n\n\u003e **Important:** When more than one `handler` exists for an event, they are executed in the order that they were assigned.\n\n```js\nlet store = vegemite({ count: 5 });\n\nlet detach = store.on('count:add', (state, event) =\u003e {\n\tstate.count += event.value;\n});\n\nawait store.dispatch('count:add', { value: 10 });\nconsole.log(store.state); //=\u003e { count: 15 }\n\ndetach(); // deactivated\n\nawait store.dispatch('count:add', { value: 3 });\nconsole.log(store.state); //=\u003e { count: 15 } (unchanged)\n```\n\n#### event\nType: `String`\n\nThe event/topic name.\n\n#### handler\nType: `Handler`\n\nSee [Handlers](#handlers) below.\n\n\n### Vegemite.listen(subscriber)\u003cbr\u003eVegemite.listen(event, subscriber)\nReturns: `Function`\n\nAttach a new subscription \u0026mdash; when no `event` is specified, then the `subscriber` will listen to _all_ events.\n\nReturns an unsubscriber function that, when called, will detach this `subscriber`.\n\n\u003e **Important:** When more than one `subscriber` exists for an event (or globally), they are executed in the order that they were assigned.\n\n```js\nlet store = vegemite({ count: 5 });\n\nstore.on('increment', state =\u003e {\n\tstate.count++;\n});\n\nlet unlisten = store.listen('increment', (state, prevState) =\u003e {\n\tconsole.log(`heard increment: ${prevState.count} ~\u003e ${state.count}`);\n});\n\nawait store.dispatch('increment');\n//=\u003e heard increment: 5 ~\u003e 6\n\nunlisten(); // deactivated\n\nawait store.dispatch('increment');\n// (silence)\n```\n\n#### event\nType: `String`\n\nThe event/topic name, if any.\n\n#### subscriber\nType: `Subscriber`\n\nSee [Subscribers](#subscribers) below.\n\n\n### Vegemite.set(state, event?)\nReturns: `void`\n\nForcibly set the instance's state.\n\nWhen `set()` is run, all related subscribers/listeners are called.\u003cbr\u003eOptionally pass an `event` to trigger a specific set of listeners.\n\n\u003e **Note:** This is automatically called at the end of a [`dispatch()`](#vegemitedispatchevent-eventdata) chain.\n\n#### state\nType: `State`\n\nThe new `State` data.\n\n#### event\nType: `String`\n\nThe event/topic on whose behalf you're committing.\n\n\n### Vegemite.dispatch(event, eventData)\nReturns: `Promise\u003cvoid\u003e`\n\nSends a new message to an event/topic.\n\nWhen `dispatch()` is called, all related action handlers (via `on()`) are run to produce a new state. Once this chain is complete, `set()` is invoked with the new state, which will trigger any listeners for the `event` (in addition to global listeners).\n\n\u003e **Attention TypeScript Users:**\u003cbr\u003eFor events that do not require `eventData`, you must dispatch with an `undefined` payload, or similar.\n\n#### event\nType: `String`\n\nThe event/topic to target.\n\n#### eventData\nType: `any`\n\nAny data to send the topic.\n\n\u003e **Important:** Your `eventData` is sent to your event/topic's [handlers](#handlers) as is!\n\n\n## Handlers\n\nAction handlers can be thought of as \"producers\" or \"reducers\" – they are functions that, when matched, are invoked to help create a new state.\n\nAction handlers belong to a specific event and are assigned via `on()`. Multiple handlers may exist for a single event topic. When this is the case, the list of handlers will be executed in the order that they were assigned. This allows handlers to access previous handlers' results and use those as part of its own computation, if desired.\n\nEvery handler receives the current `state` as its first parameter and then any `eventData` that was sent to the event (via `dispatch()`). Handlers may return a new `state` object or they may choose to mutate the `state` in place and forgo a return entirely. A `Promise` may also be returned that resolves either to a new state or to a mutation.\n\n```ts\ntype Handler\u003cT, X\u003e = (state: T, eventData: X) =\u003e Promise\u003cT | void\u003e | T | void;\n```\n\nWhile an event's handlers are running (dubbed the \"dispatch chain\"), you are free to mutate the `state` and `eventData` object _as much as you'd like_ \u0026mdash; mutations have no effect on the instance's actual state during this time! Once the \"dispatch chain\" has resolved (aka, _all_ handlers have completed), Vegemite will promote the chain's final `state` object as the instance's new actual state (via `set(state, event)`).\n\nAt this point, the instance's state has been updated, which means `ctx.state` will reflect the current changes. Additionally, any [subscribers](#subscribers) (via `listen()`) are enqueued for action.\n\n## Subscribers\n\nSubscribers, or listeners, run _after_ the instance has updated its internal state. These can be thought of as \"callbacks\" or side-effects that, when matched, should be alerted about a change in state.\n\nAll subscribers receive the current `state` as its first parameter and the _previous_ state as its second parameter. You can use this information to infer what, specifically, changed between the two state objects. The return value from subscribers has no effect anywhere.\n\n```ts\ntype Listener\u003cT\u003e = (state: T, prevState: T) =\u003e any;\n```\n\nSubscribers can listen in on a specific event topic, or they can listen to everything (aka, \"global\" listeners).\n\nWhen an event's \"dispatch chain\" has resolved, any subscribers for that event will be queued in the order that they were assigned. Additionally, all \"global\" subscribers, if any, will be executed in the order that they were defined. However, please note that **global subscribers always execute before event-specific subscribers.**\n\n\n## TypeScript Support\n\nVegemite is made to work with an \"EventMap\" \u0026mdash; this is an interface whose keys are the names of your events and whose values are the event-datas you expect to pass along as the message. With this information alone, TypeScript and `vegemite` can ensure that **all** of your event publishers and subscribers are passing or expecting the correct data types.\n\nAdditionally, `vegemite` requires a `State` descriptor \u0026mdash; this is a separate interface that describes _what_ you want this `vegemite` instance to contain at all times. With this information, TypeScript can ensure your actions/handlers are abiding by the rules \u0026 not causing mischief.\n\n```ts\ninterface Todo {\n\tid: number;\n\tdone: boolean;\n\ttitle: string;\n\tassigned?: string[];\n}\n\ninterface Assignment {\n\tid: Todo['id'];\n\tuserid: string;\n}\n\ninterface EventMap {\n\t'todo:add': Todo['title'];\n\t'todo:toggle': Todo['id'];\n\t'todo:assign': Assignment;\n\t// ...\n}\n\ninterface State {\n\tnext: number;\n\ttodos: Todo[];\n}\n\nconst todomvc = vegemite\u003cEventMap, State\u003e({\n\tnext: 1,\n\ttodos: []\n});\n```\n\nWith the setup above, TypeScript can help us ensure that the `todo:add` and `todo:toggle` topics will only send or receive their respective `Todo` attributes, whereas the `todo:assign` topic will always send or receive a more complex `Assignment` object.\n\nMeanwhile, any manipulations within the `State` will be protected, ensuring that `state.todos` only contains valid `Todo` objects and `state.next` is always a number.\n\n\n## License\n\nMIT © [Luke Edwards](https://lukeed.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukeed%2Fvegemite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukeed%2Fvegemite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukeed%2Fvegemite/lists"}