{"id":21304970,"url":"https://github.com/maximilianmairinger/josm","last_synced_at":"2026-01-29T03:40:10.165Z","repository":{"id":41107615,"uuid":"249777763","full_name":"maximilianMairinger/josm","owner":"maximilianMairinger","description":"An object oriented state manager.","archived":false,"fork":false,"pushed_at":"2024-10-22T23:35:52.000Z","size":474,"stargazers_count":0,"open_issues_count":7,"forks_count":0,"subscribers_count":1,"default_branch":"v1","last_synced_at":"2025-04-13T12:39:46.526Z","etag":null,"topics":["client","client-side","declarative","manager","object-oriented","server-side","state","web"],"latest_commit_sha":null,"homepage":"https://maximilian-mairinger.gitbook.io/jsom/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maximilianMairinger.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2020-03-24T17:47:22.000Z","updated_at":"2024-10-22T23:35:56.000Z","dependencies_parsed_at":"2024-10-23T06:06:33.919Z","dependency_job_id":"3572cc97-9b09-4faa-8e6d-ce4d7c5e4a4c","html_url":"https://github.com/maximilianMairinger/josm","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximilianMairinger%2Fjosm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximilianMairinger%2Fjosm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximilianMairinger%2Fjosm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maximilianMairinger%2Fjosm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maximilianMairinger","download_url":"https://codeload.github.com/maximilianMairinger/josm/tar.gz/refs/heads/v1","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248937200,"owners_count":21186184,"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":["client","client-side","declarative","manager","object-oriented","server-side","state","web"],"created_at":"2024-11-21T16:16:30.829Z","updated_at":"2026-01-29T03:40:10.132Z","avatar_url":"https://github.com/maximilianMairinger.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\r\ndescription: Object-oriented state manager\r\n---\r\n\r\n# Josm\r\n\r\nJosm is an \\(JS\\) **o**bject-oriented **s**tate **m**anager for the web \u0026 node, which aims to be both, a lightweight observable \\(called Data\\) implementation \u0026 a feature rich state manager, providing an awesome developer experience. It may be used to build declarative UI (frameworks) or as a server side data store (using a mongodb adapter for persistent storage) suitable for realtime applications (with a websocket bridge).\r\n\r\n## Usage\r\n\r\nNote that the state manager can be tree-shaken off the observable implementation using esImports \u0026 a properly configured bundler \\(e.g. webpack\\).\r\n\r\n```ts\r\nimport { Data, ... } from \"josm\"\r\n```\r\n\r\n### Observable\r\n\r\n#### Data\r\n\r\nObservables are called `Data` in Jsom. These are the simplest building blocks as they observe the mutations of one primitive value over time. Or in other words: One Data instance contains one value. As you change that value all prior registered oberv**ers** \\(callbacks\\) get notified \\(called\\).\r\n\r\n```typescript\r\nlet data = new Data(1)\r\n\r\n// Calls the observing function every time data is set\r\ndata.get((value) =\u003e {\r\n  console.log(value)\r\n}) \r\n\r\ndata.set(10)\r\ndata.set(100)\r\n\r\nconsole.log(data.get()) // This gets the current value of data (100)\r\n```\r\n\r\nThis would log `1; 10; 100; 100`.\r\n\r\n#### DataCollection\r\n\r\nTo observe multiple values under one obser**ver** nest `Data`s into one `DataCollection`.\r\n\r\n```typescript\r\nlet data1 = new Data(1)\r\nlet data2 = new Data(2)\r\n\r\nlet dataCollection = new DataCollection(data1, data2)\r\n\r\ndataCollection.get((value) =\u003e {\r\n  console.log(value)\r\n}, /*initialize: boolean = true*/ false)\r\n\r\ndata1.set(10)\r\ndata1.set(100)\r\ndata2.set(20)\r\n```\r\n\r\nThis would log `[10, 2]; [100, 2]; [100, 20]`.\r\n\r\n#### DataSubscription\r\n\r\nBoth Data and DataCollection return a `DataSubscription` when subscribing \\(via `Data#get(cb)`\\). These can be used to manage the subscription state \u0026 can be viewed independently of their source \\(their source can be changed\\).\r\n\r\n```typescript\r\nlet data1 = new Data(1)\r\n\r\nlet dataSubscription = data.get((value) =\u003e {\r\n  console.log(value)\r\n})\r\n\r\ndataSubscription.deactivate()\r\ndata1.set(10)\r\ndataSubscription.activate(/*initialize: boolean = true*/ false)\r\n\r\ndata1.got(dataSubscription)\r\ndata1.get(dataSubscription)\r\n\r\n\r\nconsole.log(dataSubscription.data())         // Gets the current data (data1)\r\nconsole.log(dataSubscription.active())       // Gets the current active status (true)\r\nconsole.log(dataSubscription.subscription()) // Gets the current subscription (console.log)\r\n\r\n\r\nlet dataCollection = new DataCollection(new Data(2), new Data(3))\r\n\r\ndataSubscription.data(dataCollection, /*initialize: boolean = true*/)\r\ndataSubscription.active(false)\r\ndataSubscription.subscription((d2, d3) =\u003e {\r\n  console.log(\"Custom Subscription\", d2, d3)\r\n})\r\n```\r\n\r\n--------\r\n\r\n### DataBase\r\n\r\n`DataBase`s function similar to `Data`s, only that they are used to store multiple indexed `Data`s (objects / arrays).\r\n\r\n```ts\r\nlet db = new DataBase({\r\n  key1: 1,\r\n  key2: 2\r\n  nestedKey: [\"a\", \"b\", \"c\"]\r\n})\r\n```\r\n\r\n\u003e Note: Observed objects can be circular\r\n\r\n#### Traversal\r\n\r\nThis instance can be traversed like a plain object. The primitive values are wrapped inside `Data`s.\r\n\r\n```ts\r\nconsole.log(db.key1.get())          // 2\r\nconsole.log(db.nestedKey[2].get())  // \"c\"\r\n```\r\n\r\n#### Bulk change\r\n\r\nAll operations concerning more than a primitive can be accessed via the various function overloads on a `DataBase`.\r\n\r\nA simple example for this would be to change multiple values of an object.\r\n\r\n```ts\r\ndb({key1: 11, key2: 22})\r\nconsole.log(db.key1.get(), db.key2.get())   // 11, 22\r\n```\r\n\r\n#### Bulk add or delete\r\n\r\nAdding or deleting properties (`undefined` stands for delete)\r\n\r\n```ts\r\ndb({key3: 33, key1: undefined})\r\n```\r\n\r\n##### Getting\r\n\r\nRetrieving the whole object\r\n\r\n```ts\r\n// once\r\nconsole.log(db())         // { key2: 22, key3: 33, nestedKey: [\"a\", \"b\", \"c\"] }\r\n\r\n// observed\r\ndb((ob) =\u003e {\r\n  console.log(\"db\", ob)\r\n})\r\n```\r\n\r\n\u003e Note: The observer is being invoked every time something below it changes. So when `db.nestedKey[0]` is changed, the event is propagated to all observers above or on it.\r\n\r\n#### Relative traversal\r\n\r\nThe object can also be traversed via an overload\r\n\r\n```ts\r\ndb(\"nested\", 2).get()   // Equivalent to db.key2[2].get()\r\n```\r\n\r\nEven `Data`s can be used as key here\r\n\r\n```ts\r\nlet lang = new DataBase({\r\n  en: {\r\n    greeting: \"Hello\",\r\n    appName: \"Cool.oi\"\r\n  },\r\n  de: {\r\n    greeting: \"Hallo\",\r\n    appName: \"Cool.io\"\r\n  }\r\n})\r\n\r\nlet currentLangKey = new Data(\"en\")\r\n\r\nlang(currentLangKey).appName.get((val) =\u003e {\r\n  console.log(val)                              // \"Cool.oi\"  // initially english\r\n})   \r\ncurrentLangKey.set(\"de\")                        // \"Cool.io\"  // now german\r\nlang.en.appName.set(\"Cool.io\")\r\ncurrentLangKey.set(\"de\")                        //            // no change (\"Cool.io\" \u003e \"Cool.io\") \r\n```\r\n\r\n\u003e Caveat: `name` (and some other properties) cannot be used, as they are already defined on the function object\r\n\r\n\u003e Caveat: IntelliSense will show all properties that function has\r\n\r\n\r\n### Derivables\r\n\r\nWith the above interface virtually every manipulation is possible, but often not very handy.\r\n\r\n\u003e What increasing a number / appending to a string would look like\r\n\r\n```ts\r\nlet num = new Data(2)\r\nnum.set(num.get() + 1)\r\n\r\nlet str = new Data(\"Hel\")\r\nstr.set(str.get() + \"lo\")\r\n```\r\n\r\nThats what derivables solve. Defining repeated manipulation processes once, directly on the type it is made for.\r\n\r\n```ts\r\nconst DATA = setDataDerivativeIndex(\r\n  class Num extends Data\u003cnumber\u003e {\r\n    inc(by: number = 1) {\r\n      this.set(this.get() + by)\r\n    }\r\n    dec(by: number = 1) {\r\n      this.set(this.get() - by)\r\n    }\r\n  },\r\n  class Str extends Data\u003cstring\u003e {\r\n    append(txt: string) {\r\n      this.set(this.get() + txt)\r\n    }\r\n  }\r\n)\r\n```\r\n\r\nWith this declared, it can be used on the fly as the typing adapts.\r\n\r\n\u003e Note: While this example is really just about convenience, it excels when defining more complex procedures (like injecting something into a string, etc.)\r\n\r\n```ts\r\nlet num = new DATA(2)\r\nnum.inc()\r\n\r\nlet str = new DATA(\"Hel\")\r\nstr.append(\"lo\")\r\n```\r\n\r\n\u003e Caveat: No function name can be used twice withing all dataDerivables or within all dataBaseDerivables.\r\n\r\nWhile derivable usage on `Data`s is substantial on its own, applying it to certain interfaces `DataBases`s does provide seamless interaction on a very high level.\r\n\r\n```ts\r\ninterface Person {\r\n  age: number,\r\n  firstName: string,\r\n  lastName: string\r\n}\r\n\r\nconst DATABASE = setDataBaseDerivativeIndex(\r\n  class Pers extends DataBase\u003cPerson\u003e {\r\n    happyBirthday() {\r\n      this.age.inc()\r\n    }\r\n  }\r\n)\r\n\r\nlet person = new DATABASE({\r\n  age: 18,\r\n  firstName: \"Max\",\r\n  lastName: \"Someone\"\r\n})\r\n\r\nperson.happyBirthday()\r\n```\r\n\r\n\r\n### Specifics\r\n\r\n#### Subscription pertinention\r\n\r\nWhen nesting observer declarations in synchronous code (which would without precautions result in a memory leak), josm tries to unsubscribe the old (unused) subscription in favor of the new one.\r\n\r\n\u003e Bad practice: In real code, use a [`DataCollection`](#DataCollection) instead. While this would work (without a memory leak), it is not clean nor performant and breaks when the data1 callback were asynchronous. This may however be unavoidable in some situations. The following however is just for demonstration.\r\n\r\n```ts\r\nlet data1 = new Data(\"value1\")\r\nlet data2 = new Data(\"value2\")\r\n\r\ndata1.get((d1) =\u003e {\r\n  data2.get((d2) =\u003e {\r\n    console.log(d1, d2)\r\n  })\r\n})\r\n```\r\n\r\n\r\n## Contribute\r\n\r\nAll feedback is appreciated. Create a pull request or write an issue.\r\n\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaximilianmairinger%2Fjosm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaximilianmairinger%2Fjosm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaximilianmairinger%2Fjosm/lists"}