{"id":25526239,"url":"https://github.com/exbotanical/vivisector","last_synced_at":"2026-01-05T20:30:16.331Z","repository":{"id":39182920,"uuid":"275040153","full_name":"exbotanical/vivisector","owner":"exbotanical","description":"Convert any object into an evented, reactive state machine","archived":false,"fork":false,"pushed_at":"2023-07-20T17:51:55.000Z","size":12077,"stargazers_count":2,"open_issues_count":7,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2023-12-23T03:04:39.857Z","etag":null,"topics":["custom-datatypes","event-driven-programming","javascript-proxy","metaprogramming","observable","observablearray","polymorphism","proxies","reactive","reactive-programming","reflection","self-modifying-code"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/vivisector","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/exbotanical.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-06-26T00:12:41.000Z","updated_at":"2022-08-16T16:57:56.000Z","dependencies_parsed_at":"2022-08-24T18:30:50.276Z","dependency_job_id":null,"html_url":"https://github.com/exbotanical/vivisector","commit_stats":null,"previous_names":["matthewzito/vivisector-js"],"tags_count":13,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exbotanical%2Fvivisector","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exbotanical%2Fvivisector/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exbotanical%2Fvivisector/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exbotanical%2Fvivisector/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/exbotanical","download_url":"https://codeload.github.com/exbotanical/vivisector/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239735257,"owners_count":19688262,"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":["custom-datatypes","event-driven-programming","javascript-proxy","metaprogramming","observable","observablearray","polymorphism","proxies","reactive","reactive-programming","reflection","self-modifying-code"],"created_at":"2025-02-19T21:17:03.087Z","updated_at":"2026-01-05T20:30:16.266Z","avatar_url":"https://github.com/exbotanical.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Vivisector Logo](/docs/vx.png)\n\n# Vivisector | Convert any object into an evented, reactive state machine ✂️\n\n[![Continuous Integration](https://github.com/MatthewZito/vivisector/actions/workflows/ci.yml/badge.svg)](https://github.com/MatthewZito/vivisector/actions/workflows/ci.yml)\n[![Continuous Deployment](https://github.com/MatthewZito/vivisector/actions/workflows/cd.yml/badge.svg)](https://github.com/MatthewZito/vivisector/actions/workflows/cd.yml)\n[![Coverage Status](https://coveralls.io/repos/github/MatthewZito/vivisector/badge.svg?branch=master)](https://coveralls.io/github/MatthewZito/vivisector?branch=master)\n[![npm version](https://badge.fury.io/js/vivisector.svg)](https://badge.fury.io/js/vivisector)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n## API and Documentation\n\n## Table of Contents\n\n- [Introduction](#intro)\n  - [Features](#feat)\n- [Install](#install)\n- [Documentation](#docs)\n  - [Getting Started](#start)\n  - [Event Types](#evtypes)\n  - [Methods](#methods)\n  - [State Commitment](#state)\n- [Next](#next)\n\n## \u003ca name=\"intro\"\u003e\u003c/a\u003e Introduction\n\n**Convert any object into an evented, reactive state machine.**\n\n`Vivisector` is a light-weight building block for pub/sub modeling, state management, and reactive programming. Here, we may add event listeners to plain objects and arrays, binding *N* actions to their state mutations. Registered actions can then intercept state transitions and decide whether to commit or revert them.\n\n```js\nconst { vivisect } = require('vivisector-js');\n\nconst state = vivisect({\n  firstName: '',\n  lastName: '',\n  email: ''\n})\n  .subscribe('set', ({ prevState, nextState, done }) =\u003e {\n    if (!isValidEmail(nextState.email)) {\n      emitErrorMessage();\n      done(false);\n    } else {\n      sendWelcomeEmail();\n      done(true);\n    }\n\n    ...\n  });\n```\n\n### \u003ca name=\"feat\"\u003e\u003c/a\u003e Features\n\n- 📦 zero dependencies\n- 🪶 light-weight and compact at [~ 2kb gzipped](https://bundlephobia.com/package/vivisector@1.5.0)\n- 👌 simple and well-documented API\n- 🔥 support for (and built with) TypeScript\n- ✨ available for ESM, UMD, and CommonJS build targets\n- 🚀 bind actions to a variable's state on the fly\n- 🔌 harness the power of reactive programming without the excess boilerplate\n- ✔️ preview, then declaratively cancel or commit state changes\n\n## \u003ca name=\"install\"\u003e\u003c/a\u003e Installation\n\n[Install via NPM](https://www.npmjs.com/package/vivisector)\n\nNPM:\n\n```bash\nnpm install vivisector-js\n```\n\nYarn:\n\n```bash\nyarn add vivisector-js\n```\n\n## \u003ca name=\"docs\"\u003e\u003c/a\u003e Documentation\n\nBefore we dive in, here's a couple of quick notes that are rather **important**:\n\n- `Vivisected` objects are COPIED by value, not reference\n- don't mutate state in callbacks - doing this will result in *undefined behavior*; that's what the `done` function is for\n- nested objects become their own proxies\n\n  For example, in the following code\n\n  ```js\n  const o = vivisect({ a: {} });\n\n  Object.assign(o.a, someObject);\n  ```\n\n  `o.a` will invoke events with a base state of `{}`\n\n### \u003ca name=\"start\"\u003e\u003c/a\u003e Getting Started\n\nLet's manage some evented state!\n\nFirst, we'll import the `vivisect` utility:\n\n```js\nconst { vivisect } = require('vivisector'); // assuming cjs for this tutorial, but Vivisector supports es modules, too\n```\n\nThis function will take our object or array and return an evented copy.\n\nIn this example, we'll `vivisect` an array and register a callback function for the `add` event. Our callback will be invoked whenever *new elements* are added to the array. We'll keep things simple for now by passing along the `alwaysCommit` option, which means state transitions associated with `add` events will always be committed.\n\n```js\nconst logAdditions = ({ type, prevState, nextState }) =\u003e {\n    console.log(`${type} event captured. ${prevState} --\u003e ${nextState}`);\n};\n\n// instantiate our `users` list - what an interesting bunch!\nconst users = vivisect(['Damo Suzuki', 'Soren Kierkegaard', 'Donald Knuth']);\n\n// every time an item is added to `users`, we want to invoke `logAdditions`\nusers.subscribe('add', logAdditions, { alwaysCommit: true });\n\n// let's bring someone fictional into the mix\nusers.push('Elric of Melnibone');\n// 'add event captured. ['Damo Suzuki', 'Soren Kierkegaard', 'Donald Knuth'] ==\u003e ['Damo Suzuki', 'Soren Kierkegaard', 'Donald Knuth', 'Elric of Melnibone']\n```\n\nBoth arrays and objects can be `vivisected` in this manner:\n\n```js\nconst albums = vivisect({ krautrock: ['Tago Mago', 'Monster Movie', 'Ege Bamyasi'] });\n```\n\nThe object's prototype is unaffected, save for the added event registrars (more on these later)\n```js\nconsole.log(Object.values(albums)[0].findIndex(i =\u003e i.startsWith('T')));; // 0\n```\n\nEvent handlers are registered by calling `subscribe`. This method will exist on every `vivisected` object:\n\n```js\nusers.subscribe(eventType, eventHandler, options);\n```\n\nAnd when we're done, we can remove the handler by passing a reference to it into the `unsubscribe` method:\n\n```js\nusers.unsubscribe(eventType, eventHandlerRef);\n```\n\n### \u003ca name=\"evtypes\"\u003e\u003c/a\u003e Event Types\n\nThis section documents all builtin `Vivisector` events and their behaviors.\n\n#### add\n\nA new element or property has been *added* to the target. 'Add' typically constitutes as a new indexed property that previously did not exist.\n\nCallbacks will receive a function, `done`, and an object consisting of:\n| Property | Value |\n| --- | --- |\n| **type** | Enum 'add', denoting the event-type that was triggered |\n| **prevState** | the previous state |\n| **nextState** | the next state, i.e. the result of the add event that was captured |\n\n**Fires on:** Additive array functions; adding new properties\n\n**Note:** Operations such as `Array.prototype.push` are considered `batched` events if provided more than a single argument\n\n#### set\n\nAn existing element or property has changed.\n\nCallbacks will receive a function, `done`, and an object consisting of:\n| Property | Value |\n| --- | --- |\n| **type** | Enum 'set', denoting the event-type that was triggered |\n| **prevState** | the previous state |\n| **nextState** | the next state, i.e. the result of the add event that was captured |\n\n**Fires on:** Setting existing properties; mutating indexed accessors\n\n#### del\n\nAn element or property has been deleted.\n\nCallbacks will receive a function, `done`, and an object consisting of:\n| Property | Value |\n| --- | --- |\n| **type** | String \"del\", denoting the event-type that was triggered |\n| **prevState** | the previous state |\n| **nextState** | the next state, i.e. the result of the add event that was captured |\n\n**Fires on:** methods such as `pop`; `delete` called on a property\n\n#### batched\n\nA batched event has occurred. Batched events are those which carry several state changes as the result of a single action. For example, `Array.prototype.unshift` may prepend an element and shifts each element. Similarly, `Array.prototype.push` may be a batched event if provided more than a single argument.\n\nCallbacks will receive a function, `done`, and an object consisting of:\n| Property | Value |\n| --- | --- |\n| **type** | String \"batched\", denoting the event-type that was triggered |\n| **prevState** | the previous state |\n| **nextState** | the next state, i.e. the result of the add event that was captured |\n\n**Fires on:** methods such as `shift`, `unshift`, `push` when called with multiple elements\n\n### \u003ca name=\"methods\"\u003e\u003c/a\u003e Methods\n\nMethods bound to all `vivisected` objects:\n\n#### subscribe (eventName: ISubscriptionEvent, handler: ISubscriptionCallback, opts?: ISubscriptionOpts) =\u003e IVivisectorApi\n\nBind the callback `handler` to fire whenever an event of `eventName` has been triggered.\n\n**Options:**\n| Property | Value |\n| --- | --- |\n| **alwaysCommit?** | a boolean indicating whether this action will always commit its state transitions. defaults to false |\n\n**Throws when**: provided an invalid event type or non-function handler\n\n**Example:**\n\n```js\nconst logMsg = function (event, done) {\n  // every time an item is added to the array, fire this callback\n  console.log(`Added item such that ${event.prevState} becomes ${event.nextState}`);\n  if (event.nextState.length) done(true);\n});\n\nconst languages = vivisect(['C', 'Go']).subscribe('add', logMsg);\n\nlanguages.push('JavaScript');\n// \"Added item such that ['C','Go'] becomes ['C','Go', 'JavaScript']\"\n```\n\n#### unsubscribe (eventName: ISubscriptionEvent, handler: ISubscriptionCallback, opts?: ISubscriptionOpts) =\u003e IVivisectorApi\n\nRemove an existing callback from the respective event-type to which it has been registered.\n\n**Options:** n/a\n\n**Throws when**: provided an invalid event type or non-function handler\n\n**Example:**\n\n```js\nconst logMsg = function (event) {\n  ...\n});\n\nconst queens = vivisect(['RuPaul', 'Alaska'])\n  .subscribe('add', logMsg, { alwaysCommit: true })\n  .unsubscribe('add', logMsg);\n\nqueens.push('Bianca Del Rio');\n// no log - handler was removed ^\n```\n\n### \u003ca name=\"state\"\u003e\u003c/a\u003e State Commitment\nAs we've seen throughout the documentation, `Vivisector` events provide the opportunity to commit or revert state mutations. Every event callback is provided a `done` function with the following signature:\n\n`done (commit: boolean)`\n\nYou'll have the opportunity to preview what the state transition would be by inspecting the `nextState` property. Then, you may programmatically commit the transition by passing `true` to the `done` function.\n\nPassing `false` or not invoking `done` at all will revert any state changes and `nextState` will not take effect. The exception to this rule is the `alwaysCommit` option, which may be passed when registering the callback.\n\n## \u003ca name=\"next\"\u003e\u003c/a\u003e What's Next for `Vivisector`?\n\nHere's a short list of upcoming features...\n\n- ~~cancellable state mutations~~\n- deferred and 'auto-async' state mutations\n- queued events\n- custom event types (bind specific and user-defined prototype methods)\n- optional batching\n\nContributions and feature requests are always welcome!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexbotanical%2Fvivisector","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexbotanical%2Fvivisector","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexbotanical%2Fvivisector/lists"}