{"id":18924638,"url":"https://github.com/dash-os/tcl-state-manager","last_synced_at":"2026-01-28T21:01:30.133Z","repository":{"id":71821440,"uuid":"84890210","full_name":"Dash-OS/tcl-state-manager","owner":"Dash-OS","description":"Extensible Tcl State Containers written in pure Tcl.","archived":false,"fork":false,"pushed_at":"2017-03-14T01:13:01.000Z","size":14,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-02T07:39:09.656Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Tcl","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/Dash-OS.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,"zenodo":null}},"created_at":"2017-03-14T01:09:44.000Z","updated_at":"2018-10-11T03:20:05.000Z","dependencies_parsed_at":null,"dependency_job_id":"dabf401d-3cd5-4ecc-9504-6a6c3857ed61","html_url":"https://github.com/Dash-OS/tcl-state-manager","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Dash-OS/tcl-state-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-state-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-state-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-state-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-state-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Dash-OS","download_url":"https://codeload.github.com/Dash-OS/tcl-state-manager/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-state-manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28851838,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-28T15:15:36.453Z","status":"ssl_error","status_checked_at":"2026-01-28T15:15:13.020Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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-11-08T11:07:39.075Z","updated_at":"2026-01-28T21:01:30.128Z","avatar_url":"https://github.com/Dash-OS.png","language":"Tcl","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tcl State Manager (TSM)\n\n\u003e **Note:** Currently setup to work in the Tcl Package Manager we have not yet released \n\u003e so this likely will not work quite yet without some changes.  This is just an initial\n\u003e commit.\n\nManaging the state of our applications can often prove to be a task which gets\nmore and more unorganized and messy as our applications grow and evolve.  As \nmore side effects are built off of our data model(s) it becomes a necessity to \nhave a clear way to organize and manage the application state. \n\nTSM was built to solve this need.  It provides many powerful core features \nthat make it extensible, useful, and downright powerful.\n\n#### Core Features\n\n - Singleton or \"Keyed\" states supported\n - Extendable type validation\n - Extendable \"query builders\" and \"modifiers\"\n - Include or build \"middlewares\" that hook into the lifecycle\n - Powerful and absolutely simple \"getters\" and \"setters\"\n\n#### Included Middlewares\n\n - Query Subscriptions (execute $cmd when meets conditions)\n - Persistent Layer via sqlite \n - JSON Serialization via yajltcl\n - Simple API to build \u0026 share your own!\n\n## Introduction\n\nTSM is a schema-backed state management utility which uses many optimization \ntechniques to provide a performant and reliable state layer to your application(s).\n\nIt is not meant to be a general-purpose data container like **`[dict]`** or **`[list]`** but \nrather a tool to manage and maintain your important pieces of state within your \napplication.  \n\n## Registering a State Schema\n\nLets start by looking at a simple example of registering a \"singleton\" state within \nour application.\n\n```tcl\nstate register MyState {\n  items {\n    required string foo\n    required number bar\n    optional ip     baz\n  }\n}\n```\n\nThis provides us with a data model which ends up taking the form: `[dict create foo \u003cstring\u003e bar \u003cnumber\u003e ip \u003cip\u003e]`.\nEach value is validated when set to insure our data matches the expected format.  We have also identified \n`foo` and `bar` as required items which means we will not be able to set our state at all unless they are defined \nin the first \"setting\" of the state.  In this case that may not be as useful, but more on that later.\n\nThe full API of the registration will come.  \n\n## Setting Your State\n\nSetting of the state uses the same `[state]` command that we used for registration.\n\n### Singleton State \n\nIn it's simplest form it takes the same form as setting a `[dict]`.\n\n```tcl\nstate set MyState [dict create foo foo bar 123]\n```\n\nState is meant to be managed throughout your application.  Setting of your state will cause a (merge)\nwith the previous values (if any).  For example, when we set our state again but only provide the ip, \nit does not raise an error because foo and bar are already included.  \n\n```tcl\nstate set Mystate [dict create ip 192.168.1.1]\n```\n\nOur state will now take a form which resembles `[dict create foo foo bar 123 ip 192.168.1.1]`\n\n### Keyed State\n\n\nKeyed State provides us with a much more powerful data layer.  With keyed state, each unique \n\"key\" will be a separate \"entry\" within your state.  Your data is de-normalized and saved in a \nway which makes it easy to extend and build upon.\n\nHere is an example of a more powerful \"keyed\" schema which provides us with indexed keys, configuration options, \"middlewares\", and more.\n\n```tcl\nstate register MyKeyedState [dict create async 1 batch 0] {\n  middlewares { persist subscriptions }\n  config { async $async batch $batch }\n  items {\n    key number      integrationID\n    required bool   active\n    optional bool   state\n    optional string ref\n  }\n}\n```\n\n\u003e Middlewares are also supported with singleton state containers.\n\nWith keyed state, the \"key\" is required every time you wish to set an entry within your state. Lets \nimagine our application is running and setting the state at various points:\n\n```tcl\nstate set MyKeyedState [dict create integrationID 1 active 0]\nstate set MyKeyedState [dict create integrationID 2 active 1 state 0]\nstate set MyKeyedState [dict create integrationID 3 active 0 state 1 ref fooRef]\nstate set MyKeyedState [dict create integrationID 1 active 1]\nstate set MyKeyedState [dict create integrationID 1 ref barRef]\n\n# These would produce errors\n\n% state set MyKeyedState [dict create integrationID 4 state 1]\n[State MyKeyedState]: \u003cSchema Error\u003e missing required item \"active\" \nwhile attempting to set \"integrationID 4 state 1\"\n\n% state set MyKeyedState [dict create integrationID 4 active foo]\n[State MyKeyedState]: \u003cType Error\u003e while setting \"active\", expected \"bool\"\n```\n\nOur state will now look something like:\n\n```tcl\n{\n  1 {active 1 ref barRef}\n  2 {active 1 state 0}\n  3 {active 0 state 1 ref fooRef}\n}\n```\n\n#### Setting Multiple Values\n\nWe can also set multiple values simultaneously when needed.\n\n```tcl\nstate set MyKeyedState \\\n  [dict create integrationID 1 active 0] \\\n  [dict create integrationID 2 active 1 state 0] \\\n  [dict create integrationID 3 active 0 state 1 ref fooRef]\n```\n\n### Getting Our State\n\nThere are quite a few powerful ways that we can query and get our state based upon \nour needs as our application does its thing.  In it's simplest form we simply call\n`[state get]` with the name of the state.\n\n#### Singleton State\n\n```tcl\n% set state [state get MyState]\nfoo foo bar 123 ip 192.168.1.1\n```\n\n#### Keyed State\n\n```tcl\n% set state [state get MyKeyedState]\n1 {active 1 ref barRef} 2 {active 1 state 0} 3 {active 0 state 1 ref fooRef}\n```\n\nHowever, we also a few other options that we can use to filter our results. Below\nare a few of the options that can be used\n\n```tcl\n% set data [state get MyState foo ip]\nfoo foo ip 192.168.1.1\n\n% set data [state get MyState ip]\nip 192.168.1.1\n\n% set data [state get MyKeyedState [list 1 3]]\n1 { active 1 ref barRef } 3 { active 0 state 1 ref fooRef }\n\n% set data [state get MyKeyedState [list 1 3] [list active]\n1 {active 1} 3 {active 0}\n\n% state pull MyState foo bar\n% puts \"$foo $bar\"\nfoo 123\n\n% state pull MyKeyedState 3 active ref\n% puts \"Key 3 has active $active with ref $ref\"\nKey 3 has active 1 with ref fooRef\n\n% set data [state get MyKeyedState {} [list ref active]]\n1 {active 1 ref barRef} 2 {active 1} 3 {active 0 ref fooRef}\n\n% set values [state withKey MyKeyedState {} state]\n1 1 2 1 3 0\n\n% set values [state withKey MyKeyedState {} ref]\n1 barRef 3 fooRef\n\n% set keys [state keys MyKeyedState]\n1 2 3\n\n% set results [state query MyKeyedState [dict create match foo* ids [list 1 2 3]] {\n    ref match        | $match\n    integrationID in | $ids\n  }]\n3\n\n% set values [state values MyKeyedState]\n{integrationID 1 active 1 ref barRef} {integrationID 2 active 1 state 0} \n{integrationID 3 active 0 state 1 ref fooRef}\n\n# Assumes serializer middleware is defined\n% set json [state serialize MyKeyedState [list 1 2] ref\n{ \"1\": { \"ref\": \"barRef\" } } ; # Only 1 has \"ref\"\n\n```\n\nAs I have time I will go into detail on the following.  Below are various examples \nof extending and building the state, using middleware, and/or examples of commands:\n\n\n### Subscriptions Middleware\n\nSubscriptions used to follow a pattern more like proc where you would define\n{arg1 arg2 arg3...}, but it is far more performant to evaluate the script within \nthe context of our evaluator, giving you access to many variables. \n\nIt is probably a good idea to use the \"body\" of the subscription to call your \ncommands rather than writing long scripts within the body itself.\n\nThe executed body has directly access to the entire context that caused the execution\nwhich is the data structure passed to middlewares.  The \"snapshot\" is unique to the \ngiven key within the state and does not reflect information about any other key.\n\nBelow is a general example of each variable as well as an idea of the form it will \nhave and the data that will be present.\n\n```tcl\n$keyID    \u003ckey id\u003e\n$keyValue \u003ckey value\u003e\n$set      [list \u003c...keys set\u003e]\n$created  [list \u003c...keys created\u003e]\n$changed  [list \u003c...keys changed\u003e] \n$keys     [list \u003c...keys present\u003e]\n$removed  [list \u003c...keys removed\u003e]\n$setters  [dict create \u003c...dict provided during subscribe, if any\u003e]\n$items    [dict create \n  \u003citemKey\u003e [dict create value \u003ccurrent value\u003e prev \u003cprevious value\u003e]\n  \u003c...other items\u003e\n]\n```\n\n```tcl\nstate subscribe MyKeyedState [dict create one two] {\n  conditions {\n    ip match | 192.168*\n  }\n} { \n  puts \"Subscription Activated for $keyID\"\n  puts \"Key Value: $keyValue\"\n  puts \"Items: $items\"\n  \n  # ... call your command!\n}\n```\n\n```tcl\nstate subscribe MyKeyedState [dict create op \u003e n 2] {\n  conditions {\n    ref changed\n    active = | 1\n    integrationID $op | $n\n    ip match | 192.168*\n  }\n} { \n  puts \"Subscription Activated for $keyID\"\n  puts \"Key Value: $keyValue\"\n  puts \"Items: $items\"\n  puts \"Setters: $setters\" ; # {op \u003e n 2}\n  # ... call your command!\n}\n```\n\n\u003e By default subscriptions are both asynchronous and provide \"snapshot batching\".  This means \n\u003e that we intelligently merge snapshots that occcur within the same event loop evaluation and \n\u003e will not run the evaluation until the event loop calls it.  \n\u003e\n\u003e You may use \"config\" to modify this behavior. \n\n```tcl\nstate subscribe MyKeyedState [dict create op set] {\n  config { async 1 batch 0 }\n  conditions { ref changed; active $op }\n} { ... }\n```\n\n### State Persistent Middleware\n\nWhen provided during registration, the state will be persisted into a sqlite \ndatabase.  Similar to subscriptions, persistence evaluation is both asynchronous\nand batches snapshots unless specified otherwise through the configuration.\n\nWill go more into this in the future, but it's essentially completely transparent. \nIt also automatically modifies and copies tables as you change the schema so that,\nif possible, we will copy the values over to the new schema.\n\n\n\n### Custom Types \n\nVariables available to type validators are as follows:\n\n```tcl\n$value    \u003ccurrent value\u003e\n$prev     \u003cprevious value\u003e\n$params   [list \u003c...params defined after | during registration\u003e]\n$setters  [dict create \u003c...dict provided during subscribe, if any\u003e]\n```\n\nIn addition to validation, types can define hooks that will be processed before \nand after validation occurs.  This allows us to setup a value for evaluation \nand, if needed, modify it before it is saved to our state.\n\n```tcl\nstate type register enum {\n  validate { expr { $value in $params } }\n  json {\n    if {[string is entier -strict $value]}     { $json map_key $key number $value\n    } elseif {[string is bool -strict $value]} { $json map_key $key bool   $value\n    } else {                                     $json map_key $key string $value\n    }\n  }\n}\n\n# Modify bools to always be saved as 0 / 1 for unified processing and queries\nstate type register bool {\n\tvalidate { string is bool -strict $value }\n\tpost     { expr { bool($v) } }\n\tjson     { $json map_key $key bool $value }\n}\n\n# Normalize IP before we validate.  In this case [ipNormalize] returns an empty \n# string if the value was not a valid IP.  This also insures all IP's saved to our \n# state with this type will be normalized.\nstate type register ip {\n\tpre      { ipNormalize $value  }\n\tvalidate { expr {$value ne {}} }\n\tjson     { $json map_key $key string $value }\n}\n```\n\n\u003e `json` is only required for the serializer middleware and is likely to be changed \n\u003e to become part of its configuration / setup.\n\n\n\n\n\n### Custom \"Queries\" \n\nQueries are utilized by the `[state query]` commands to provide a means for filtering\nthe state and efficiently finding any matching keys.  In addition, the compiled expressions\nare made available for use outside of the query command (for example, with `[state subscribe]`)\n\nQueries \"evaluate\" command is evaluated in the same context as queries and subscriptions so they \nhave access to the same variables defined above (`snapshot`).  In addition they have access to \n(and technically so do subscription scripts):\n\n```tcl\n$value  \u003cvalue of queried\u003e\n$prev   \u003cprev value of queried\u003e\n$params \u003cparams provided after | during registration\u003e\n```\n\n```tcl\n# conditions { name created }\nstate query register created {\n  active 1\n  alias {added}\n  evaluate { expr { $key in $created || ( $key eq \"*\" \u0026\u0026 $created ne [list] ) } }\n}\n\n# conditions { name removed }\nstate query register removed {\n  active 1\n  alias {deleted}\n  evaluate { expr { $key in $removed || ( $key eq \"*\" \u0026\u0026 $remove ne [list] ) } }\n}\n\nstate query register \u003e_ {\n  active 1\n  alias {\"rises above\"}\n  evaluate { expr { $value \u003e $params \u0026\u0026 $prev \u003c= $params } }\n}\n\n# conditions { name = | foo }\nstate query register = {\n  alias {eq == equal}\n  evaluate { expr { $value == $params } }\n}\n\n# conditions { name not equal | bar }\nstate query register != {\n  alias {ne \"not equal\"}\n  evaluate { expr { $value != $params } }\n}\n\n# conditions { name match | -nocase br* }\nstate query register match {\n  evaluate {\n    if {\"-nocase\" in $params} {\n      set params [string trim [string map {\"-nocase\" \"\"} $params]]\n      lappend args -nocase\n    } else { set args {} }\n    string match {*}$args $params $value\n  }\n}\n\n```\n\n\u003e Note that \"active\" is an important property which indicates that we are expecting \n\u003e that the given value must have been \u003cset\u003e in order to evaluate as true.  Therefore \n\u003e we can optimize evaluations in a significant way when active queries are defined.\n\u003e\n\u003e In addition, active queries are always evaluated before inactive queries.\n\n### Query Modifiers\n\nWe may also define \"query modifiers\" which provide hooks into a queries lifecycle \nwhen defined.  These allow us to modify the values before the query evaluates them\nso that we can provide a more powerful level of evaluation and customization.\n\nModifiers may be chained and will be executed in the order they were defined. Currently\nwe only have \"before-eval\" and \"after-eval\" as options.  Both may be defined.  \n\n - **after-eval**  - $result will be true/false -- has access to same variables as query.\n - **before-eval** - any modifications to variables will be evaluated by the query evaluator.\n\n```tcl\n# These allow us to define out subscription modifiers \n# conditions { name changed } - otherwise a single argument would be an error.\nstate modifier register set {}\nstate modifier register removed {}\nstate modifier register is {}\n\n# Change $value to $prev\n# conditions { name was != | foo }\nstate modifier register was {\n\tbefore-eval { set value $prev }\n}\n\n# conditions { name not match | *foo }\nstate modifier register not {\n\tafter-eval { set result [ string is false -strict $result ] }\n}\n\n# Chained Example\n# conditions { name was not match | *foo }\n\n```\n\n### Custom Middlewares\n\nMiddlewares are simple to build and allow you to hook into the lifecycle of \nthe state which defines requests the given middleware.\n\n\n#### Middleware Mixins\n\nWe start by providing a middleware that can be used by any registered state.  This \nprocess will define methods that we wish to mixin to the various steps within our \nstate.  \n\nYou may define some or no mixins depending on its use.\n\n - **api**       - [state $method $args $body]\n - **container** - Generally api will call the container which can be retrieved by \n                   calling [my ref $localID]\n - **entry**     - An entry for our keyed state (and singleton which only has one)\n - **item**      - Each items container holding an index of current/prev values\n\n```tcl\nstate middleware provide myMiddleware [namespace current]::MyMiddlewareClass {\n  ...middlewareConfig\n} {\n  api {\n    method myMiddleware { localID args } {\n    \n    }\n  }\n  container {\n    method myMiddleware { subscription } {\n    \n    }\n  }\n  entry {\n    method ...\n  }\n  item {\n    method ...\n  }\n}\n```\n\n\u003e The middlewareConfig is meant to assist in bringing values from the local context or \n\u003e namespace into the instance when created.  It is not meant to be used to configure \n\u003e by the end-user / while register your scripts.  \n\n#### Middleware Object\n\nWhen we registered our middleware, we provided a callback to a TclOO class which will \nbe built and added to our State when requested.  The class will be built into the \nstates namespace and will have access to the various commands and options to assist \nwith its needs.\n\nDuring registration, our state will check to see which lifecycle hooks have been \ndefined by the middleware and will call the methods when appropriate.\n\nBelow we see a class built using the meta class `class@` which is used to help us\nevaluate within both the namespace and context of our state / defintition.\n\n```tcl\nclass@ create MyMiddlewareClass {\n\n  variable CONTAINER CONFIG\n  \n  constructor { container stateConfig middlewareConfig } {\n    set CONTAINER $container\n    set CONFIG    $stateConfig\n  }\n\n  destructor {\n    \n  }\n  \n  # onSnapshot is called by the middleware processor whenever a new snapshot is \n  # available to parse.\n  method onSnapshot { snapshot } {\n  \n  }\n  \n  # When a key is created on the state\n  method onCreated { snapshot } {\n   \n  }\n  \n  # When a key is removed from the state\n  method onRemoved { snapshot } {\n    \n  }\n  \n  # When an error occurs anywhere during evaluation\n  method onError { result options } {\n    \n  }\n\t\n}\n```# tcl-state-manager\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdash-os%2Ftcl-state-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdash-os%2Ftcl-state-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdash-os%2Ftcl-state-manager/lists"}