{"id":13566494,"url":"https://github.com/alwins0n/pmsm","last_synced_at":"2025-04-04T00:30:55.375Z","repository":{"id":94439242,"uuid":"334618247","full_name":"alwins0n/pmsm","owner":"alwins0n","description":"Poor Man's State Management - Scala.js","archived":false,"fork":false,"pushed_at":"2021-08-01T14:53:12.000Z","size":44,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-11-04T20:42:33.166Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Scala","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/alwins0n.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-01-31T09:29:56.000Z","updated_at":"2023-03-11T17:55:15.000Z","dependencies_parsed_at":"2023-05-04T09:48:44.631Z","dependency_job_id":null,"html_url":"https://github.com/alwins0n/pmsm","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/alwins0n%2Fpmsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alwins0n%2Fpmsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alwins0n%2Fpmsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alwins0n%2Fpmsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alwins0n","download_url":"https://codeload.github.com/alwins0n/pmsm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247103306,"owners_count":20884023,"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":[],"created_at":"2024-08-01T13:02:10.780Z","updated_at":"2025-04-04T00:30:50.365Z","avatar_url":"https://github.com/alwins0n.png","language":"Scala","funding_links":[],"categories":["Scala"],"sub_categories":[],"readme":"# PMSM - Poor Man's State Management\n\nA super lightweight library for \"redux-like\" development in scala.js\n\n---\n\n# Motivation\nWhile there are existing state management solutions for scala.js (Diode, redux)\nmy requirements for a project were too simple to throw a complex framework at it\n\n# Design Goals\n- simplicity, ease of use and ease of understanding (no magic imports, etc.)\n- scala.js idiomatic\n- leverage FP but be pragmatic (don't obsess with purity, effect tracking, ...)\n- minimal/clean API (make common things easy and specialized things possible)\n\n# Basic Usage\n\n*TODO artifact coordinates*\n\n---\n\nGet started by instantiating a `Store[S, M]`.\nStore is a (stateful) central processing unit typed to hold a state `S` \nand accept messages of type `M`.\n\n```scala\nval store = Store(MyState()) // : Store[MyState, Any]\n```\n\ncreates a new \"generic\" store. It is capable of handling any type of message.\nIt can be useful to narrow acceptable messsage down to a type.\n\nUse\n\n```scala\nval store = new Store[MyState, MyMessage](MyState())\n```\n\nor\n\n```scala\nval store = Store.accepting[MyMessage].init(MyState())\n```\n\nto create a `Store[MyState, MyMessage]`.\n\nWhile the state can be any class, it is recommended to be a case class with sensible defaults.\ne.g.\n\n\n```scala\ncase class MyState(alerts: List[String] = Nil, ...)\n```\n\n*Note that in order for changes to be detected\n`equals` is called on the state or parts thereof. \nWhich is why it is recommended to use pure values/data*\n\n## Input\n\nWe now can send messages to the store of the acceptable type via `dispatch` \nor just using the apply method.\nOnce instantiated, a Store is a function `M =\u003e Unit` from usage side.\n\n```scala\nstore.dispatch(SomeMessage(\"param\"))\n// or\nstore(SomeMessage(\"param\"))\n```\n\n## Behaviour\n\nBehaviour is installed in the store by adding reducers.\nHave a look at the internal definition of a store's reducer:\n\n```scala\ntype Reducer = (S, M) =\u003e S \n```\n\nAdd behaviour to the store by defining a new reducer:\n\n```scala\nstore.addReducer { (s, m) =\u003e \n  ... // use m and return updated s\n}\n```\n\nThe `Reducer` will match on the message to decide on how to update the state \nas well as provide a default (the untouched state) for all unhandled cases.\n\nSince this is a common pattern it is syntactially improved by using a curried reducer.\n\n```scala\ntype ReducerCurried = S =\u003e M =\u003e S \n```\n\nThis is the curried form of a\npure function which takes a state and a message\nand returns the resulting or \"next\" state.\n\nIt can be installed as\n`S =\u003e PartialFunction[M, S]` which gives us the possibility\nto non-exhaustively list all messages that should be handled.\n\n```scala\nstore.reduce(state =\u003e {\n   case ChangeName(newName) =\u003e state.copy(nameState = ...)\n   case OtherMessage =\u003e ...\n   // no default case necessary since there is no exhaustiveness check\n})\n```\n\nIt can be read as: \"given a `state` use it to handle all listed messages\"\n\n### Behaviour Details\n\nAfter a message is received by the store all `Reducers` \nare invoked in the order of registration. That implies that later \nregistered state transitions of some message M\ncan react to previously installed behaviour (state changes). \nThe reduction step is performed \"transactionally\" \ni.e. if an error occurs the state is left\nunchanged (or rolled back if you will). \n\nThe state after each invocation \nis fed into the next `Reducer` resulting in a `foldLeft` semantic.\n\n## Output\n\nThe current state which is the last sucessfully reduced state is\navailable via `store.state`.\n\nFurthermore the downstream of the store can take two forms:\n`Subscription` (targeted state downstream) or \n`Listener` (targeted message downstream).\n\n### Subscription\n\nIf the state changes due to reduction \nthe changed state can be consumed by subscribers. \nThey take the form `S =\u003e Unit` and can be created as follows:\n\n```scala\nstore.subscribe { state =\u003e \n  ... // use changed state\n}\n```\n\nChange detection works via deep equals on the state, as mentioned before.\n\n### Listeners\n\nThe `Listener` is a side-effecting function which gives the store\nthe capability to react to messages.\n\n```scala\ntype Listener = M =\u003e Unit\n```\n\nSimilar to `addReducer` installing a listener is done by calling:\n\n```scala\nstore.addListener { m =\u003e \n  ... // use m\n}\n```\n\nAnd similar to `reduce` the `listen` API handles messages non-exhaustive.\n\n```scala\nstore.listen { \n  case Handled(param) =\u003e ...\n}\n```\n\nDispatching a message from within a listener is possible and encouraged \n(e.g. for messages resulting in ajax calls). \n\n*Note that if the current \"digest\" is not finished\n(not all listeners are processed) the dispatched message \nis queued to ensure a clean ordering of messages.*\n\n# Advanced Usage\n\nThe previous sections introduced concepts and APIs which are useful only\nin the simplest of use cases.\n\nIf state and messages grow in complexity adding behaviour becomes cumbersome, \namount of messages that are not handled properly increases \nand state change detection is always global.\n\nThere are APIs to zoom in on state and message types to make the store\nbehave more robust and sensible while keeping the boilerplate to a minimum\n\n## Message Selection\n\nAssume there is a component capable of issuing a `Message` \nhaving two concrete types (message is a \"sum-type\"):\n\n```scala\n// i.e. Message = ChangeName | ConfirmName\nsealed trait ComponentMessage\nobject ComponentMessage {\n  final case class ChangeName(newName: String) extends ComponentMessage\n  case object ConfirmName extends ComponentMessage\n}\n\n```\n\nto install a reducer for a particular message type use `addMessageReducer`\n\n```scala\nstore.addMessageReducer[ChangeName] { (state, message) =\u003e // message is guaranteed to be of type \"ChangeNamed\"\n    state.copy(nameState = message.newName)\n    ...\n})\n```\n\n\nTo use the curried function syntax introduced earlier, but\nbenefit from type safety (i.e. exhaustiveness matching) use `reduceMessage`.\n\n```scala\nstore.reduceMessage[ComponentMessage](state =\u003e {\n    case ChangeName(newName) =\u003e state.copy(nameState = ...)\n    // warning/error since ConfirmName is not handled\n})\n```\n\n---\n\nListeners can also installed pre-selecting messages to be handled with `listenTo`.\n\n```scala\nstore.listenTo[ChangeName] { message =\u003e // mesage is guaranteed ot be of type ChangeName\n  console.log(s\"name ${message.name} has been entered and processed to be ${store.state.sanitizedName}\")\n}\n```\n\nNote that the reduced state after receiving the message `ChangeName`\nis accessed via `store.state` in the listener.\n\n## State Optics\n\nSubscriptions without specific change detection have limits in their usefulnes.\nIn general we are only interested in certain changes of the state.\n\nThis is why a subscription is actually function `S =\u003e Unit` composed of two functions\n- `S =\u003e A` (selection)\n- `A =\u003e Unit` (consumption)\n\nAssume we have a state `case class State(componentState = ComponentState(), ...)`\nand we want to focus on the component substate.\n\nTo create a subscription we first `select` a slice of the state\nand `subscribe` to changes:\n\n```scala\nstore.select(_.componentState).subscribe { cs =\u003e \n  ... // use changed componentState cs\n}\n```\n\nIf your downstream consumers are functions or side effecting methods,\nsubscriptions become\n\n```scala\nstore.select(_.componentState).subscribe(myComponent.update)\n```\n\nIf a message is received by the store, after the state is reduced\nthe following mechanics are invoked:\n\n- compute the slice of each subscription (selection)\n- compare each slice with the slice of the previous state\n- if changed, invoke the downstream function (consumption) with the slice as parameter\n\n`select` thus slices the state for readonly functionality with a \"getter\" function\nTo \"upgrade\" the slice for adding behaviour use `modifying` which takes a \"setter\" function\n\n```scala\nval sliced = store\n  .select(_.componentState)\n  .modifying((s, cs) =\u003e s.copy(componentState = cs))\n// results in something like `Store[ComponentState, M]\n```\n\n\"Getter\" and \"setter\" can also be combined with the `lens` method\n\n```scala\nval lensed = store.lens(_.componentState)((s, a) =\u003e s.copy(componentState = a))\n// equivalent to \"sliced\"\n```\n\nThis state is now able to add reducers and subscribe to changes based on the slice of the state\n\n```scala\nlensed.reduce(compState =\u003e {\n  case SomeMessage =\u003e compState.doFoo() // returns a ComponentState\n    ...\n})\n```\n\n```scala\nlensed.subscribe(myComponent.update)\n```\n\n# Error Handling\n\n`Store` offers two APIs for error handling: `addErrorListener` and `addErrorHandler`\n\nAny exceptions occuring in the reduction steps or at downstream consumption\ncan be either consumed for e.g. logging (`addErrorListener`)\nor used to update the state in a certain way e.g. alerts (`addErrorHandler`)\n\nThe functions dealing with errors are assumed to be fail safe - any exceptions\nwill not be handled further.\n\n# Full Example with Fluent API\n\n```scala    \n// dummy \"components\" to test output\nval counterComponentBuffer = ListBuffer.empty[Int]\nval alertBuffer = ListBuffer.empty[String]\n\n// \"complex\" message\nsealed trait CounterMessage\nobject CounterMessage {\n  case object Increment extends CounterMessage\n  case object SetToDefaultValue extends CounterMessage\n}\n\ncase object CheckCounter\n\n// event not dispatched by components but by external sources\ncase object SevenWasConfirmedEvent\n\n// the test state\ncase class AppState(component: CounterState, alertMessage: String)\ncase class CounterState(value: Int) {\n  def increment(): CounterState = copy(value = value + 1)\n  def setTo(int: Int): CounterState = CounterState(int)\n}\n\n// kick off\nval init = AppState(component = CounterState(0), alertMessage = \"\")\nval store = Store(init)\nstore\n  .addListener(msg =\u003e println(s\"DEBUG: msg received: $msg\"))\n  .addErrorHandler { (ex, state, msg) =\u003e\n    println(s\"ERROR: error ${ex.getMessage} when handling $msg\")\n    state.copy(alertMessage = \"An error occured\")\n  }\n  .select(_.component) // focus into counter component\n  .subscribe(updatedCounterState =\u003e\n    counterComponentBuffer prepend updatedCounterState.value\n  )\n  .modifying((state, counterState) =\u003e state.copy(component = counterState))\n  .reduceMessage[CounterMessage](counterState =\u003e {\n    case CounterMessage.Increment         =\u003e counterState.increment()\n    case CounterMessage.SetToDefaultValue =\u003e counterState.setTo(7)\n  })\n  .listen {\n    case CheckCounter =\u003e\n      if (store.state.component.value == 7) {\n        // async call after which on success:\n        store.dispatch(SevenWasConfirmedEvent)\n      }\n  }\n  .delegate // back to root store\n  .lens(_.alertMessage)((state, alert) =\u003e state.copy(alertMessage = alert))\n  .subscribe(newMsg =\u003e alertBuffer prepend newMsg)\n  .addMessageReducer[SevenWasConfirmedEvent.type]((_, _) =\u003e \"The 7 was confirmed\")\n\n// init all components by pushing the state manually\nstore.push()\n```\n\n# Cookbook\n\n## Usage for HTML output\n\nNo assumptions on output formatting is been made here. Use your subscriptions\nas you wish. It could be HTML outputting or console logging, etc.\n\n## Functional Initialisation\n\nTo initialize the store \"in one go\" and have typesafe (exhaustiveness)\nreduction, use the builder DSL\n\n```scala\nval store = Store.accepting[MyMessage].reducing(MyState(...))((s, m) =\u003e m match {\n   ...\n}) // = Store[MyState, MyMessage]\n```\n\n## Async\n\n*TODO* describe better\n\nThere is no extra magic to support async behaviour.\nUse a `Listener` to catch messages that should result in async calls (eg. XHR)\nand feed the result back into the store by dispatching to it in the async callback.\n\n## Connect multiple Stores (Parent - Child)\n\n*TODO* better example via listeners and dispatch?\n\n```scala\nval parent = Store(Nil) // accepts Any message\nval child = Store.accepting[TestMessage].init(Nil)\n\nchild.addListener(parent) // parent is now notified by all child messages\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falwins0n%2Fpmsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falwins0n%2Fpmsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falwins0n%2Fpmsm/lists"}