{"id":18924647,"url":"https://github.com/dash-os/tcl-sagas","last_synced_at":"2026-03-19T07:08:55.791Z","repository":{"id":71821459,"uuid":"85559325","full_name":"Dash-OS/tcl-sagas","owner":"Dash-OS","description":"Tcl Sagas - providing a powerful cooperative multi-tasking facility to Tcl Programmers.","archived":false,"fork":false,"pushed_at":"2017-05-01T07:38:13.000Z","size":32,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-06-08T12:05:00.528Z","etag":null,"topics":["asynchronous","fibers","multitasking","promises","sagas","tcl"],"latest_commit_sha":null,"homepage":null,"language":"Tcl","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/Dash-OS.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-03-20T09:41:51.000Z","updated_at":"2022-05-18T06:06:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"e3c03890-265b-4ff5-a2e1-76ee24fff1c4","html_url":"https://github.com/Dash-OS/tcl-sagas","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Dash-OS/tcl-sagas","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-sagas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-sagas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-sagas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-sagas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Dash-OS","download_url":"https://codeload.github.com/Dash-OS/tcl-sagas/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dash-OS%2Ftcl-sagas/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271454825,"owners_count":24762698,"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-08-21T02:00:08.990Z","response_time":74,"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":["asynchronous","fibers","multitasking","promises","sagas","tcl"],"created_at":"2024-11-08T11:07:39.599Z","updated_at":"2026-02-10T06:32:26.013Z","avatar_url":"https://github.com/Dash-OS.png","language":"Tcl","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tcl Sagas \n\n\u003e **Note:** This is a work in progress!  However, it should be to the point that is is generally useable.  \n\u003e We are currently in the process of testing various use-cases to make sure there are not any unforeseen issues \n\u003e with the API. \n\n## Summary\n\nWhat Sagas are, or what they look like, has been met with some confusion if you search \nthe internet.  At the end of the day, my interpretation of what a Saga looks like is \nreally quite simple:\n\n\u003e Saga's provide a means for intelligently handling long-lived (generally) asynchronous \n\u003e transactions.  Specifically making it easy for each step to \"clean-up\" after itself \n\u003e should a cancellation or undesired result occur later in the transaction.\n\nI'm sure I will need to read, revise, and rewrite that to be more clear at some point, \nbut lets take a look at what tcl-saga looks like and hopefully it will become clear \nquickly how this pattern can benefit you. \n\n## Saga Globals\n\nSaga provides a few commands which can be called globally (from outside a saga's context) \nto work with and manipulate sagas.\n\n - `saga run`\n - `saga dispatch`\n - `saga take`\n - `saga broadcast`\n - `saga cancel`\n - `saga exists`\n\n## Saga Effects\n\nAs a starting point for the documentation, [saga] defines the following \"effects\" which \nare available to any [saga] context.\n\n - `saga fork`\n - `saga spawn`\n - `saga call`\n - `saga sleep`\n - `saga await`\n - `saga after`\n - `saga self`\n - `saga pool`\n - `saga cancel`\n - `saga resolve`\n - `saga dispatch`\n - `saga take`\n - `saga race`\n - `saga vwait`\n - `saga variable`\n - `saga upvar`\n - `saga uplevel`\n - `saga level`\n - `saga parent`\n - `saga eval`\n\n## Throttle Request Example\n\n```tcl\n# Take at most 1 request per second\nsaga run {\n  while 1 {\n    # wait here until we receive a REQUEST dispatch\n    take REQUEST {\n      # This occurs in its own forked context\n      puts \"Trigger $REQUEST\"\n    }\n    # sleep for 1  second, any dispatched messages will be ignored in the meantime\n    saga sleep 1000\n  }\n}\n```\n\n## Saga Pool Example\n\nBelow we have a slightly more advanced example where we are utilizing some of sagas \nmore powerful features such as [saga uplevel], [saga pool], [saga dispatch], and friends.\n\nWith [saga pool], we take a list of dicts which represent the arguments. Each element \nwill create an independent fork of the worker.  If a result handler is provided it will \nthen be called once all workers in the pool have resolved their results.\n\n[saga] is meant to be a toolkit for handling these kinds of asynchronous data flows without \nthe complexity they may normally require. \n \n```tcl\npackage require saga\n\nputs \"[clock microseconds] | Running the Saga!\"\n\nsaga run HTTP {\n  \n  puts \"[clock microseconds] | Saga Starts Evaluation\"\n  \n  # Create our default arguments which can be retrieved from our asynchronous\n  # workers as-needed.\n  set DefaultOptions [dict create \\\n    timeout 10000\n  ]\n  \n  # Our RequestHandler is the body that we will fork when the pool is called. \n  # Each element in the list will be simultaneously forked and we will await \n  # the results from all before continuing.\n  set RequestHandler {\n    try {\n      puts \"[clock microseconds] | $pool_id-$pool_n | Request Handler Starts\"\n      if { ! [info exists url] } {\n        saga resolve error \"No URL Provided\"\n      } else {\n        # We are able to handle the HTTP Request similarly to how we would if\n        # we were doing so synchronously.\n        set token [::http::geturl $url -command [saga self] -timeout $timeout]\n        # Pause and wait for the http request to complete (or timeout)\n        saga await\n        puts \"[clock microseconds] | $pool_id-$pool_n | Request Handler - Response Received\"\n        set status [::http::status $token]\n        set ncode  [::http::ncode  $token]\n        # For the example we will just capture the status\n        saga resolve $status $ncode {Data Would Be Here}\n      }\n    } trap cancel { reason } {\n      saga resolve cancelled $reason\n    } on error { result options } {\n      saga resolve error $result $options\n    } finally {\n      if { [info exists token] } {\n        ::http::cleanup $token\n      }\n    }\n  }\n  \n  # Our ResultHandler will receive the aggregated results once all the members of \n  # the given pool have resolved their responses.\n  set ResultHandler {\n    if { [dict exists $options callback] } {\n      after 0 [list [dict get $options callback] $results $options] \n    }\n  }\n  \n  set cancelled 0\n  \n  # Our while loop will allow us to continuously take REQUEST events then generate\n  # the pooled results from there.\n  while { ! $cancelled } {\n    try {\n      saga take REQUEST {\n        # Each time a REQUEST is dispatched, we will receive the arguments \n        # in the $REQUEST variable.  In this case we expect two arguments where \n        # the first are options for the pool (which replace the $RequestContext) \n        # and the second is a list of requests to make (each in their own saga).\n        lassign $REQUEST options requests\n        \n        # There are a few ways we can get the variables from our parent context. \n        # Below we are using [saga uplevel] to evaluate and grab them.  We could\n        # also use [saga variable] or [saga upvar].\n        set request_args [ lassign [saga uplevel {\n          list $DefaultOptions $RequestHandler $ResultHandler\n        }] default_options ]\n        \n        # Now we are ready to start our pool.  We merge the request options with the\n        # default then feed in the rest of the arguments to start the pool.  \n        saga pool $requests [dict merge $default_options $options] {*}$request_args\n        \n        # We are free to do whatever else, [saga pool] is handled asynchronously\n        # so this will occur immediately after the command above.\n      }\n      \n      # After each request is received we want to again take REQUEST so that \n      # we can continuously handle requests.\n      \n    } trap cancel { reason } {\n      # trap cancel allows us to conduct logic should we get cancelled for any\n      # reason.  In this case we will simply graciously exit the while loop.\n      set cancelled 1\n    } on error {result options} {\n      puts \"Error! $result\"\n    }\n  }\n\n}\n\n# Our results proc will be called by the saga whenever results are made availble.\n# We define this through the \"options\" dict's callback key.\nproc results {results options} {\n  puts \"------------------------------------------------\"\n  puts \"[clock microseconds] | Results Callback Received\"\n  puts \"------------------------------------------------\"\n  puts \"Results: $results\"\n  puts \"Options: $options\"\n  puts \"------------------------------------------------\"\n}\n\n# With [saga pool], each element should be a dict where the keys will become\n# variables within the forked context.  For each dict that is received, a \n# forked saga will be created.  Our result handler will be called once \n# all requests have completed and are available.  \n#\n# We can setup multiple forks as we are continually servicing the [saga take] \n# in a loop until we are explicitly cancelled.\n\nputs \"[clock microseconds] | Starting Request Dispatches\"\n\nsaga dispatch HTTP REQUEST [dict create \\\n  timeout 5000 \\\n  callback results\n] [list \\\n  [dict create url http://www.google.com] \\\n  [dict create url http://www.bing.com]\n]\n  \nsaga dispatch HTTP REQUEST [dict create callback results] [list \\\n  [dict create url http://www.yahoo.com] \\\n  [dict create url http://www.bing.com]\n]\n\n```\n\nSo now lets look at an example output.  In this example it would appear we had \nsome network latency with the first request.  This ends up showing what pool is \ndoing for us since it waits for the results of the cluster before reporting to \nthe callback.\n\nWe could, of course, decide we would rather receive responses as quickly as\npossible in which case we simply would call the callback from each pool script and\nnot define a ResultHandler at all.  In this case we do not aggregate the results \nat all.\n\n\u003e Timestamps are seen in `[clock microseconds]`.\n\n```tcl\n###### Example Output\n#\n# 1490053630766088 | Running the Saga!\n# 1490053630767397 | Starting Request Dispatches\n# 1490053630767902 | Saga Starts Evaluation\n# 1490053630770704 | 1_2_pool4-1 | Request Handler Starts\n# 1490053630775409 | 1_2_pool4-2 | Request Handler Starts\n# 1490053630777093 | 1_3_pool7-1 | Request Handler Starts\n# 1490053630778764 | 1_3_pool7-2 | Request Handler Starts\n# 1490053630834844 | 1_2_pool4-1 | Request Handler - Response Received\n# 1490053630844328 | 1_3_pool7-1 | Request Handler - Response Received\n# 1490053630856961 | 1_3_pool7-2 | Request Handler - Response Received\n# ------------------------------------------------\n# 1490053630858315 | Results Callback Received\n# ------------------------------------------------\n# Results: 1 {result {ok 301 {Data Would Be Here}} args {url http://www.yahoo.com}} \n#          2 {result {ok 200 {Data Would Be Here}} args {url http://www.bing.com}}\n# Options: timeout 10000 callback results\n# ------------------------------------------------\n# 1490053630860929 | 1_2_pool4-2 | Request Handler - Response Received\n# ------------------------------------------------\n# 1490053630862089 | Results Callback Received\n# ------------------------------------------------\n# Results: 1 {result {ok 200 {Data Would Be Here}} args {url http://www.google.com}} \n#          2 {result {ok 200 {Data Would Be Here}} args {url http://www.bing.com}}\n# Options: timeout 5000 callback results\n# ------------------------------------------------\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdash-os%2Ftcl-sagas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdash-os%2Ftcl-sagas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdash-os%2Ftcl-sagas/lists"}