[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐞 Bug Report\ndescription: Create a report to help us improve.\nbody:\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Welcome\n      options:\n        - label: Yes, I've searched similar issues on GitHub and didn't find any.\n          required: true\n        - label: Yes, I've included all information below (version, config, etc).\n          required: true\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: Description of the problem\n      placeholder: Your problem description\n    validations:\n      required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: Version of Valkeyrie\n    validations:\n      required: true\n\n  - type: textarea\n    id: go-env\n    attributes:\n      label: Go environment\n      value: |-\n        <details>\n\n        ```console\n        $ go version && go env\n        # paste output here\n        ```\n\n        </details>\n    validations:\n      required: true\n\n  - type: textarea\n    id: code-example\n    attributes:\n      label: Code example or link to a public repository\n      value: |-\n        <details>\n\n        ```go\n        // add your code here\n        ```\n\n        </details>\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: ❓ Questions\n    url: https://github.com/kvtools/valkeyrie/discussions\n    about: If you have a question, or are looking for advice, please post on our Discussions forum!\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 💡 Feature request\ndescription: \"Suggest an idea for this project.\"\nbody:\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: Your feature request related to a problem? Please describe.\n      placeholder: \"A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: Describe the solution you'd like.\n      placeholder: \"A clear and concise description of what you want to happen.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Describe alternatives you've considered.\n      placeholder: \"A clear and concise description of any alternative solutions or features you've considered.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional context.\n      placeholder: \"Add any other context or screenshots about the feature request here.\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new_store.yml",
    "content": "name: 🧩 Add a new type of store\ndescription: \"Proposal\"\nbody:\n\n  - type: input\n    id: store-name\n    attributes:\n      label: The name of the store.\n    validations:\n      required: true\n\n  - type: input\n    id: repo-url\n    attributes:\n      label: The URL of your repository.\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Provide information about the store.\n      placeholder: \"A clear and concise description.\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: gomod\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    cooldown:\n      default-days: 7\n    groups:\n      go:\n        patterns:\n          - \"*\"  # Group all updates into a single larger pull request.\n\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    cooldown:\n      default-days: 7\n    groups:\n      github-actions:\n        patterns:\n          - \"*\"  # Group all updates into a single larger pull request.\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and test\n\non: [push, pull_request]\n\nenv:\n  GOLANGCI_LINT_VERSION: v2.11\n\npermissions: {}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go-version: [stable, oldstable]\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Download and check dependencies\n        run: |\n          go mod tidy --diff\n\n      - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: ${{ env.GOLANGCI_LINT_VERSION }}\n\n      - name: Run tests\n        run: make test\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.DS_Store\n*.iml\ndump.rdb\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nformatters:\n  enable:\n    - gci\n    - gofumpt\n  settings:\n    gofumpt:\n      extra-rules: false\n\nlinters:\n  default: none\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - contextcheck\n    - depguard\n    - dogsled\n    - durationcheck\n    - err113\n    - errcheck\n    - errname\n    - errorlint\n    - forbidigo\n    - forcetypeassert\n    - funlen\n    - gochecknoglobals\n    - gochecknoinits\n    - gocognit\n    - goconst\n    - gocritic\n    - gocyclo\n    - godot\n    - godox\n    - goheader\n    - gomoddirectives\n    - gomodguard\n    - goprintffuncname\n    - gosec\n    - govet\n    - importas\n    - ineffassign\n    - makezero\n    - misspell\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - predeclared\n    - promlinter\n    - revive\n    - staticcheck\n    - tagliatelle\n    - thelper\n    - unconvert\n    - unparam\n    - unused\n    - usestdlibvars\n    - wastedassign\n\n  settings:\n    depguard:\n      rules:\n        main:\n          deny:\n            - pkg: github.com/instana/testify\n              desc: not allowed\n            - pkg: github.com/sirupsen/logrus\n              desc: not allowed\n            - pkg: github.com/pkg/errors\n              desc: Should be replaced by standard lib errors package\n    funlen:\n      lines: -1\n      statements: 45\n    goconst:\n      min-len: 5\n      min-occurrences: 3\n    gocritic:\n      disabled-checks:\n        - unnamedResult\n        - sloppyReassign\n        - rangeValCopy\n        - octalLiteral\n        - paramTypeCombine # already handle by gofumpt.extra-rules\n      enabled-tags:\n        - diagnostic\n        - style\n        - performance\n      settings:\n        hugeParam:\n          sizeThreshold: 100\n    gocyclo:\n      min-complexity: 15\n    godox:\n      keywords:\n        - FIXME\n    govet:\n      disable:\n        - fieldalignment\n      enable-all: true\n    misspell:\n      locale: US\n    staticcheck:\n      checks:\n        - all\n\n  exclusions:\n    warn-unused: true\n    rules:\n      - linters:\n          - funlen\n          - goconst\n        path: (.+)_test.go\n      - linters:\n          - gochecknoglobals\n        path: valkeyrie.go\n\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nAll contributions are useful, whether it is a simple typo, a more complex change, or just pointing out an issue.\nWe welcome any contribution so feel free to take part in the discussions.\nIf you want to contribute to the project, please make sure to review this document carefully.\n\n- [Setting up the environment](#working-environment)\n- [Before submitting a change](#before-submitting-a-change)\n- [Your first pull request](#your-first-pull-request)\n\n## Working Environment\n\n### Prerequisites\n\n- Git\n- Golang\n- One or all of the supported datastore (Zookeeper / Consul / Etcd / Redis / BoltDB)\n\n### Installing Golang\n\nInstall golang using your favorite package manager on Linux or with the archive\nfollowing these [Guidelines](https://golang.org/doc/install).\n\nAn easy way to get started on macOS is to use [homebrew](https://brew.sh) and type\n`brew install go` in a shell.\n\n### Local testing of key/value stores\n\nIn addition to installing golang, you will need to install some or all of the key value stores for testing.\n\nRefer to each of these stores documentation in order to proceed with installation.\nGenerally, the tests are using the **default configuration** with the **default port** to connect to a store and run the test suite.\n\nTo test a change, you can run the test suite with the following command:\n\n```bash\nmake test\n```\n\n## Before submitting a change\n\nMake sure you check each of these items before you submit a pull request to avoid many unnecessary back and forth in GitHub comments\n(and will help us review and include the change as soon as possible):\n\n- **Open an issue** to clearly state the problem.\nThis will be helpful to keep track of what needs to be fixed. This also helps sort and prioritize issues.\n\n- **Run the following command**: `make validate`, to ensure that your code is properly formatted.\n\n- **For non-trivial changes, write a test**: this is to ensure that we don't encounter any regression in the future.\n\n- **Write a complete description** for your pull request (avoid using `-m` flag when committing a change unless it is a trivial one).\n\n- **Sign-off your commits** using the `-s` flag (you can configure an alias to `git commit` adding `-s` for convenience).\n\n- **Squash your commits** if the pull requests includes many commits that are related.\nThis is to maintain a clean history of the change and better identify faulty commits if reverting a change is ever needed.\nWe will tell you if squashing your commits is necessary.\n\n- **If the change is solving one or more issues listed on the repository**:\nyou can reference the issue in your comment with `closes #XXX` or `fixes #XXX`.\nThis will automatically close the related issues on merging the change.\nSee [GitHub documentation](https://help.github.com/articles/closing-issues-using-keywords/) for more details.\n\nFinally, submit your *Pull Request*.\n\n## Your first Pull Request\n\nYou made it to your first Pull Request? It's only the start of the process.\nFollowing steps may include a discussion on the design and tradeoffs of your proposed solution.\n\nAdditionally, there will be a *code review process* to find out potential bugs.\nPart of being a helpful community is to make sure we point out improvements\nand deliver actionable items to work towards fixing potential issues.\nFeel free to ask questions if you are stuck, so we can help you.\n\n*Don't be discouraged* if your change happens not to be included.\nAll contributions are helpful in a way.\nYour PR most certainly made the discussion go forward in many aspects\nand helped to work towards our common goal of making the project better for everyone.\n\n**Welcome!**\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright 2014-2016 Docker, Inc.\n   Copyright 2021-2022 Valkeyrie authors\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: all\nall: validate test\n\n## Run validates\n.PHONY: validate\nvalidate:\n\tgolangci-lint run\n\n## Run tests\n.PHONY: test\ntest:\n\tgo test -v -race ./...\n"
  },
  {
    "path": "docs/examples.md",
    "content": "# Examples\n\nThis document contains useful example of usage for `valkeyrie`.\nIt might not be complete but provides with general information on how to use the client.\n\n## Create a Store and Use `Put`/`Get`/`Delete`\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/kvtools/consul\"\n\t\"github.com/kvtools/valkeyrie\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\tconfig := &consul.Config{\n\t\tConnectionTimeout: 10 * time.Second,\n\t}\n\n\tkv, err := valkeyrie.NewStore(ctx, consul.StoreName, []string{\"localhost:8500\"}, config)\n\tif err != nil {\n\t\tlog.Fatal(\"Cannot create store consul\")\n\t}\n\n\tkey := \"foo\"\n\n\terr = kv.Put(ctx, key, []byte(\"bar\"), nil)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error trying to put value at key: %v\", key)\n\t}\n\n\tpair, err := kv.Get(ctx, key, nil)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error trying accessing value at key: %v\", key)\n\t}\n\n\terr = kv.Delete(ctx, key)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error trying to delete key %v\", key)\n\t}\n\n\tlog.Printf(\"value: %s\", string(pair.Value))\n}\n```\n\n## List Keys\n\n```go\n// List will list all the keys under `key` if it contains a set of child keys/values\nentries, err := kv.List(ctx, key, nil)\nfor _, pair := range entries {\n    fmt.Printf(\"key=%v - value=%v\", pair.Key, string(pair.Value))\n}\n```\n\n## Watching for Events on a Single Key (`Watch`)\n\nYou can use watches to watch modifications on a key.\nFirst you need to check if the key exists.\nIf this is not the case, we need to create it using the `Put` function.\n\n```go\npackage example\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n)\n\nfunc watch(ctx context.Context, kv store.Store, key string) error {\n\t// Checking on the key before watching\n\texists, _ := kv.Exists(ctx, key, nil)\n\tif !exists {\n\t\terr := kv.Put(ctx, key, []byte(\"bar\"), nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"something went wrong when initializing key %v\", key)\n\t\t}\n\t}\n\n\tevents, err := kv.Watch(ctx, key, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase pair := <-events:\n\t\t\t// Do something with events\n\t\t\tfmt.Printf(\"value changed on key %v: new value=%v\", key, pair.Value)\n\t\t}\n\t}\n\t// ...\n\n\treturn nil\n}\n```\n\n## Watching for Events Happening on Child Keys (`WatchTree`)\n\nYou can use watches to watch modifications on a key.\nFirst you need to check if the key exists.\nIf this is not the case, we need to create it using the `Put` function.\n\nThere is a special step here if you are using etcd **APIv2** and if you want your code to work across backends.\n`etcd` with **APIv2** makes the distinction between directories and keys,\nwe need to make sure that the created key is considered as a directory by enforcing `IsDir` at `true`.\n\n```go\npackage example\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n)\n\nfunc watchTree(ctx context.Context, kv store.Store, key string) error {\n\t// Checking on the key before watching\n\texists, _ := kv.Exists(ctx, key, nil)\n\tif !exists {\n\t\t// Do not forget `IsDir:true` if you are using etcd APIv2\n\t\terr := kv.Put(ctx, key, []byte(\"bar\"), &store.WriteOptions{IsDir: true})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"something went wrong when initializing key %v\", key)\n\t\t}\n\t}\n\n\tevents, err := kv.WatchTree(ctx, key, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tselect {\n\tcase pairs := <-events:\n\t\t// Do something with events\n\t\tfor _, pair := range pairs {\n\t\t\tfmt.Printf(\"value changed on key %v: new value=%v\", key, pair.Value)\n\t\t}\n\t}\n\n\t// ...\n\n\treturn nil\n}\n```\n\n## Distributed Locking, Using Lock/Unlock\n\n```go\npackage example\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n)\n\nfunc foo(ctx context.Context, kv store.Store) error {\n\tkey := \"lockKey\"\n\tvalue := []byte(\"bar\")\n\n\t// Initialize a distributed lock. TTL is optional, it is here to make sure that\n\t// the lock is released after the program that is holding the lock ends or crashes\n\tlock, err := kv.NewLock(key, &store.LockOptions{Value: value, TTL: 2 * time.Second})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"something went wrong when trying to initialize the Lock\")\n\t}\n\n\t// Try to lock the key, the call to Lock() is blocking\n\t_, err = lock.Lock(nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"something went wrong when trying to lock key %v\", key)\n\t}\n\n\t// Get should work because we are holding the key\n\tpair, err := kv.Get(ctx, key, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"key %v has value %v\", key, pair.Value)\n\t}\n\n\t// Unlock the key\n\terr = lock.Unlock(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"something went wrong when trying to unlock key %v\", key)\n\t}\n\n\t// ...\n\n\treturn nil\n}\n```\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/kvtools/valkeyrie\n\ngo 1.22\n\nrequire github.com/stretchr/testify v1.11.1\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "maintainers.md",
    "content": "- [Alexandre Beslic](https://github.com/abronan)\n- [Victor Castell](https://github.com/victorcoder)\n- [Nicolas Mengin](https://github.com/nmengin)\n- [Maciej Winnicki](https://github.com/mthenw)\n- [Ludovic Fernandez](https://github.com/ldez)\n- [Kevin Pollet](https://github.com/kevinpollet)\n- [Tom Moulard](https://github.com/tomMoulard)\n"
  },
  {
    "path": "mock_test.go",
    "content": "package valkeyrie\n\nimport (\n\t\"context\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n)\n\nconst testStoreName = \"mock\"\n\nfunc newStore(ctx context.Context, endpoints []string, options Config) (store.Store, error) {\n\tcfg, ok := options.(*Config)\n\tif !ok && options != nil {\n\t\treturn nil, &store.InvalidConfigurationError{Store: testStoreName, Config: options}\n\t}\n\n\treturn New(ctx, endpoints, cfg)\n}\n\ntype Mock struct {\n\tcfg *Config\n}\n\n// New creates a new Mock client.\n//\n//nolint:gocritic\nfunc New(_ context.Context, _ []string, cfg *Config) (*Mock, error) {\n\treturn &Mock{cfg: cfg}, nil\n}\n\nfunc (m Mock) Put(_ context.Context, _ string, _ []byte, _ *store.WriteOptions) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) Get(_ context.Context, _ string, _ *store.ReadOptions) (*store.KVPair, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) Delete(_ context.Context, _ string) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) Exists(_ context.Context, _ string, _ *store.ReadOptions) (bool, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) Watch(_ context.Context, _ string, _ *store.ReadOptions) (<-chan *store.KVPair, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) WatchTree(_ context.Context, _ string, _ *store.ReadOptions) (<-chan []*store.KVPair, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) NewLock(_ context.Context, _ string, _ *store.LockOptions) (store.Locker, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) List(_ context.Context, _ string, _ *store.ReadOptions) ([]*store.KVPair, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) DeleteTree(_ context.Context, _ string) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) AtomicPut(_ context.Context, _ string, _ []byte, _ *store.KVPair, _ *store.WriteOptions) (bool, *store.KVPair, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) AtomicDelete(_ context.Context, _ string, _ *store.KVPair) (bool, error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m Mock) Close() error {\n\tpanic(\"implement me\")\n}\n"
  },
  {
    "path": "readme.md",
    "content": "<p align=\"center\">\n  <img alt=\"golangci-lint logo\" src=\"docs/valkeyrie.png\" height=\"350\" />\n  <h3 align=\"center\">Valkeyrie</h3>\n  <p align=\"center\">Distributed Key/Value Store Abstraction Library</p>\n</p>\n\n# Valkeyrie\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/kvtools/valkeyrie.svg)](https://pkg.go.dev/github.com/kvtools/valkeyrie)\n[![Build and test](https://github.com/kvtools/valkeyrie/actions/workflows/build.yml/badge.svg)](https://github.com/kvtools/valkeyrie/actions/workflows/build.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/kvtools/valkeyrie)](https://goreportcard.com/report/github.com/kvtools/valkeyrie)\n\n`valkeyrie` provides a Go native library to store metadata using Distributed Key/Value stores (or common databases).\n\nIts goal is to abstract common store operations (`Get`, `Put`, `List`, etc.) for multiple Key/Value store backends.\n\nFor example, you can easily implement a generic *Leader Election* algorithm on top of it (see the [docker/leadership](https://github.com/docker/leadership) repository).\n\nThe benefit of `valkeyrie` is not to duplicate the code for programs that should support multiple distributed Key/Value stores such as `Consul`/`etcd`/`zookeeper`, etc.\n\n## Examples of Usage\n\nYou can refer to [Examples](https://github.com/kvtools/valkeyrie/blob/master/docs/examples.md) for a basic overview of the library.\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/kvtools/consul\"\n\t\"github.com/kvtools/valkeyrie\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\t\n\tconfig := &consul.Config{\n\t\tConnectionTimeout: 10 * time.Second,\n\t}\n\n\tkv, err := valkeyrie.NewStore(ctx, consul.StoreName, []string{\"localhost:8500\"}, config)\n\tif err != nil {\n\t\tlog.Fatal(\"Cannot create store consul\")\n\t}\n\n\tkey := \"foo\"\n\t\n\n\terr = kv.Put(ctx, key, []byte(\"bar\"), nil)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error trying to put value at key: %v\", key)\n\t}\n\n\tpair, err := kv.Get(ctx, key, nil)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error trying accessing value at key: %v\", key)\n\t}\n\n\tlog.Printf(\"value: %s\", string(pair.Value))\n\n\terr = kv.Delete(ctx, key)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error trying to delete key %v\", key)\n\t}\n}\n```\n\n## Compatibility\n\nA **storage backend** in `valkeyrie` implements (fully or partially) the [Store](https://github.com/kvtools/valkeyrie/blob/master/store/store.go#L69) interface.\n\n| Calls                 | Consul | Etcd | Zookeeper | Redis | BoltDB | DynamoDB |\n|-----------------------|:------:|:----:|:---------:|:-----:|:------:|:--------:|\n| Put                   |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| Get                   |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| Delete                |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| Exists                |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| Watch                 |  🟢️   | 🟢️  |    🟢️    |  🟢️  |   🔴   |    🔴    |\n| WatchTree             |  🟢️   | 🟢️  |    🟢️    |  🟢️  |   🔴   |    🔴    |\n| NewLock (Lock/Unlock) |  🟢️   | 🟢️  |    🟢️    |  🟢️  |   🔴   |   🟢️    |\n| List                  |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| DeleteTree            |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| AtomicPut             |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n| AtomicDelete          |  🟢️   | 🟢️  |    🟢️    |  🟢️  |  🟢️   |   🟢️    |\n\nThe store implementations:\n\n- [boltdb](https://github.com/kvtools/boltdb)\n- [consul](https://github.com/kvtools/consul)\n- [dynamodb](https://github.com/kvtools/dynamodb)\n- [etcdv2](https://github.com/kvtools/etcdv2)\n- [etcdv3](https://github.com/kvtools/etcdv3)\n- [redis](https://github.com/kvtools/redis)\n- [zookeeper](https://github.com/kvtools/zookeeper)\n\nThe store template:\n\n- [template](https://github.com/kvtools/template)\n\n## Limitations\n\nDistributed Key/Value stores often have different concepts for managing and formatting keys and their associated values.\nEven though `valkeyrie` tries to abstract those stores aiming for some consistency, in some cases it can't be applied easily.\n\nCalls like `WatchTree` may return different events (or number of events) depending on the backend\n(for now, `Etcd` and `Consul` will likely return more events than `Zookeeper` that you should triage properly).\n\n## Contributing\n\nWant to contribute to `valkeyrie`?\nTake a look at the [Contribution Guidelines](https://github.com/kvtools/valkeyrie/blob/master/CONTRIBUTING.md).\n\nThe [Maintainers](https://github.com/kvtools/valkeyrie/blob/master/maintainers.md).\n\n## Copyright and License\n\nApache License Version 2.0\n\nValkeyrie started as a hard fork of the unmaintained [libkv](https://github.com/docker/libkv).\n"
  },
  {
    "path": "store/errors.go",
    "content": "package store\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\nvar (\n\t// ErrCallNotSupported is thrown when a method is not implemented/supported by the current backend.\n\tErrCallNotSupported = errors.New(\"the current call is not supported with this backend\")\n\t// ErrNotReachable is thrown when the API cannot be reached for issuing common store operations.\n\tErrNotReachable = errors.New(\"api not reachable\")\n\t// ErrCannotLock is thrown when there is an error acquiring a lock on a key.\n\tErrCannotLock = errors.New(\"error acquiring the lock\")\n\t// ErrKeyModified is thrown during an atomic operation if the index does not match the one in the store.\n\tErrKeyModified = errors.New(\"unable to complete atomic operation, key modified\")\n\t// ErrKeyNotFound is thrown when the key is not found in the store during a Get operation.\n\tErrKeyNotFound = errors.New(\"key not found in store\")\n\t// ErrPreviousNotSpecified is thrown when the previous value is not specified for an atomic operation.\n\tErrPreviousNotSpecified = errors.New(\"previous K/V pair should be provided for the Atomic operation\")\n\t// ErrKeyExists is thrown when the previous value exists in the case of an AtomicPut.\n\tErrKeyExists = errors.New(\"previous K/V pair exists, cannot complete Atomic operation\")\n)\n\n// InvalidConfigurationError is thrown when the type of the configuration is not supported by a store.\ntype InvalidConfigurationError struct {\n\tStore  string\n\tConfig any\n}\n\nfunc (e *InvalidConfigurationError) Error() string {\n\treturn fmt.Sprintf(\"%s: invalid configuration type: %T\", e.Store, e.Config)\n}\n\n// UnknownConstructorError is thrown when a requested store is not register.\ntype UnknownConstructorError struct {\n\tStore string\n}\n\nfunc (e UnknownConstructorError) Error() string {\n\treturn fmt.Sprintf(\"unknown constructor %q (forgotten import?)\", e.Store)\n}\n"
  },
  {
    "path": "store/helpers.go",
    "content": "package store\n\nimport (\n\t\"strings\"\n)\n\n// CreateEndpoints creates a list of endpoints given the right scheme.\nfunc CreateEndpoints(addrs []string, scheme string) (entries []string) {\n\tfor _, addr := range addrs {\n\t\tentries = append(entries, scheme+\"://\"+addr)\n\t}\n\n\treturn entries\n}\n\n// SplitKey splits the key to extract path information.\nfunc SplitKey(key string) (path []string) {\n\tif strings.Contains(key, \"/\") {\n\t\treturn strings.Split(key, \"/\")\n\t}\n\n\treturn []string{key}\n}\n"
  },
  {
    "path": "store/store.go",
    "content": "// Package store contains KV store backends.\npackage store\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// Store represents the backend K/V storage.\n// Each store should support every call listed here.\n// Or it couldn't be implemented as a K/V backend for valkeyrie.\ntype Store interface {\n\t// Put a value at the specified key.\n\tPut(ctx context.Context, key string, value []byte, opts *WriteOptions) error\n\n\t// Get a value given its key.\n\tGet(ctx context.Context, key string, opts *ReadOptions) (*KVPair, error)\n\n\t// Delete the value at the specified key.\n\tDelete(ctx context.Context, key string) error\n\n\t// Exists Verify if a Key exists in the store.\n\tExists(ctx context.Context, key string, opts *ReadOptions) (bool, error)\n\n\t// Watch for changes on a key.\n\tWatch(ctx context.Context, key string, opts *ReadOptions) (<-chan *KVPair, error)\n\n\t// WatchTree watches for changes on child nodes under a given directory.\n\tWatchTree(ctx context.Context, directory string, opts *ReadOptions) (<-chan []*KVPair, error)\n\n\t// NewLock creates a lock for a given key.\n\t// The returned Locker is not held and must be acquired with `.Lock`.\n\t// The Value is optional.\n\tNewLock(ctx context.Context, key string, opts *LockOptions) (Locker, error)\n\n\t// List the content of a given prefix.\n\tList(ctx context.Context, directory string, opts *ReadOptions) ([]*KVPair, error)\n\n\t// DeleteTree deletes a range of keys under a given directory.\n\tDeleteTree(ctx context.Context, directory string) error\n\n\t// AtomicPut Atomic CAS operation on a single value.\n\t// Pass previous = nil to create a new key.\n\tAtomicPut(ctx context.Context, key string, value []byte, previous *KVPair, opts *WriteOptions) (bool, *KVPair, error)\n\n\t// AtomicDelete Atomic delete of a single value.\n\tAtomicDelete(ctx context.Context, key string, previous *KVPair) (bool, error)\n\n\t// Close the store connection.\n\tClose() error\n}\n\n// KVPair represents {Key, Value, LastIndex} tuple.\ntype KVPair struct {\n\tKey       string\n\tValue     []byte\n\tLastIndex uint64\n}\n\n// WriteOptions contains optional request parameters.\ntype WriteOptions struct {\n\tIsDir bool\n\tTTL   time.Duration\n\n\t// If true, the client will keep the lease alive in the background\n\t// for stores that are allowing it.\n\tKeepAlive bool\n}\n\n// ReadOptions contains optional request parameters.\ntype ReadOptions struct {\n\t// Consistent defines if the behavior of a Get operation is linearizable or not.\n\t// Linearizability allows us to 'see' objects based on a real-time total order\n\t// as opposed to an arbitrary order or with stale values ('inconsistent' scenario).\n\tConsistent bool\n}\n\n// LockOptions contains optional request parameters.\ntype LockOptions struct {\n\tValue          []byte        // Optional, value to associate with the lock.\n\tTTL            time.Duration // Optional, expiration ttl associated with the lock.\n\tRenewLock      chan struct{} // Optional, chan used to control and stop the session ttl renewal for the lock.\n\tDeleteOnUnlock bool          // If true, the value will be deleted when the lock is unlocked or expires.\n}\n\n// Locker provides locking mechanism on top of the store.\n// Similar to sync.Locker except it may return errors.\ntype Locker interface {\n\tLock(ctx context.Context) (<-chan struct{}, error)\n\tUnlock(ctx context.Context) error\n}\n"
  },
  {
    "path": "testsuite/suite.go",
    "content": "// Package testsuite the valkeyrie tests suite.\npackage testsuite\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testTimeout = 60 * time.Second\n\n// RunTestCommon tests the minimal required APIs which\n// should be supported by all K/V backends.\nfunc RunTestCommon(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\ttestPutGetDeleteExists(t, kv)\n\ttestList(t, kv)\n\ttestDeleteTree(t, kv)\n}\n\n// RunTestListLock tests the list output for mutexes\n// and checks that internal side keys are not listed.\nfunc RunTestListLock(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\ttestListLockKey(t, kv)\n}\n\n// RunTestAtomic tests the Atomic operations by the K/V\n// backends.\nfunc RunTestAtomic(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\ttestAtomicPut(t, kv)\n\ttestAtomicPutCreate(t, kv)\n\ttestAtomicPutWithSlashSuffixKey(t, kv)\n\ttestAtomicDelete(t, kv)\n}\n\n// RunTestWatch tests the watch/monitor APIs supported\n// by the K/V backends.\nfunc RunTestWatch(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\ttestWatch(t, kv)\n\ttestWatchTree(t, kv)\n}\n\n// RunTestLock tests the KV pair Lock/Unlock APIs supported\n// by the K/V backends.\nfunc RunTestLock(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\ttestLockUnlock(t, kv)\n}\n\n// RunTestLockTTL tests the KV pair Lock with TTL APIs supported\n// by the K/V backends.\nfunc RunTestLockTTL(t *testing.T, kv store.Store, backup store.Store) {\n\tt.Helper()\n\n\ttestLockTTL(t, kv, backup)\n}\n\n// RunTestTTL tests the TTL functionality of the K/V backend.\nfunc RunTestTTL(t *testing.T, kv store.Store, backup store.Store) {\n\tt.Helper()\n\n\ttestPutTTL(t, kv, backup)\n}\n\nfunc checkPairNotNil(t *testing.T, pair *store.KVPair) {\n\tt.Helper()\n\n\trequire.NotNilf(t, pair, \"pair is nil\")\n\trequire.NotNilf(t, pair.Value, \"value is nil\")\n}\n\nfunc testPutGetDeleteExists(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Get a not exist key should return ErrKeyNotFound.\n\t_, err := kv.Get(ctx, \"testPutGetDelete_not_exist_key\", nil)\n\tassert.ErrorIs(t, err, store.ErrKeyNotFound)\n\n\tvalue := []byte(\"bar\")\n\tfor _, key := range []string{\n\t\t\"testPutGetDeleteExists\",\n\t\t\"testPutGetDeleteExists/\",\n\t\t\"testPutGetDeleteExists/testbar/\",\n\t\t\"testPutGetDeleteExists/testbar/testfoobar\",\n\t} {\n\t\t// Put the key.\n\t\terr = kv.Put(ctx, key, value, nil)\n\t\trequire.NoError(t, err)\n\n\t\t// Get should return the value and an incremented index.\n\t\tpair, err := kv.Get(ctx, key, nil)\n\t\trequire.NoError(t, err)\n\t\tcheckPairNotNil(t, pair)\n\t\tassert.Equal(t, pair.Value, value)\n\t\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\t\t// Exists should return true.\n\t\texists, err := kv.Exists(ctx, key, nil)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, exists)\n\n\t\t// Delete the key.\n\t\terr = kv.Delete(ctx, key)\n\t\trequire.NoError(t, err)\n\n\t\t// Get should fail.\n\t\tpair, err = kv.Get(ctx, key, nil)\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, pair)\n\n\t\t// Exists should return false.\n\t\texists, err = kv.Exists(ctx, key, nil)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, exists)\n\t}\n}\n\nfunc testWatch(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tkey := \"testWatch\"\n\tvalue := []byte(\"world\")\n\tnewValue := []byte(\"world!\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tt.Cleanup(cancel)\n\n\t// Put the key.\n\terr := kv.Put(ctx, key, value, nil)\n\trequire.NoError(t, err)\n\n\tevents, err := kv.Watch(ctx, key, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, events)\n\n\t// Update loop.\n\tgo func() {\n\t\ttimeout := time.After(1 * time.Second)\n\t\ttick := time.NewTicker(250 * time.Millisecond)\n\t\tdefer tick.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\treturn\n\t\t\tcase <-tick.C:\n\t\t\t\terr := kv.Put(ctx, key, newValue, nil)\n\t\t\t\tif assert.NoError(t, err) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Check for updates.\n\teventCount := 1\n\tfor {\n\t\tselect {\n\t\tcase event := <-events:\n\t\t\tassert.NotNil(t, event)\n\t\t\tassert.Equal(t, event.Key, key)\n\t\t\tif eventCount == 1 {\n\t\t\t\tassert.Equal(t, event.Value, value)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, event.Value, newValue)\n\t\t\t}\n\t\t\teventCount++\n\t\t\t// We received all the events we wanted to check.\n\t\t\tif eventCount >= 4 {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-time.After(4 * time.Second):\n\t\t\tt.Fatal(\"Timeout reached\")\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc testWatchTree(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tdir := \"testWatchTree\"\n\n\tnode1 := \"testWatchTree/node1\"\n\tvalue1 := []byte(\"node1\")\n\n\tnode2 := \"testWatchTree/node2\"\n\tvalue2 := []byte(\"node2\")\n\n\tnode3 := \"testWatchTree/node3\"\n\tvalue3 := []byte(\"node3\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tt.Cleanup(cancel)\n\n\terr := kv.Put(ctx, node1, value1, nil)\n\trequire.NoError(t, err)\n\terr = kv.Put(ctx, node2, value2, nil)\n\trequire.NoError(t, err)\n\terr = kv.Put(ctx, node3, value3, nil)\n\trequire.NoError(t, err)\n\n\tevents, err := kv.WatchTree(ctx, dir, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, events)\n\n\t// Update loop.\n\tgo func() {\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\terr := kv.Delete(ctx, node3)\n\t\trequire.NoError(t, err)\n\t}()\n\n\t// Check for updates.\n\teventCount := 1\n\tfor {\n\t\tselect {\n\t\tcase event := <-events:\n\t\t\tassert.NotNil(t, event)\n\t\t\t// We received the Delete event on a child node\n\t\t\t// Exit test successfully.\n\t\t\tif eventCount == 2 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\teventCount++\n\t\tcase <-time.After(4 * time.Second):\n\t\t\tt.Fatal(\"Timeout reached\")\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc testAtomicPut(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tkey := \"testAtomicPut\"\n\tvalue := []byte(\"world\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Put the key.\n\terr := kv.Put(ctx, key, value, nil)\n\trequire.NoError(t, err)\n\n\t// Get should return the value and an incremented index.\n\tpair, err := kv.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\t// This CAS should fail: previous exists.\n\tsuccess, _, err := kv.AtomicPut(ctx, key, []byte(\"WORLD\"), nil, nil)\n\tassert.Error(t, err)\n\tassert.False(t, success)\n\n\t// This CAS should succeed.\n\tsuccess, _, err = kv.AtomicPut(ctx, key, []byte(\"WORLD\"), pair, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, success)\n\n\t// This CAS should fail, key has wrong index.\n\tpair.LastIndex = 6744\n\tsuccess, _, err = kv.AtomicPut(ctx, key, []byte(\"WORLDWORLD\"), pair, nil)\n\tassert.Equal(t, err, store.ErrKeyModified)\n\tassert.False(t, success)\n}\n\nfunc testAtomicPutCreate(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Use a key in a new directory to ensure Stores will create directories\n\t// that don't yet exist.\n\tkey := \"testAtomicPutCreate/create\"\n\tvalue := []byte(\"putcreate\")\n\n\t// AtomicPut the key, previous = nil indicates create.\n\tsuccess, _, err := kv.AtomicPut(ctx, key, value, nil, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, success)\n\n\t// Get should return the value and an incremented index.\n\tpair, err := kv.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\n\t// Attempting to create again should fail.\n\tsuccess, _, err = kv.AtomicPut(ctx, key, value, nil, nil)\n\tassert.ErrorIs(t, err, store.ErrKeyExists)\n\tassert.False(t, success)\n\n\t// This CAS should succeed, since it has the value from Get().\n\tsuccess, _, err = kv.AtomicPut(ctx, key, []byte(\"PUTCREATE\"), pair, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, success)\n}\n\nfunc testAtomicPutWithSlashSuffixKey(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\tk1 := \"testAtomicPutWithSlashSuffixKey/key/\"\n\tsuccess, _, err := kv.AtomicPut(ctx, k1, []byte{}, nil, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, success)\n}\n\nfunc testAtomicDelete(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tkey := \"testAtomicDelete\"\n\tvalue := []byte(\"world\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Put the key.\n\terr := kv.Put(ctx, key, value, nil)\n\trequire.NoError(t, err)\n\n\t// Get should return the value and an incremented index.\n\tpair, err := kv.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\ttempIndex := pair.LastIndex\n\n\t// AtomicDelete should fail.\n\tpair.LastIndex = 6744\n\tsuccess, err := kv.AtomicDelete(ctx, key, pair)\n\tassert.Error(t, err)\n\tassert.False(t, success)\n\n\t// AtomicDelete should succeed.\n\tpair.LastIndex = tempIndex\n\tsuccess, err = kv.AtomicDelete(ctx, key, pair)\n\trequire.NoError(t, err)\n\tassert.True(t, success)\n\n\t// Delete a non-existent key; should fail.\n\tsuccess, err = kv.AtomicDelete(ctx, key, pair)\n\tassert.ErrorIs(t, err, store.ErrKeyNotFound)\n\tassert.False(t, success)\n}\n\nfunc testLockUnlock(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\tkey := \"testLockUnlock\"\n\tvalue := []byte(\"bar\")\n\n\t// We should be able to create a new lock on key.\n\tlock, err := kv.NewLock(ctx, key, &store.LockOptions{\n\t\tValue:          value,\n\t\tTTL:            2 * time.Second,\n\t\tDeleteOnUnlock: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, lock)\n\n\t// Lock should successfully succeed or block.\n\tlockChan, err := lock.Lock(ctx)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, lockChan)\n\n\t// Get should work.\n\tpair, err := kv.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\t// Unlock should succeed.\n\terr = lock.Unlock(ctx)\n\trequire.NoError(t, err)\n\n\t// Lock should succeed again.\n\tlockChan, err = lock.Lock(ctx)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, lockChan)\n\n\t// Get should work.\n\tpair, err = kv.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\terr = lock.Unlock(ctx)\n\trequire.NoError(t, err)\n}\n\nfunc testLockTTL(t *testing.T, kv store.Store, otherConn store.Store) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\tkey := \"testLockTTL\"\n\tvalue := []byte(\"bar\")\n\n\trenewCh := make(chan struct{})\n\n\t// We should be able to create a new lock on key.\n\tlockOC, err := otherConn.NewLock(ctx, key, &store.LockOptions{\n\t\tValue:     value,\n\t\tTTL:       2 * time.Second,\n\t\tRenewLock: renewCh,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, lockOC)\n\n\t// Lock should successfully succeed.\n\tlockChan, err := lockOC.Lock(ctx)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, lockChan)\n\n\t// Get should work.\n\tpair, err := otherConn.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\ttime.Sleep(3 * time.Second)\n\n\tvalue = []byte(\"foobar\")\n\n\t// Create a new lock with another connection.\n\tlock, err := kv.NewLock(ctx, key, &store.LockOptions{\n\t\tValue: value,\n\t\tTTL:   3 * time.Second,\n\t})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, lock)\n\n\tctxLock, cancelLock := context.WithTimeout(ctx, 4*time.Second)\n\tdefer cancelLock()\n\n\t// Lock should block, the session on the lock\n\t// is still active and renewed periodically.\n\tlockChan, _ = lock.Lock(ctxLock)\n\trequire.Nil(t, lockChan)\n\n\t// Close the connection.\n\t_ = otherConn.Close()\n\n\t// Force to stop the session renewal for the lock.\n\tclose(renewCh)\n\n\t// Let the session on the lock expire.\n\ttime.Sleep(3 * time.Second)\n\n\t// Lock should now succeed for the other client.\n\tlocked := make(chan struct{})\n\tgo func(<-chan struct{}) {\n\t\tlockChan, err = lock.Lock(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, lockChan)\n\t\tlocked <- struct{}{}\n\t}(locked)\n\n\tselect {\n\tcase <-locked:\n\t\tbreak\n\tcase <-time.After(4 * time.Second):\n\t\tt.Fatal(\"Unable to take the lock, timed out\")\n\t}\n\n\t// Get should work with the new value.\n\tpair, err = kv.Get(ctx, key, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, value)\n\tassert.NotEqual(t, pair.LastIndex, 0)\n\n\terr = lock.Unlock(ctx)\n\trequire.NoError(t, err)\n}\n\nfunc testPutTTL(t *testing.T, kv store.Store, otherConn store.Store) {\n\tt.Helper()\n\n\tfirstKey := \"testPutTTL\"\n\tfirstValue := []byte(\"foo\")\n\n\tsecondKey := \"second\"\n\tsecondValue := []byte(\"bar\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Put the first key with the Ephemeral flag.\n\terr := otherConn.Put(ctx, firstKey, firstValue, &store.WriteOptions{TTL: 2 * time.Second})\n\trequire.NoError(t, err)\n\n\t// Put a second key with the Ephemeral flag.\n\terr = otherConn.Put(ctx, secondKey, secondValue, &store.WriteOptions{TTL: 2 * time.Second})\n\trequire.NoError(t, err)\n\n\t// Get on firstKey should work.\n\tpair, err := kv.Get(ctx, firstKey, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\n\t// Get on secondKey should work.\n\tpair, err = kv.Get(ctx, secondKey, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\n\t// Close the connection.\n\t_ = otherConn.Close()\n\n\t// Let the session expire.\n\ttime.Sleep(3 * time.Second)\n\n\t// Get on firstKey shouldn't work.\n\tpair, err = kv.Get(ctx, firstKey, nil)\n\tassert.Error(t, err)\n\tassert.Nil(t, pair)\n\n\t// Get on secondKey shouldn't work.\n\tpair, err = kv.Get(ctx, secondKey, nil)\n\tassert.Error(t, err)\n\tassert.Nil(t, pair)\n}\n\nfunc testList(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tparentKey := \"testList\"\n\tchildKey := \"testList/child\"\n\tsubfolderKey := \"testList/subfolder\"\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Put the parent key.\n\terr := kv.Put(ctx, parentKey, nil, &store.WriteOptions{IsDir: true})\n\trequire.NoError(t, err)\n\n\t// Put the first child key.\n\terr = kv.Put(ctx, childKey, []byte(\"first\"), nil)\n\trequire.NoError(t, err)\n\n\t// Put the second child key which is also a directory.\n\terr = kv.Put(ctx, subfolderKey, []byte(\"second\"), &store.WriteOptions{IsDir: true})\n\trequire.NoError(t, err)\n\n\t// Put child keys under secondKey.\n\tfor i := 1; i <= 3; i++ {\n\t\tkey := \"testList/subfolder/key\" + strconv.Itoa(i)\n\t\terr = kv.Put(ctx, key, []byte(\"value\"), nil)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// List should work and return five child entries.\n\tfor _, parent := range []string{parentKey, parentKey + \"/\"} {\n\t\tpairs, errList := kv.List(ctx, parent, nil)\n\t\trequire.NoError(t, errList)\n\t\tassert.Len(t, pairs, 5)\n\t}\n\n\t// List on childKey should return 0 keys.\n\tpairs, err := kv.List(ctx, childKey, nil)\n\trequire.NoError(t, err)\n\tassert.Empty(t, pairs)\n\n\t// List on subfolderKey should return 3 keys without the directory.\n\tpairs, err = kv.List(ctx, subfolderKey, nil)\n\trequire.NoError(t, err)\n\tassert.Len(t, pairs, 3)\n\n\t// List should fail: the key does not exist.\n\tpairs, err = kv.List(ctx, \"idontexist\", nil)\n\tassert.ErrorIs(t, err, store.ErrKeyNotFound)\n\tassert.Nil(t, pairs)\n}\n\nfunc testListLockKey(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tlistKey := \"testListLockSide\"\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\terr := kv.Put(ctx, listKey, []byte(\"val\"), &store.WriteOptions{IsDir: true})\n\trequire.NoError(t, err)\n\n\terr = kv.Put(ctx, listKey+\"/subfolder\", []byte(\"val\"), &store.WriteOptions{IsDir: true})\n\trequire.NoError(t, err)\n\n\t// Put keys under subfolder.\n\tfor i := 1; i <= 3; i++ {\n\t\tkey := listKey + \"/subfolder/key\" + strconv.Itoa(i)\n\t\terrPut := kv.Put(ctx, key, []byte(\"val\"), nil)\n\t\trequire.NoError(t, errPut)\n\n\t\t// We lock the child key.\n\t\tlock, errPut := kv.NewLock(ctx, key, &store.LockOptions{Value: []byte(\"locked\"), TTL: 2 * time.Second})\n\t\trequire.NoError(t, errPut)\n\t\trequire.NotNil(t, lock)\n\n\t\tlockChan, errPut := lock.Lock(ctx)\n\t\trequire.NoError(t, errPut)\n\t\tassert.NotNil(t, lockChan)\n\t}\n\n\t// List children of the root directory (`listKey`), this should\n\t// not output any `___lock` entries and must contain 4 results.\n\tpairs, err := kv.List(ctx, listKey, nil)\n\trequire.NoError(t, err)\n\tassert.Len(t, pairs, 4)\n\n\tfor _, pair := range pairs {\n\t\tif strings.Contains(pair.Key, \"___lock\") {\n\t\t\tassert.FailNow(t, \"tesListLockKey: found a key containing lock suffix '___lock'\")\n\t\t}\n\t}\n}\n\nfunc testDeleteTree(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tprefix := \"testDeleteTree\"\n\n\tfirstKey := \"testDeleteTree/first\"\n\tfirstValue := []byte(\"first\")\n\n\tsecondKey := \"testDeleteTree/second\"\n\tsecondValue := []byte(\"second\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\t// Put the first key.\n\terr := kv.Put(ctx, firstKey, firstValue, nil)\n\trequire.NoError(t, err)\n\n\t// Put the second key.\n\terr = kv.Put(ctx, secondKey, secondValue, nil)\n\trequire.NoError(t, err)\n\n\t// Get should work on the first Key.\n\tpair, err := kv.Get(ctx, firstKey, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, firstValue)\n\tassert.NotEqual(t, 0, pair.LastIndex)\n\n\t// Get should work on the second Key.\n\tpair, err = kv.Get(ctx, secondKey, nil)\n\trequire.NoError(t, err)\n\tcheckPairNotNil(t, pair)\n\tassert.Equal(t, pair.Value, secondValue)\n\tassert.NotEqual(t, 0, pair.LastIndex)\n\n\t// Delete Values under directory `nodes`.\n\terr = kv.DeleteTree(ctx, prefix)\n\trequire.NoError(t, err)\n\n\t// Get should fail on both keys.\n\tpair, err = kv.Get(ctx, firstKey, nil)\n\tassert.Error(t, err)\n\tassert.Nil(t, pair)\n\n\tpair, err = kv.Get(ctx, secondKey, nil)\n\tassert.Error(t, err)\n\tassert.Nil(t, pair)\n}\n\n// RunCleanup cleans up keys introduced by the tests.\nfunc RunCleanup(t *testing.T, kv store.Store) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), testTimeout)\n\tdefer cancel()\n\n\tfor _, key := range []string{\n\t\t\"testAtomicPutWithSlashSuffixKey\",\n\t\t\"testPutGetDeleteExists\",\n\t\t\"testWatch\",\n\t\t\"testWatchTree\",\n\t\t\"testAtomicPut\",\n\t\t\"testAtomicPutCreate\",\n\t\t\"testAtomicDelete\",\n\t\t\"testLockUnlock\",\n\t\t\"testLockTTL\",\n\t\t\"testPutTTL\",\n\t\t\"testList/subfolder\",\n\t\t\"testList\",\n\t\t\"testListLockSide/subfolder\",\n\t\t\"testListLockSide\",\n\t\t\"testDeleteTree\",\n\t} {\n\t\terr := kv.DeleteTree(ctx, key)\n\t\tif err != nil {\n\t\t\tassert.ErrorIsf(t, err, store.ErrKeyNotFound, \"failed to delete tree key %s\", key)\n\t\t}\n\n\t\terr = kv.Delete(ctx, key)\n\t\tif err != nil {\n\t\t\tassert.ErrorIsf(t, err, store.ErrKeyNotFound, \"failed to delete key %s\", key)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "valkeyrie.go",
    "content": "// Package valkeyrie Distributed Key/Value Store Abstraction Library written in Go.\npackage valkeyrie\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/kvtools/valkeyrie/store\"\n)\n\nvar (\n\tconstructorsMu sync.RWMutex\n\tconstructors   = make(map[string]Constructor)\n)\n\n// Config the raw type of the store configurations.\ntype Config any\n\n// Constructor The signature of a store constructor.\ntype Constructor func(ctx context.Context, endpoints []string, options Config) (store.Store, error)\n\n// Register makes a store constructor available by the provided name.\n// If Register is called twice with the same name or if constructor is nil, it panics.\nfunc Register(name string, cttr Constructor) {\n\tconstructorsMu.Lock()\n\tdefer constructorsMu.Unlock()\n\n\tif cttr == nil {\n\t\tpanic(\"valkeyrie: Register constructor is nil\")\n\t}\n\n\tif _, dup := constructors[name]; dup {\n\t\tpanic(\"valkeyrie: Register called twice for constructor \" + name)\n\t}\n\n\tconstructors[name] = cttr\n}\n\n// Unregister Unregisters a store.\nfunc Unregister(storeName string) {\n\tconstructorsMu.Lock()\n\tdefer constructorsMu.Unlock()\n\n\tdelete(constructors, storeName)\n}\n\n// UnregisterAllConstructors Unregisters all stores.\nfunc UnregisterAllConstructors() {\n\tconstructorsMu.Lock()\n\tdefer constructorsMu.Unlock()\n\n\tconstructors = make(map[string]Constructor)\n}\n\n// Constructors returns a sorted list of the names of the registered constructors.\nfunc Constructors() []string {\n\tconstructorsMu.RLock()\n\tdefer constructorsMu.RUnlock()\n\n\tlist := make([]string, 0, len(constructors))\n\tfor name := range constructors {\n\t\tlist = append(list, name)\n\t}\n\n\tsort.Strings(list)\n\n\treturn list\n}\n\n// NewStore creates a new store instance.\nfunc NewStore(ctx context.Context, storeName string, endpoints []string, options Config) (store.Store, error) {\n\tconstructorsMu.RLock()\n\tconstruct, ok := constructors[storeName]\n\tconstructorsMu.RUnlock()\n\n\tif !ok {\n\t\treturn nil, &store.UnknownConstructorError{Store: storeName}\n\t}\n\n\tif construct == nil {\n\t\treturn nil, &store.UnknownConstructorError{Store: storeName}\n\t}\n\n\treturn construct(ctx, endpoints, options)\n}\n"
  },
  {
    "path": "valkeyrie_test.go",
    "content": "package valkeyrie\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRegister(t *testing.T) {\n\tt.Cleanup(UnregisterAllConstructors)\n\n\tRegister(testStoreName, newStore)\n\n\tassert.Len(t, constructors, 1)\n}\n\nfunc TestRegister_duplicate(t *testing.T) {\n\tt.Cleanup(UnregisterAllConstructors)\n\n\tRegister(testStoreName, newStore)\n\tassert.Len(t, constructors, 1)\n\n\tassert.Panics(t, func() {\n\t\tRegister(testStoreName, newStore)\n\t})\n}\n\nfunc TestRegister_nil(t *testing.T) {\n\tt.Cleanup(UnregisterAllConstructors)\n\n\tassert.Panics(t, func() {\n\t\tRegister(testStoreName, nil)\n\t})\n}\n\nfunc TestUnregister(t *testing.T) {\n\tt.Cleanup(UnregisterAllConstructors)\n\n\tRegister(testStoreName, newStore)\n\tassert.Len(t, constructors, 1)\n\n\tUnregister(testStoreName)\n\n\tconstructorsMu.Lock()\n\tdefer constructorsMu.Unlock()\n\n\tassert.Empty(t, constructors)\n}\n\nfunc TestConstructors(t *testing.T) {\n\tt.Cleanup(UnregisterAllConstructors)\n\n\tRegister(testStoreName, newStore)\n\tassert.Len(t, constructors, 1)\n\n\tcttrs := Constructors()\n\n\texpected := []string{testStoreName}\n\tassert.Equal(t, expected, cttrs)\n}\n\nfunc TestNewStore(t *testing.T) {\n\tt.Cleanup(UnregisterAllConstructors)\n\n\tRegister(testStoreName, newStore)\n\n\tassert.Len(t, constructors, 1)\n\n\ts, err := NewStore(context.Background(), testStoreName, nil, nil)\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, s)\n\tassert.IsType(t, &Mock{}, s)\n}\n"
  }
]