{"id":32650753,"url":"https://github.com/ahbruns/make-idempotent","last_synced_at":"2026-06-20T21:31:57.494Z","repository":{"id":44330108,"uuid":"512064679","full_name":"AHBruns/make-idempotent","owner":"AHBruns","description":"A small utility library to generalize the process of combining a non-idempotent request and an idempotent query to create an idempotent request.","archived":false,"fork":false,"pushed_at":"2022-07-09T02:50:15.000Z","size":10,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-13T07:20:06.922Z","etag":null,"topics":["idempotency","requests","ruby-gem"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/make_idempotent","language":"Ruby","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/AHBruns.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}},"created_at":"2022-07-09T01:20:17.000Z","updated_at":"2022-07-09T02:27:28.000Z","dependencies_parsed_at":"2022-08-30T05:41:32.194Z","dependency_job_id":null,"html_url":"https://github.com/AHBruns/make-idempotent","commit_stats":null,"previous_names":["ahbruns/make_idempotent"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/AHBruns/make-idempotent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AHBruns%2Fmake-idempotent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AHBruns%2Fmake-idempotent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AHBruns%2Fmake-idempotent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AHBruns%2Fmake-idempotent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AHBruns","download_url":"https://codeload.github.com/AHBruns/make-idempotent/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AHBruns%2Fmake-idempotent/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34586666,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"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":["idempotency","requests","ruby-gem"],"created_at":"2025-10-31T07:54:13.511Z","updated_at":"2026-06-20T21:31:57.477Z","avatar_url":"https://github.com/AHBruns.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Make-idempotent\n\nThis is a small utility library to generalize the process of combining a non-idempotent request and an idempotent query to create an idempotent request.\n\n# Installation\n\nTo install the latest version of this gem run `bundle install make_idempotent` or the equivalent in your ruby gem manager of choice.\n\n# Usage\n\nEasiest way to grok how this works is to read the tests. Here's a good one:\n\n```ruby\nserver_datastore = { ids: Set[], data: [] }\nserver_api = {\n  mutate: Proc.new do |id, data|\n    server_datastore[:ids].add(id)\n    server_datastore[:data].append(data)\n  end,\n  query: Proc.new { |id, _data| server_datastore[:ids].member?(id) }\n}\n\nrequests_datastore = Set[]\nrequest_sender = MakeIdempotent::RequestSender.new(\n  send_request: Proc.new do |id, data|\n    # only 25% request gets to the server\n    raise MakeIdempotent::InconclusiveRequestError unless rand() \u003e 0.75\n\n    result = server_api[:mutate].call(id, data)\n\n    # only 0.01% of responses get back\n    raise MakeIdempotent::InconclusiveRequestError unless rand() \u003e 0.9999\n\n    result\n  end,\n  check_if_request_received: Proc.new do |id, data|\n    # only 25% request gets to the server\n    raise MakeIdempotent::InconclusiveRequestError unless rand() \u003e 0.75\n\n    result = server_api[:query].call(id)\n\n    # only 0.01% of responses get back\n    raise MakeIdempotent::InconclusiveRequestError unless rand() \u003e 0.9999\n\n    result\n  end,\n  store: Proc.new do |id, data|\n    if (requests_datastore.member?(id))\n      raise MakeIdempotent::RequestAlreadySendingError\n    end\n    requests_datastore.add(id)\n  end,\n  unstore: Proc.new { |id, data| requests_datastore.delete(id) }\n)\n\nwhile true\n  begin\n    request_sender.send_request([\"idempotency key\", \"data\"])\n    break\n  rescue =\u003e exception\n    break if exception.is_a?(MakeIdempotent::RequestAlreadySentError)\n    next if exception.is_a?(MakeIdempotent::InconclusiveRequestError)\n    raise exception\n  end\nend\n\nexpect(server_datastore[:data]).to eq([\"data\"])\n```\n\nThis is the general usecase, but often time you'll want to use the same store and unstore methods across many or all your requests. When this is the case, you can use the following:\n\n```ruby\nrequest_sender = MakeIdempotent::RequestSenderFactory.new(\n  store: your_store_implementation,\n  unstore: your_unstore_implementation\n)\nrequest_sender.send_request(...)\n```\n\n# The contract\n\nWhile the API is simple, the implementer does need to ensure their implementation meets some basic requirements. Here's the contract.\n\n- `store` must persist the request_description it is passed to a datastore before returning. It must be the same datastore that unstore deletes from. If the request_definition already exists in the data store, it must throw `MakeIdempotent::RequestAlreadySendingError`.\n- `unstore` must handle unstoring requests that don't exist in its store. It must treat them as successful.\n- `send_request` must throw `MakeIdempotent::InconclusiveRequestError` if and only if it is unclear whether the request was processed by the receiver. In most (all?) cases this will be a request timeout.\n- `check_if_request_received` must return true if the request has been received, and false if not. It must also be idempotent.\n- You may only call `send_request` with the same request_description once at a time. Basically, don't let more than one request go at once. Obviously, this isn't possible if you don't know if the previous request failed. For example, `MakeIdempotent::InconclusiveRequestError` is thrown by a request, or the process crashed in the middle of sending a previous request. In these situations, this library only gives a best effort idempotency guarantee due to the possibility of network race conditions. Though the likelyhood of idempotency goes up as the time between requests increases, it never reaches 100%.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahbruns%2Fmake-idempotent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fahbruns%2Fmake-idempotent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahbruns%2Fmake-idempotent/lists"}