├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── build-lib.ts ├── deno.json ├── deno.lock ├── main.ts ├── mod.ts ├── parse.ts ├── types.ts └── util.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'main' 8 | tags: 9 | - 'v*' 10 | pull_request: 11 | branches: 12 | - 'main' 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v2 25 | 26 | - name: Docker meta 27 | id: meta 28 | uses: docker/metadata-action@v4 29 | with: 30 | images: voxo/nqlite 31 | tags: | 32 | type=schedule 33 | type=ref,event=branch 34 | type=ref,event=pr 35 | type=semver,pattern={{version}} 36 | type=semver,pattern={{major}}.{{minor}} 37 | type=semver,pattern={{major}} 38 | 39 | - name: Login to GitHub Container Registry 40 | uses: docker/login-action@v2 41 | with: 42 | username: ${{ github.actor }} 43 | password: ${{ secrets.DOCKER_PAT }} 44 | 45 | - name: Build and push 46 | uses: docker/build-push-action@v3 47 | with: 48 | context: . 49 | push: true 50 | labels: ${{ steps.meta.outputs.labels }} 51 | tags: ${{ steps.meta.outputs.tags }} 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max 54 | - uses: denoland/setup-deno@v1.1.1 55 | with: 56 | deno-version: v1.x 57 | - name: Generate the artifacts 58 | if: startsWith(github.ref, 'refs/tags/v') 59 | run: | 60 | VERSION=${GITHUB_REF_NAME#v} 61 | deno compile --target=x86_64-unknown-linux-gnu --output nqlite-$VERSION-linux-amd64/nqlite -A --unstable main.ts 62 | deno compile --target=x86_64-apple-darwin --output nqlite-$VERSION-darwin-amd64/nqlite -A --unstable main.ts 63 | deno compile --target=aarch64-apple-darwin --output nqlite-$VERSION-darwin-arm64/nqlite -A --unstable main.ts 64 | mkdir bin 65 | tar -czf bin/nqlite-$VERSION-linux-amd64.tar.gz nqlite-$VERSION-linux-amd64/nqlite 66 | tar -czf bin/nqlite-$VERSION-darwin-amd64.tar.gz nqlite-$VERSION-darwin-amd64/nqlite 67 | tar -czf bin/nqlite-$VERSION-darwin-arm64.tar.gz nqlite-$VERSION-darwin-arm64/nqlite 68 | 69 | - name: Upload the artifacts 70 | uses: svenstaro/upload-release-action@v2 71 | if: startsWith(github.ref, 'refs/tags/v') 72 | with: 73 | repo_token: ${{ secrets.CR_PAT }} 74 | file: bin/* 75 | tag: ${{ github.ref_name }} 76 | overwrite: true 77 | file_glob: true 78 | - name: Release to homebrew 79 | uses: Justintime50/homebrew-releaser@v1 80 | if: startsWith(github.ref, 'refs/tags/v') 81 | with: 82 | homebrew_owner: ${{ github.repository_owner }} 83 | homebrew_tap: homebrew-apps 84 | formula_folder: formula 85 | github_token: ${{ secrets.CR_PAT }} 86 | commit_owner: homebrew-releaser 87 | commit_email: homebrew-releaser@example.com 88 | 89 | # Custom install command for your formula. 90 | # Required - string. 91 | install: 'bin.install "nqlite"' 92 | target_darwin_amd64: true 93 | target_darwin_arm64: true 94 | target_linux_amd64: true 95 | target_linux_arm64: false 96 | update_readme_table: true 97 | skip_commit: false 98 | debug: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nqlite-data -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM denoland/deno:alpine 2 | 3 | EXPOSE 4001 4 | 5 | WORKDIR /app 6 | 7 | ADD . . 8 | 9 | RUN deno cache main.ts 10 | 11 | # Pre-download the sqlite module 12 | RUN deno run -A --unstable build-lib.ts 13 | 14 | ENTRYPOINT ["deno", "run", "-A", "--unstable", "main.ts"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 VOXO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nqlite (NATS)qlite 2 | 3 | nqlite is an easy-to-use, lightweight relational database using 4 | [SQLite](https://www.sqlite.org/) as the storage engine and 5 | [NATS Jetstream](https://docs.nats.io/nats-concepts/jetstream) for replication 6 | and persistence. nqlite is heavily influenced by 7 | [rqlite](https://github.com/rqlite/rqlite) but provides a simpler data API 8 | because there is no need for a RAFT leader. 9 | 10 | nqlite aims to solve the problems of distributing relational data globally and 11 | at the edge like: 12 | 13 | - Management of IP's and DNS 14 | - Complexity of read/write splitting and proxies 15 | - Not having your relational data next to your application 16 | - Disaster recovery/failover of master and general orchestration 17 | - Traditional relational databases are not designed for edge deployments 18 | 19 | ## Features 20 | 21 | - Simple HTTP API for interacting with SQLite via `db/query` 22 | - Snapshotting/restore out of the box (using 23 | [Object Store](https://docs.nats.io/using-nats/developer/develop_jetstream/object)) 24 | - NATS JetStream for SQL replication and persistence (via 25 | [Ephemeral Push Consumer](https://docs.nats.io/using-nats/developer/develop_jetstream/consumers)) 26 | - Lightweight, easy-to-use - just run the binary and pass in the NATS Websocket 27 | URL 28 | - Deploy anywhere - Linux, macOS, Windows, ARM, Edge, Kubernetes, Docker, etc. 29 | 30 | ## Why nqlite? 31 | 32 | We love rqlite and NATS and think these two projects should marry, have a baby 33 | and name it **nqlite**. We want to be able to deploy relational databases on the 34 | edge at VOXO and not deal with the complexity of IP's, DNS, proxies, 35 | survivability, orchestration, and consensus. nqlite has no concept of leader 36 | election or primary/replica. NATS Jetstream serves as this durable layer with 37 | its message sequencing and at-least-once delivery symantics. nqlite takes the 38 | complexity out of **deploying globally distributed relational databases.**. All 39 | it needs is a connection to NATS. 40 | 41 | - NATS JetStream with at-least once delivery is a great fit for SQL replication 42 | and persistence 43 | - Object Store is a great fit for snapshotting/restore 44 | - Who doesn't already use NATS for pub/sub? 45 | - The need for a dead simple edge relational database closer to the application 46 | - **Database nodes don't need to be aware of each other** 47 | - Deno is fun 48 | 49 | ## Quick Start 50 | 51 | The quickest way to get up and running is to download the binary from the 52 | [Releases](https://github.com/voxoco/nqlite/releases) page and run it like so: 53 | 54 | ```bash 55 | $ nqlite --nats-host=wss://FQDN --creds=./nats.creds 56 | ``` 57 | 58 | ### Command line options 59 | 60 | ```bash 61 | nqlite [options] 62 | --nats-host=wss://... # NATS NATS host e.g 'nats://localhost:4222' || 'ws://localhost:8080' (required) 63 | 64 | --creds=./nats.creds # NATS creds file - required if --token not provided 65 | 66 | --token=secret # NATS auth token - required if --creds not provided 67 | 68 | --data-dir=/nqlite-data # Data directory - optional (default: ./nqlite-data) 69 | 70 | --external-backup=http # External backup/restore method (option: 'http') 71 | 72 | --external-backup-url=http://someBlockStorage/backup/nqlite.db # External backup/restore URL 73 | 74 | --h - Help 75 | ``` 76 | 77 | ### Docker 78 | 79 | ```bash 80 | docker run voxo/nqlite -p 4001:4001 --nats-host=wss://FQDN --creds=./nats.creds 81 | ``` 82 | 83 | The Docker image is available on 84 | [Docker Hub](https://hub.docker.com/r/voxo/nqlite). 85 | 86 | ### Homebrew 87 | 88 | ```bash 89 | $ brew install voxoco/apps/nqlite 90 | $ nqlite --nats-host=wss://FQDN --token=secret 91 | ``` 92 | 93 | ### Deno 94 | 95 | ```bash 96 | $ deno run -A --unstable https://deno.land/x/nqlite/main.ts --nats-host=wss://FQDN --creds=./nats.creds 97 | ``` 98 | 99 | ## Dependencies 100 | 101 | - [natsws](https://deno.land/x/natsws/src/mod.ts) - For NATS 102 | - [sqlite3](https://deno.land/x/sqlite) - For performance 103 | - [Hono](https://deno.land/x/hono) - For HTTP API 104 | 105 | ## Coming Soon 106 | 107 | - [ ] Prometheus exporter 108 | - [ ] Transactions 109 | - [ ] API Authentication 110 | - [ ] InsertId and affectedRows in response 111 | - [ ] Work with Deno Deploy (memory db) 112 | - [ ] Handle queries via NATS request/reply 113 | - [ ] Ideas welcome! 114 | 115 | ## How it works 116 | 117 | nqlite is a Deno application that connects to NATS JetStream. It bootstraps 118 | itself by creating the necessary JetStream streams, consumers, and object store. 119 | It also takes care of snapshotting and restoring the SQLite db. When a node 120 | starts up, it checks the object store for an SQLite snapshot. If it finds one, 121 | it restores from the snapshot. If not, it creates a new SQLite db. The node then 122 | subscribes to the JetStream stream at the last sequence number processed by the 123 | SQLite db. Anytime the node receives a query via the stream, it executes the 124 | query and updates the `_nqlite_` table with the sequence number. Read requests 125 | are handled locally. Read more below about snapshotting and purging. 126 | 127 | ### Built-in configuration 128 | 129 | ```bash 130 | # Default Data directory 131 | ./.nqlite-data/nqlite 132 | 133 | # SQLite file 134 | ./.nqlite-data/nqlite/nqlite.db 135 | 136 | # NATS JetStream stream 137 | nqlite 138 | 139 | # NATS JetStream publish subject 140 | nqlite.push 141 | 142 | # NATS object store bucket 143 | nqlite 144 | 145 | # Snapshot interval hours (check how often to snapshot the db) 146 | 2 147 | 148 | # Snapshot threshold 149 | # Every snapshot interval, check if we have processed 150 | # this many messages since the last snapshot. If so, snapshot. 151 | 1024 152 | ``` 153 | 154 | ### Snapshot and purging 155 | 156 | Every `snapInterval` nqlite gets the latest snapshot sequence from object store 157 | description and compares it with the last processed sequence number from 158 | JetStream. If the node has processed more than `snapThreshold` messages since 159 | the object store snapshot sequence, the node unsubscribes from the stream and 160 | attempts a snapshot. 161 | 162 | The node backs up the SQLite db to object store and sets the latest processed 163 | sequence number to the object store `description` and purges all previous 164 | messages from the stream (older than `snapSequence - snapThreshold`). The node 165 | then resumes the JetStream subscription. The nice thing here is that JetStream 166 | will continue pushing messages to the interator from where it left off (so the 167 | node doesn’t miss any db changes and eventually catches back up). 168 | 169 | The other nice thing about this setup is that we can still accept writes on this 170 | node while the snapshotting is taking place. So the only sacrifice we make here 171 | is the nodes are eventually consistent (which seems to be an acceptable trade 172 | off). 173 | 174 | ### Snapshot restore 175 | 176 | When a node starts up, it checks if there is a snapshot in object store. If 177 | there is, it attempts to restore the SQLite db from object store. If there is no 178 | snapshot, it creates a new SQLite db and subscribes to the stream. 179 | 180 | ### API 181 | 182 | nqlite has a simple HTTP API for interacting with SQLite. Any db changes like 183 | (`INSERT`, `UPDATE`, `DELETE`) are published to a NATS JetStream subject. NATS 184 | handles replicating to other nqlite nodes. Any db queries like (`SELECT`) are 185 | handled locally and do not get published to NATS. 186 | 187 | ## Data API 188 | 189 | ### `db/query` 190 | 191 | Each nqlite node exposes an HTTP API for interacting with SQLite. Any db changes 192 | like (`INSERT`, `UPDATE`, `DELETE`) are published to NATS JetStream via an 193 | `nqlite.push` subject and replicated to other nqlite nodes. Any db queries like 194 | (`SELECT`) are handled locally and do not require NATS. All data requests are 195 | handled via a `POST` or `GET` request to the `/db/query` endpoint. 196 | 197 | ### Writing Data 198 | 199 | ```bash 200 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 201 | "CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT, age INTEGER)" 202 | ]' 203 | 204 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 205 | "INSERT INTO foo(name, age) VALUES(\"fiona\", 20)" 206 | ]' 207 | ``` 208 | 209 | ### Response 210 | 211 | ```json 212 | { 213 | "results": [ 214 | { 215 | "nats": { 216 | "stream": "nqlite", 217 | "seq": 4, 218 | "duplicate": false 219 | } 220 | } 221 | ], 222 | "time": 18.93841699999757 // ms to publish to NATS 223 | } 224 | ``` 225 | 226 | ### Querying Data 227 | 228 | ```bash 229 | curl -G 'localhost:4001/db/query' --data-urlencode 'q=SELECT * FROM foo' 230 | ``` 231 | 232 | #### OR 233 | 234 | ```bash 235 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 236 | "SELECT * FROM foo" 237 | ]' 238 | ``` 239 | 240 | ### Response 241 | 242 | ```json 243 | { 244 | "results": [ 245 | { 246 | "rows": [ 247 | { 248 | "id": 1, 249 | "name": "fiona", 250 | "age": 20 251 | } 252 | ] 253 | } 254 | ], 255 | "time": 0.5015830000047572 // ms to get results 256 | } 257 | ``` 258 | 259 | ## Parameterized Queries 260 | 261 | ### Write 262 | 263 | ```bash 264 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 265 | ["INSERT INTO foo(name, age) VALUES(?, ?)", "fiona", 20] 266 | ]' 267 | ``` 268 | 269 | ### Read 270 | 271 | ```bash 272 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 273 | ["SELECT * FROM foo WHERE name=?", "fiona"] 274 | ]' 275 | ``` 276 | 277 | ## Named Parameters 278 | 279 | ### Write 280 | 281 | ```bash 282 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 283 | ["INSERT INTO foo(name, age) VALUES(:name, :age)", {"name": "fiona", "age": 20}] 284 | ]' 285 | ``` 286 | 287 | ### Read 288 | 289 | ```bash 290 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 291 | ["SELECT * FROM foo WHERE name=:name", {"name": "fiona"}] 292 | ]' 293 | ``` 294 | 295 | ## Bulk Queries 296 | 297 | ### Write 298 | 299 | ```bash 300 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d "[ 301 | \"INSERT INTO foo(name) VALUES('fiona')\", 302 | \"INSERT INTO foo(name) VALUES('sinead')\" 303 | ]" 304 | ``` 305 | 306 | ### Parameterized Bulk Queries 307 | 308 | ```bash 309 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 310 | ["INSERT INTO foo(name) VALUES(?)", "fiona"], 311 | ["INSERT INTO foo(name) VALUES(?)", "sinead"] 312 | ]' 313 | ``` 314 | 315 | ## Named Parameter Bulk Queries 316 | 317 | ```bash 318 | curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ 319 | ["INSERT INTO foo(name) VALUES(:name)", {"name": "fiona"}], 320 | ["INSERT INTO foo(name) VALUES(:name)", {"name": "sinead"}] 321 | ]' 322 | ``` 323 | 324 | ## Transactions 325 | 326 | Not implemented yet 327 | 328 | ## Error handling 329 | 330 | nqlite will return a body that looks like this: 331 | 332 | ```json 333 | { 334 | "results": [ 335 | { 336 | "error": "Some error message" 337 | } 338 | ], 339 | "time": 0.5015830000047572 340 | } 341 | ``` 342 | 343 | ## Contributing 344 | 345 | Contributions are welcome! Please open an issue or PR. Any criticism/ideas are 346 | welcome. 347 | -------------------------------------------------------------------------------- /build-lib.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "sqlite3"; 2 | 3 | const db = new Database(":memory:"); 4 | 5 | db.close(); 6 | 7 | Deno.exit(0); 8 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "run": "deno cache --unstable main.ts && deno run -A --unstable main.ts", 4 | "build_linux": "deno compile --target=x86_64-unknown-linux-gnu --output nqlite_linux -A --unstable main.ts", 5 | "build_mac": "deno compile --target=aarch64-apple-darwin --output nqlite_mac -A --unstable main.ts", 6 | "build_windows": "deno compile --target=x86_64-pc-windows-msvc --output nqlite_windows -A --unstable main.ts" 7 | }, 8 | "imports": { 9 | "sqlite3": "https://deno.land/x/sqlite3@0.8.1/mod.ts", 10 | "natsws": "https://deno.land/x/natsws@v1.14.0/src/mod.ts", 11 | "nats": "https://deno.land/x/nats@v1.13.0/src/mod.ts", 12 | "serve": "https://deno.land/std@0.181.0/http/server.ts", 13 | "hono": "https://deno.land/x/hono@v2.7.8/mod.ts" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/std@0.152.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", 5 | "https://deno.land/std@0.152.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", 6 | "https://deno.land/std@0.152.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", 7 | "https://deno.land/std@0.152.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", 8 | "https://deno.land/std@0.152.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", 9 | "https://deno.land/std@0.152.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", 10 | "https://deno.land/std@0.152.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", 11 | "https://deno.land/std@0.152.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac", 12 | "https://deno.land/std@0.152.0/path/posix.ts": "c1f7afe274290ea0b51da07ee205653b2964bd74909a82deb07b69a6cc383aaa", 13 | "https://deno.land/std@0.152.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", 14 | "https://deno.land/std@0.152.0/path/win32.ts": "bd7549042e37879c68ff2f8576a25950abbfca1d696d41d82c7bca0b7e6f452c", 15 | "https://deno.land/std@0.168.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", 16 | "https://deno.land/std@0.168.0/flags/mod.ts": "4f50ec6383c02684db35de38b3ffb2cd5b9fcfcc0b1147055d1980c49e82521c", 17 | "https://deno.land/std@0.176.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", 18 | "https://deno.land/std@0.176.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", 19 | "https://deno.land/std@0.176.0/encoding/hex.ts": "50f8c95b52eae24395d3dfcb5ec1ced37c5fe7610ef6fffdcc8b0fdc38e3b32f", 20 | "https://deno.land/std@0.176.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471", 21 | "https://deno.land/std@0.176.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", 22 | "https://deno.land/std@0.176.0/fs/copy.ts": "14214efd94fc3aa6db1e4af2b4b9578e50f7362b7f3725d5a14ad259a5df26c8", 23 | "https://deno.land/std@0.176.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", 24 | "https://deno.land/std@0.176.0/fs/ensure_dir.ts": "724209875497a6b4628dfb256116e5651c4f7816741368d6c44aab2531a1e603", 25 | "https://deno.land/std@0.176.0/fs/ensure_file.ts": "c38602670bfaf259d86ca824a94e6cb9e5eb73757fefa4ebf43a90dd017d53d9", 26 | "https://deno.land/std@0.176.0/fs/ensure_link.ts": "c0f5b2f0ec094ed52b9128eccb1ee23362a617457aa0f699b145d4883f5b2fb4", 27 | "https://deno.land/std@0.176.0/fs/ensure_symlink.ts": "2955cc8332aeca9bdfefd05d8d3976b94e282b0f353392a71684808ed2ffdd41", 28 | "https://deno.land/std@0.176.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", 29 | "https://deno.land/std@0.176.0/fs/exists.ts": "b8c8a457b71e9d7f29b9d2f87aad8dba2739cbe637e8926d6ba6e92567875f8e", 30 | "https://deno.land/std@0.176.0/fs/expand_glob.ts": "45d17e89796a24bd6002e4354eda67b4301bb8ba67d2cac8453cdabccf1d9ab0", 31 | "https://deno.land/std@0.176.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", 32 | "https://deno.land/std@0.176.0/fs/move.ts": "4cb47f880e3f0582c55e71c9f8b1e5e8cfaacb5e84f7390781dd563b7298ec19", 33 | "https://deno.land/std@0.176.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", 34 | "https://deno.land/std@0.176.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", 35 | "https://deno.land/std@0.176.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", 36 | "https://deno.land/std@0.176.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", 37 | "https://deno.land/std@0.176.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", 38 | "https://deno.land/std@0.176.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", 39 | "https://deno.land/std@0.176.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", 40 | "https://deno.land/std@0.176.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", 41 | "https://deno.land/std@0.176.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", 42 | "https://deno.land/std@0.176.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", 43 | "https://deno.land/std@0.177.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", 44 | "https://deno.land/std@0.177.0/bytes/bytes_list.ts": "b4cbdfd2c263a13e8a904b12d082f6177ea97d9297274a4be134e989450dfa6a", 45 | "https://deno.land/std@0.177.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", 46 | "https://deno.land/std@0.177.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", 47 | "https://deno.land/std@0.177.0/io/buf_reader.ts": "90a7adcb3638d8e1361695cdf844d58bcd97c41711dc6f9f8acc0626ebe097f5", 48 | "https://deno.land/std@0.177.0/io/buf_writer.ts": "759c69d304b04d2909976f2a03a24a107276fbd81ed13593c5c2d43d104b52f3", 49 | "https://deno.land/std@0.177.0/io/buffer.ts": "e2b7564f684dad625cab08f5106f33572d325705d19a36822b3272fbdfb8f726", 50 | "https://deno.land/std@0.177.0/io/copy_n.ts": "c498021ce291576a68b5bae9f9d3a27f97644f4af6c1047fb1cff054af19e436", 51 | "https://deno.land/std@0.177.0/io/limited_reader.ts": "d709b5b3113d4cbf934415ba242596e0ecb130e8868fb47197217e09dbb59558", 52 | "https://deno.land/std@0.177.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b", 53 | "https://deno.land/std@0.177.0/io/multi_reader.ts": "5f7ef6e987486322b38c72e206b8fbc8916d55a87fbcdc97a8b2596386c28d44", 54 | "https://deno.land/std@0.177.0/io/read_delim.ts": "7e102c66f00a118fa1e1ccd4abb080496f43766686907fd8b9522fdf85443586", 55 | "https://deno.land/std@0.177.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f", 56 | "https://deno.land/std@0.177.0/io/read_lines.ts": "baee9e35034f2fdfccf63bc24b7e3cb45aa1c1c5de26d178f7bcbc572e87772f", 57 | "https://deno.land/std@0.177.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e", 58 | "https://deno.land/std@0.177.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5", 59 | "https://deno.land/std@0.177.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20", 60 | "https://deno.land/std@0.177.0/io/read_string_delim.ts": "46eb0c9db3547caf8c759631effa200bbe48924f9b34f41edc627bde36cee52d", 61 | "https://deno.land/std@0.177.0/io/slice_long_to_bytes.ts": "b096472afa3a0dd90fa84584dde7706ed29fc16d48009a581c49368f09fe70f4", 62 | "https://deno.land/std@0.177.0/io/string_reader.ts": "ad9cbecb8509732afcf3d73bb72fa551ec0ccc34f9b8127826247f0190753a65", 63 | "https://deno.land/std@0.177.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e", 64 | "https://deno.land/std@0.177.0/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784", 65 | "https://deno.land/std@0.181.0/async/abortable.ts": "fd682fa46f3b7b16b4606a5ab52a7ce309434b76f820d3221bdfb862719a15d7", 66 | "https://deno.land/std@0.181.0/async/deadline.ts": "c5facb0b404eede83e38bd2717ea8ab34faa2ffb20ef87fd261fcba32ba307aa", 67 | "https://deno.land/std@0.181.0/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332", 68 | "https://deno.land/std@0.181.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", 69 | "https://deno.land/std@0.181.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", 70 | "https://deno.land/std@0.181.0/async/mod.ts": "f04344fa21738e5ad6bea37a6bfffd57c617c2d372bb9f9dcfd118a1b622e576", 71 | "https://deno.land/std@0.181.0/async/mux_async_iterator.ts": "70c7f2ee4e9466161350473ad61cac0b9f115cff4c552eaa7ef9d50c4cbb4cc9", 72 | "https://deno.land/std@0.181.0/async/pool.ts": "fd082bd4aaf26445909889435a5c74334c017847842ec035739b4ae637ae8260", 73 | "https://deno.land/std@0.181.0/async/retry.ts": "dd19d93033d8eaddbfcb7654c0366e9d3b0a21448bdb06eba4a7d8a8cf936a92", 74 | "https://deno.land/std@0.181.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", 75 | "https://deno.land/std@0.181.0/http/server.ts": "cbb17b594651215ba95c01a395700684e569c165a567e4e04bba327f41197433", 76 | "https://deno.land/x/hono@v2.7.8/compose.ts": "55f00eb0547c368d2c13304eb2dd9ea6e2cf738bb2c9e3fae966260a948568f6", 77 | "https://deno.land/x/hono@v2.7.8/context.ts": "f38b208bea645c4c1e9da8cd35edaa69dece92b200ab5217eee50f8816591013", 78 | "https://deno.land/x/hono@v2.7.8/hono.ts": "6243db4056e9d03322ceac6cc6c5304a842661909401b954db74945dc0cf4911", 79 | "https://deno.land/x/hono@v2.7.8/mod.ts": "a6f5fc79ba07e98e1918694cb96f78358e0e967def1184ddb941d3a905cd9fbd", 80 | "https://deno.land/x/hono@v2.7.8/request.ts": "b0e1b2ad2aaa925ce992065c108f7ab2c75ab145012fd94a0816b99562834311", 81 | "https://deno.land/x/hono@v2.7.8/router.ts": "21448bc2e6019574c10fae11237da4367037fa107e68bf3d049cd2fd0efd2adb", 82 | "https://deno.land/x/hono@v2.7.8/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db", 83 | "https://deno.land/x/hono@v2.7.8/router/reg-exp-router/node.ts": "08bb532ffe43e6e11510f9f66122baef43288f05f592b17987fc3a2dc195251a", 84 | "https://deno.land/x/hono@v2.7.8/router/reg-exp-router/router.ts": "b158d519d7bc5bbae6a8b099a8563c51b90c132116353967ab9bef7f6e86e446", 85 | "https://deno.land/x/hono@v2.7.8/router/reg-exp-router/trie.ts": "ab8a4ec6c3cdfdbcd1f53130db28b6174318bdfa815d57c792154203e71bc3d2", 86 | "https://deno.land/x/hono@v2.7.8/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef", 87 | "https://deno.land/x/hono@v2.7.8/router/smart-router/router.ts": "1d54f5c87875d856ed5fc2d22a100e1ff31debe3e9d8e9b1cc18d8e5706239f2", 88 | "https://deno.land/x/hono@v2.7.8/router/static-router/index.ts": "f31aaebff3b2b05f00946d3a8b688289a43b5c2bfb124e54ffadeee6a24a1b52", 89 | "https://deno.land/x/hono@v2.7.8/router/static-router/router.ts": "75a15b2964dc90a46aed7b313f8760b60af5d5e8a819c3a948b4ad8d3fdb7662", 90 | "https://deno.land/x/hono@v2.7.8/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41", 91 | "https://deno.land/x/hono@v2.7.8/router/trie-router/node.ts": "514ef8e66a96d1ececc2823421d5a4001d59a1cec241bb545688716cba1e9dbb", 92 | "https://deno.land/x/hono@v2.7.8/router/trie-router/router.ts": "0a969528a0c1680b552b20f0ca90e484e968ac279be9d5fd952b61a804d680e7", 93 | "https://deno.land/x/hono@v2.7.8/types.ts": "1b8a876c013caee63287c198983740e3467e6f88a7f1a56301418904147370fb", 94 | "https://deno.land/x/hono@v2.7.8/utils/body.ts": "095afd5b9cd8458bb78353479911a428a23acbef1ffd545483f7b051c08a9fc0", 95 | "https://deno.land/x/hono@v2.7.8/utils/cookie.ts": "545872bd7af3b455c24fd386ecbccfd161e7d4a0038d6b09b1bb22723602f90a", 96 | "https://deno.land/x/hono@v2.7.8/utils/http-status.ts": "2d6003e352c1fe918db663fa4bd2b20bf0b9d4e1699ba5e163f317f00b29d938", 97 | "https://deno.land/x/hono@v2.7.8/utils/json.ts": "ae34c6191e7a844f04a95efaffb177b6a15a3bc5b17321c5679081940df6ab1d", 98 | "https://deno.land/x/hono@v2.7.8/utils/types.ts": "173dedfe018b447cc6b067d2b6968c1f1dccba67ad50526d356b79e0465a5753", 99 | "https://deno.land/x/hono@v2.7.8/utils/url.ts": "120cfd1297f91dc269454ad4bc3fabb54593cd9994ec2e6c4f56b6d9018419a0", 100 | "https://deno.land/x/hono@v2.7.8/validator/rule.ts": "a6614973170e1e4b71e260a678c1b28aecfd17053273e59bf99f1382b4f74eaa", 101 | "https://deno.land/x/hono@v2.7.8/validator/sanitizer.ts": "da03fdd70baa6d2f178d616a9f35cea1028a15d1429339ee3c3f9eff9aded38f", 102 | "https://deno.land/x/hono@v2.7.8/validator/schema.ts": "14200fd2e40022534f609b1331cd2058f87e060d448d0949bd56d80ad122d1ec", 103 | "https://deno.land/x/hono@v2.7.8/validator/validator.ts": "e29a871c60f98cc393638d334194d21c8aafe461765301e441f5c0fce65fff4a", 104 | "https://deno.land/x/nats@v1.13.0/nats-base-client/authenticator.ts": "957275b79259a732d14774e153615512046e511a981550fefb5f58a4c4f113b9", 105 | "https://deno.land/x/nats@v1.13.0/nats-base-client/base64.ts": "c031bf9f8ac451f2e50c44a3b2f9ff564d61bb14c51834ce73db837e1dee432b", 106 | "https://deno.land/x/nats@v1.13.0/nats-base-client/bench.ts": "4345f9a0b74867476b91ef4e3fd8228574821fc09dd74715bc187a3c1c517a09", 107 | "https://deno.land/x/nats@v1.13.0/nats-base-client/codec.ts": "7067df0935473afe5d7e943bd33381d2be768972b7bf306c3b0cd88cdfcfcf26", 108 | "https://deno.land/x/nats@v1.13.0/nats-base-client/databuffer.ts": "4f92f772ca6701d15f2d148d2d34543558d458b20014d0404f605f84b747b75f", 109 | "https://deno.land/x/nats@v1.13.0/nats-base-client/denobuffer.ts": "83686bd747953167824694a3f4882ed7134a4fe70b3af89b65b41c284bc0c06a", 110 | "https://deno.land/x/nats@v1.13.0/nats-base-client/encoders.ts": "af6cb8be55541f934c7048a611e9d1af306093edfe32cd33977f005ebde65d70", 111 | "https://deno.land/x/nats@v1.13.0/nats-base-client/error.ts": "f00f6c60f9c462415c92652e326ca6c4da2c2edd4d55335a593d50a54b8c8c49", 112 | "https://deno.land/x/nats@v1.13.0/nats-base-client/headers.ts": "bdc3042cdd89673bf6e1e7ae1d2922f9d5b4c73ae99c08567055f181170dfe63", 113 | "https://deno.land/x/nats@v1.13.0/nats-base-client/heartbeats.ts": "d5cca54532f4280437263f54288c0cdf5cc17263ed5799db73b7bee6b5549532", 114 | "https://deno.land/x/nats@v1.13.0/nats-base-client/idleheartbeat.ts": "42c778ccc9de852c162715e3e8bcdabddeae21d038b6ffecf07f257568a1c2da", 115 | "https://deno.land/x/nats@v1.13.0/nats-base-client/internal_mod.ts": "a5d9a70f98de40e2f98538f81d6609ae2d2ab58e4d4102fbbdf3fb8860d3a230", 116 | "https://deno.land/x/nats@v1.13.0/nats-base-client/ipparser.ts": "eced7163655be5bb457f9a3a5880239131996e3ae7d7817db88c156faa42d29f", 117 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsbaseclient_api.ts": "ea0eb1e4ef1f39027303c0cddc6ab2daad529029e5f9570e549b99c7440781b6", 118 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsclient.ts": "e3435c06a45cdc37ce0eddb0438c0262b4a82cc7ea0bf707b8e9d8cf9ac2fb35", 119 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsconsumeropts.ts": "95ea16f67ded03e7ecd5f302ac3f0b710c1e1f88d832b030f139b36be95e9f0c", 120 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jslister.ts": "f7e360f75ad77813c043aa3ab96e17bf9c2711f20709bacadd0402fbfbf70004", 121 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsm.ts": "89087bcd6da1d3a2207055ffe76063ba70e1eebee2fb0f1f0322b68c823ca39a", 122 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsmconsumer_api.ts": "7178b335856a16c34360cd5120e8031eeeeeff9c92c204e13536dc5345c8ca08", 123 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsmdirect_api.ts": "ffa7e3c880f89c8273cbb4fa2a12ac1f96312d1a1a47a59d6a0fcdf296e0dd98", 124 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsmsg.ts": "920b6663c1c52ac02fa50ad80024b1f364c25b529e826a22d92e339262f5c5a8", 125 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsmstream_api.ts": "350ce3f1cca5f0fb2525c247d4a69ce308ae4dde17f9877d422043b053665ec9", 126 | "https://deno.land/x/nats@v1.13.0/nats-base-client/jsutil.ts": "fdfedddb046393f7c0c688c3c0b39eac0906288800740444b0f523a598f66fe9", 127 | "https://deno.land/x/nats@v1.13.0/nats-base-client/kv.ts": "82240e37be2ae36e573f677c4d6d478cf4cb16b558f4389b387c4b4ad09b2e8a", 128 | "https://deno.land/x/nats@v1.13.0/nats-base-client/mod.ts": "21843a44e5c0db2b9014e6408b251d43563e06df73315214bf24efeb7967ec8a", 129 | "https://deno.land/x/nats@v1.13.0/nats-base-client/msg.ts": "a5f7a546d7753d05377c14d192ad9b518be6d15fb02d7b679e723d05f7218719", 130 | "https://deno.land/x/nats@v1.13.0/nats-base-client/muxsubscription.ts": "aba292629719f8e0a2dac597292e7a74407840a0049dbd044134773d9fd49eac", 131 | "https://deno.land/x/nats@v1.13.0/nats-base-client/nats.ts": "59456c645d2603fb5aa8ba5536df170625b6861ac40fddd8c351cf5e54ed26a9", 132 | "https://deno.land/x/nats@v1.13.0/nats-base-client/nkeys.ts": "01aa92908831dd3baad8dbc0e1b3eb05c7b8532a96f799244ff5da16aaee8e69", 133 | "https://deno.land/x/nats@v1.13.0/nats-base-client/nuid.ts": "f9b0a944338f6b5cf44dd652d22016acdc59ab6fbdaef6cdffa4bae633f2e0f5", 134 | "https://deno.land/x/nats@v1.13.0/nats-base-client/objectstore.ts": "3b85fe3d6b86f6b3795db5541b047beb510e0434bf1562a5e9d81c4bcbfcb01a", 135 | "https://deno.land/x/nats@v1.13.0/nats-base-client/options.ts": "08dbf25c5a8f858758e3799ea1dc806f1164b1da206e6a234ab128dc7c07463a", 136 | "https://deno.land/x/nats@v1.13.0/nats-base-client/parser.ts": "1fb5b9cd07be194ff8c93ee531e04e0b1d03da76001f3268e87599cc1edabe1b", 137 | "https://deno.land/x/nats@v1.13.0/nats-base-client/protocol.ts": "64fca11cdf72bac9e7ef0cfb1547211c1d5be61b170d284076e4275d8c9f64ed", 138 | "https://deno.land/x/nats@v1.13.0/nats-base-client/queued_iterator.ts": "0d92da9f22d3849936f69e060256558eaf04269cc26d9e3741d5cd0769392f6d", 139 | "https://deno.land/x/nats@v1.13.0/nats-base-client/request.ts": "4166bb803a5d82589f5b7bc7b45bfb2c5647fe32adff86ff88aa85280bd64811", 140 | "https://deno.land/x/nats@v1.13.0/nats-base-client/semver.ts": "ff3950bb332433d2c6f18ff752cc5943b4eb658e87791a1d1eccf4924e86ac48", 141 | "https://deno.land/x/nats@v1.13.0/nats-base-client/servers.ts": "774abccd926ca3353dfc771ddb16883a4d7094421551be581e83ca4028615918", 142 | "https://deno.land/x/nats@v1.13.0/nats-base-client/service.ts": "626d2ade3a27e8632b6be76d00f13c5509c4cf1271d070718e0f1293d4d864ff", 143 | "https://deno.land/x/nats@v1.13.0/nats-base-client/serviceclient.ts": "bbc99532339cef91e2b32256417d93c4c43248a73ada2c70ea6b3faf83259396", 144 | "https://deno.land/x/nats@v1.13.0/nats-base-client/sha256.js": "87a436302eafacfea9600cf0276245f39d13718c4cbaa81daff47af4ea6523fd", 145 | "https://deno.land/x/nats@v1.13.0/nats-base-client/subscription.ts": "39a5a3b583cc6edfa9e8316ec13e4fb6d0e6ba4c3718f6e8eee513b1fac9300f", 146 | "https://deno.land/x/nats@v1.13.0/nats-base-client/subscriptions.ts": "47c42aba0ab64844b3352d56b0d887b3f785dd581928500f5af7d2d89e7629c9", 147 | "https://deno.land/x/nats@v1.13.0/nats-base-client/transport.ts": "6eb5a2dd507d73e835bb66597809dbdf69f9304c050b1c563cb7806170fd1f82", 148 | "https://deno.land/x/nats@v1.13.0/nats-base-client/typedsub.ts": "7f7363c9de53ee12e607734e6598fb6a49fb94f64f7f6f54406a67fd030490ea", 149 | "https://deno.land/x/nats@v1.13.0/nats-base-client/types.ts": "bdd8f0eeb87d2ea2e315641747ddc0edd98be2c1d993641a10d92264a270dc3d", 150 | "https://deno.land/x/nats@v1.13.0/nats-base-client/util.ts": "acc86328fe7217167182eab2ed9b95d7a456ef5955334c63d6951742b94eca20", 151 | "https://deno.land/x/nats@v1.13.0/src/connect.ts": "117abbed3b942555451e5adf31befd34ee64aa2b12e36688cb11fb9c7e69c221", 152 | "https://deno.land/x/nats@v1.13.0/src/deno_transport.ts": "ec913639b44c9cb6402029339bace77f9f648ceb42efef7f04edb94143059562", 153 | "https://deno.land/x/nats@v1.13.0/src/mod.ts": "4ecf45846e6de9fc772988f22ca8d1ca533561873b91fe37ea6160105d109a9e", 154 | "https://deno.land/x/natsws@v1.14.0/src/connect.ts": "624e9f7f14e195c4bd671b1e6f5ccb5520d877a40ad56f0fe9fe639c7ac51a88", 155 | "https://deno.land/x/natsws@v1.14.0/src/mod.ts": "853c6692bc475827d82b969c0040065ef27058be0831e996ea45eaa263e397bf", 156 | "https://deno.land/x/natsws@v1.14.0/src/ws_transport.ts": "64b1f1365dfc5c1de9100afe8efd521e2ab2427da16ca9f56f2080ddc9f80359", 157 | "https://deno.land/x/plug@1.0.1/deps.ts": "35ea2acd5e3e11846817a429b7ef4bec47b80f2d988f5d63797147134cbd35c2", 158 | "https://deno.land/x/plug@1.0.1/download.ts": "8d6a023ade0806a0653b48cd5f6f8b15fcfaa1dbf2aa1f4bc90fc5732d27b144", 159 | "https://deno.land/x/plug@1.0.1/mod.ts": "5dec80ee7a3a325be45c03439558531bce7707ac118f4376cebbd6740ff24bfb", 160 | "https://deno.land/x/plug@1.0.1/types.ts": "d8eb738fc6ed883e6abf77093442c2f0b71af9090f15c7613621d4039e410ee1", 161 | "https://deno.land/x/plug@1.0.1/util.ts": "5ba8127b9adc36e070b9e22971fb8106869eea1741f452a87b4861e574f13481", 162 | "https://deno.land/x/sqlite3@0.8.1/deno.json": "87910b5c16ccb2f73ac40b45ee6f9a014e5f6317cb7aa568bfe69e2dcca7f975", 163 | "https://deno.land/x/sqlite3@0.8.1/deps.ts": "722c865b9cef27b4cde0bb1ac9ebb08e94c43ad090a7313cea576658ff1e3bb0", 164 | "https://deno.land/x/sqlite3@0.8.1/mod.ts": "d41b8b30e1b20b537ef4d78cae98d90f6bd65c727b64aa1a18bffbb28f7d6ec3", 165 | "https://deno.land/x/sqlite3@0.8.1/src/blob.ts": "3681353b3c97bc43f9b02f8d1c3269c0dc4eb9cb5d3af16c7ce4d1e1ec7507c4", 166 | "https://deno.land/x/sqlite3@0.8.1/src/constants.ts": "85fd27aa6e199093f25f5f437052e16fd0e0870b96ca9b24a98e04ddc8b7d006", 167 | "https://deno.land/x/sqlite3@0.8.1/src/database.ts": "0064992d32196c6a4de0b8d3145d21c0ff34dc7c974b38434e5e01c8e86f4ff5", 168 | "https://deno.land/x/sqlite3@0.8.1/src/ffi.ts": "a983e7c4213e0aeda684248f54c1ad51a39fe1b03b7c3635d50730c651a4e98c", 169 | "https://deno.land/x/sqlite3@0.8.1/src/statement.ts": "09fbb2182507efeff741ae3477202ed7e1f9597a08fda74a1e8ef91bcf990923", 170 | "https://deno.land/x/sqlite3@0.8.1/src/util.ts": "3892904eb057271d4072215c3e7ffe57a9e59e4df78ac575046eb278ca6239cd", 171 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/authenticator.ts": "957275b79259a732d14774e153615512046e511a981550fefb5f58a4c4f113b9", 172 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/base64.ts": "c031bf9f8ac451f2e50c44a3b2f9ff564d61bb14c51834ce73db837e1dee432b", 173 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/bench.ts": "4345f9a0b74867476b91ef4e3fd8228574821fc09dd74715bc187a3c1c517a09", 174 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/codec.ts": "7067df0935473afe5d7e943bd33381d2be768972b7bf306c3b0cd88cdfcfcf26", 175 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/databuffer.ts": "4f92f772ca6701d15f2d148d2d34543558d458b20014d0404f605f84b747b75f", 176 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/denobuffer.ts": "83686bd747953167824694a3f4882ed7134a4fe70b3af89b65b41c284bc0c06a", 177 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/encoders.ts": "af6cb8be55541f934c7048a611e9d1af306093edfe32cd33977f005ebde65d70", 178 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/error.ts": "f00f6c60f9c462415c92652e326ca6c4da2c2edd4d55335a593d50a54b8c8c49", 179 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/headers.ts": "bdc3042cdd89673bf6e1e7ae1d2922f9d5b4c73ae99c08567055f181170dfe63", 180 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/heartbeats.ts": "d5cca54532f4280437263f54288c0cdf5cc17263ed5799db73b7bee6b5549532", 181 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/idleheartbeat.ts": "42c778ccc9de852c162715e3e8bcdabddeae21d038b6ffecf07f257568a1c2da", 182 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/internal_mod.ts": "a5d9a70f98de40e2f98538f81d6609ae2d2ab58e4d4102fbbdf3fb8860d3a230", 183 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/ipparser.ts": "eced7163655be5bb457f9a3a5880239131996e3ae7d7817db88c156faa42d29f", 184 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsbaseclient_api.ts": "ea0eb1e4ef1f39027303c0cddc6ab2daad529029e5f9570e549b99c7440781b6", 185 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsclient.ts": "e3435c06a45cdc37ce0eddb0438c0262b4a82cc7ea0bf707b8e9d8cf9ac2fb35", 186 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsconsumeropts.ts": "95ea16f67ded03e7ecd5f302ac3f0b710c1e1f88d832b030f139b36be95e9f0c", 187 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jslister.ts": "f7e360f75ad77813c043aa3ab96e17bf9c2711f20709bacadd0402fbfbf70004", 188 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsm.ts": "89087bcd6da1d3a2207055ffe76063ba70e1eebee2fb0f1f0322b68c823ca39a", 189 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsmconsumer_api.ts": "7178b335856a16c34360cd5120e8031eeeeeff9c92c204e13536dc5345c8ca08", 190 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsmdirect_api.ts": "ffa7e3c880f89c8273cbb4fa2a12ac1f96312d1a1a47a59d6a0fcdf296e0dd98", 191 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsmsg.ts": "920b6663c1c52ac02fa50ad80024b1f364c25b529e826a22d92e339262f5c5a8", 192 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsmstream_api.ts": "350ce3f1cca5f0fb2525c247d4a69ce308ae4dde17f9877d422043b053665ec9", 193 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/jsutil.ts": "fdfedddb046393f7c0c688c3c0b39eac0906288800740444b0f523a598f66fe9", 194 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/kv.ts": "82240e37be2ae36e573f677c4d6d478cf4cb16b558f4389b387c4b4ad09b2e8a", 195 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/mod.ts": "21843a44e5c0db2b9014e6408b251d43563e06df73315214bf24efeb7967ec8a", 196 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/msg.ts": "a5f7a546d7753d05377c14d192ad9b518be6d15fb02d7b679e723d05f7218719", 197 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/muxsubscription.ts": "aba292629719f8e0a2dac597292e7a74407840a0049dbd044134773d9fd49eac", 198 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/nats.ts": "59456c645d2603fb5aa8ba5536df170625b6861ac40fddd8c351cf5e54ed26a9", 199 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/nkeys.ts": "01aa92908831dd3baad8dbc0e1b3eb05c7b8532a96f799244ff5da16aaee8e69", 200 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/nuid.ts": "f9b0a944338f6b5cf44dd652d22016acdc59ab6fbdaef6cdffa4bae633f2e0f5", 201 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/objectstore.ts": "3b85fe3d6b86f6b3795db5541b047beb510e0434bf1562a5e9d81c4bcbfcb01a", 202 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/options.ts": "08dbf25c5a8f858758e3799ea1dc806f1164b1da206e6a234ab128dc7c07463a", 203 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/parser.ts": "1fb5b9cd07be194ff8c93ee531e04e0b1d03da76001f3268e87599cc1edabe1b", 204 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/protocol.ts": "64fca11cdf72bac9e7ef0cfb1547211c1d5be61b170d284076e4275d8c9f64ed", 205 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/queued_iterator.ts": "0d92da9f22d3849936f69e060256558eaf04269cc26d9e3741d5cd0769392f6d", 206 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/request.ts": "4166bb803a5d82589f5b7bc7b45bfb2c5647fe32adff86ff88aa85280bd64811", 207 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/semver.ts": "ff3950bb332433d2c6f18ff752cc5943b4eb658e87791a1d1eccf4924e86ac48", 208 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/servers.ts": "774abccd926ca3353dfc771ddb16883a4d7094421551be581e83ca4028615918", 209 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/service.ts": "626d2ade3a27e8632b6be76d00f13c5509c4cf1271d070718e0f1293d4d864ff", 210 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/serviceclient.ts": "bbc99532339cef91e2b32256417d93c4c43248a73ada2c70ea6b3faf83259396", 211 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/sha256.js": "87a436302eafacfea9600cf0276245f39d13718c4cbaa81daff47af4ea6523fd", 212 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/subscription.ts": "39a5a3b583cc6edfa9e8316ec13e4fb6d0e6ba4c3718f6e8eee513b1fac9300f", 213 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/subscriptions.ts": "47c42aba0ab64844b3352d56b0d887b3f785dd581928500f5af7d2d89e7629c9", 214 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/transport.ts": "6eb5a2dd507d73e835bb66597809dbdf69f9304c050b1c563cb7806170fd1f82", 215 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/typedsub.ts": "7f7363c9de53ee12e607734e6598fb6a49fb94f64f7f6f54406a67fd030490ea", 216 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/types.ts": "bdd8f0eeb87d2ea2e315641747ddc0edd98be2c1d993641a10d92264a270dc3d", 217 | "https://raw.githubusercontent.com/nats-io/nats.deno/v1.13.0/nats-base-client/util.ts": "acc86328fe7217167182eab2ed9b95d7a456ef5955334c63d6951742b94eca20", 218 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/modules/esm/deps.ts": "38d8dcfc3ca0b7429532477127719affa0da04824610e2a1069f9f935032894f", 219 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/modules/esm/mod.ts": "43c3fbbad0244dde7ae229e778dbefba7f28955a7ff5257d4ed1635b5576fb31", 220 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/modules/esm/nacl.js": "251e4ffcd6d2c56c7773bcf8c1549e9c07b32adb7a2156908a04930a52e614de", 221 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/base32.ts": "07cbfd2551cf1ef4e45784058f7583495354a167c17a75b6cbdbf6b11d91c235", 222 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/codec.ts": "23a574d4c7e4059c503992d6cb8ab0aa5c518f52848c4599ce04049a75f8e8ee", 223 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/crc16.ts": "0290d486579668a79602cef211a28038630e685147e8ddf79b32ff41a871cfdd", 224 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/helper.ts": "8c4121b879ac5d0d4e05b227e15a7e29057642282f020d49de36ca3c33b7d1ed", 225 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/kp.ts": "360259ef2bea8ee65c8fce04efb4b5aaff14382d155c0668c131d959ef9098bb", 226 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/mod.ts": "467181195143688f969a8d1f106e4d5064116d4736a77347f681c2a6c010ede4", 227 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/nkeys.ts": "ef6d234c2661598f39dd30f68b83be1ba84e6f89b851ffaabfad5128917cd8f3", 228 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/public.ts": "b5c893dad5ec139296bcaec6ea11ab4cb06da4f6978e05ff9c37f7d17a7c6101", 229 | "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/src/util.ts": "96c4c8f707ef6aaa00c23527d0a7b935efe1df3dd94e6d40707e37c5e0f14e27" 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "https://deno.land/std@0.168.0/flags/mod.ts"; 2 | 3 | const flags = parse(Deno.args, { 4 | boolean: ["help"], 5 | string: [ 6 | "nats-host", 7 | "creds", 8 | "token", 9 | "data-dir", 10 | "external-backup", 11 | "external-backup-url", 12 | ], 13 | alias: { h: "help" }, 14 | stopEarly: true, 15 | default: { 16 | help: false, 17 | "nats-host": "", 18 | creds: "", 19 | token: "", 20 | "data-dir": ".nqlite-data", 21 | "external-backup": "", 22 | "external-backup-url": "", 23 | }, 24 | }); 25 | 26 | const showHelp = () => { 27 | console.log("Usage: ./nqlite [options]"); 28 | console.log(" --help, -h: Show this help"); 29 | console.log( 30 | " --nats-host: NATS host e.g 'nats://localhost:4222' || 'ws://localhost:8080' (required)", 31 | ); 32 | console.log( 33 | " --token: NATS authentication token (required if --creds is not provided)", 34 | ); 35 | console.log( 36 | " --creds: NATS credentials file (required if --token is not provided)", 37 | ); 38 | console.log(" --data-dir: Data directory (default: '.nqlite-data/')"); 39 | console.log( 40 | " --external-backup: External backup/restore method (option: 'http')", 41 | ); 42 | console.log( 43 | " --external-backup-url: The HTTP url for backup/restore (only required if --external-backup is provided)", 44 | ); 45 | Deno.exit(0); 46 | }; 47 | 48 | if (flags.help) showHelp(); 49 | 50 | // If no credentials or token are provided, proceed without authentication 51 | if (!flags.creds && !flags.token) { 52 | console.log( 53 | "Warning: no --creds or --token provided. Proceeding without authentication", 54 | ); 55 | } 56 | 57 | // If both credentials and token are provided, exit 58 | if (flags.creds && flags.token) { 59 | console.log( 60 | "Error: both --creds and --token provided. Please provide only one", 61 | ); 62 | showHelp(); 63 | } 64 | 65 | // Make sure nats-host is provided 66 | if (!flags["nats-host"]) { 67 | console.log("Error: --nats-host is required"); 68 | showHelp(); 69 | } 70 | 71 | // Check if external backup is provided, and if so, make sure the url is provided 72 | if (flags["external-backup"] && !flags["external-backup-url"]) { 73 | console.log("Error: --external-backup-url is required"); 74 | showHelp(); 75 | } 76 | 77 | // Make sure only allowed external backup methods are provided 78 | if (flags["external-backup"] && flags["external-backup"] !== "http") { 79 | console.log("Error: --external-backup only supports 'http'"); 80 | showHelp(); 81 | } 82 | 83 | import { Nqlite, Options } from "./mod.ts"; 84 | 85 | // Startup nqlite 86 | const nqlite = new Nqlite(); 87 | 88 | const opts: Options = { 89 | url: flags["nats-host"], 90 | creds: flags["creds"], 91 | token: flags["token"], 92 | dataDir: flags["data-dir"], 93 | externalBackup: flags["external-backup"], 94 | externalBackupUrl: flags["external-backup-url"], 95 | }; 96 | 97 | await nqlite.init(opts); 98 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Codec, 3 | consumerOpts, 4 | createInbox, 5 | JetStreamClient, 6 | JetStreamManager, 7 | JetStreamSubscription, 8 | NatsConnection, 9 | ObjectStore, 10 | PubAck, 11 | StringCodec, 12 | } from "nats"; 13 | import { serve } from "serve"; 14 | import { Context, Hono } from "hono"; 15 | import { 16 | bootstrapDataDir, 17 | httpBackup, 18 | httpRestore, 19 | restore, 20 | setupDb, 21 | setupNats, 22 | sigHandler, 23 | snapshot, 24 | snapshotCheck, 25 | } from "./util.ts"; 26 | import { Database } from "sqlite3"; 27 | import { NatsRes, Options, ParseRes, Res } from "./types.ts"; 28 | import { parse } from "./parse.ts"; 29 | 30 | export class Nqlite { 31 | dataDir!: string; 32 | dbFile!: string; 33 | sc: Codec = StringCodec(); 34 | app: string; 35 | nc!: NatsConnection; 36 | js!: JetStreamClient; 37 | db!: Database; 38 | os!: ObjectStore; 39 | subject: string; 40 | sub!: JetStreamSubscription; 41 | snapInterval: number; 42 | snapThreshold: number; 43 | jsm!: JetStreamManager; 44 | inSnapShot: boolean; 45 | externalBackup!: string; 46 | externalBackupUrl!: string; 47 | 48 | // Create a constructor 49 | constructor() { 50 | this.app = "nqlite"; 51 | this.subject = `${this.app}.push`; 52 | this.snapInterval = 2; 53 | this.snapThreshold = 1024; 54 | this.inSnapShot = false; 55 | } 56 | 57 | // Init function to connect to NATS 58 | async init(opts: Options): Promise { 59 | const { url, creds, token, dataDir, externalBackup, externalBackupUrl } = 60 | opts; 61 | 62 | this.dataDir = `${dataDir}/${this.app}`; 63 | this.dbFile = `${this.dataDir}/nqlite.db`; 64 | this.externalBackup = externalBackup; 65 | this.externalBackupUrl = externalBackupUrl; 66 | 67 | // Bootstrap the dataDir 68 | await bootstrapDataDir(this.dataDir); 69 | 70 | // Initialize NATS 71 | const res: NatsRes = await setupNats({ url, app: this.app, creds, token }); 72 | ({ nc: this.nc, js: this.js, os: this.os, jsm: this.jsm } = res); 73 | 74 | // Restore from snapshot if exists 75 | const restoreRes = await restore(this.os, this.dbFile); 76 | if (!restoreRes) { 77 | // Restore from external backup 78 | if (externalBackup === "http") { 79 | await httpRestore(this.dbFile, externalBackupUrl); 80 | } 81 | } 82 | 83 | // Setup to the database 84 | this.db = setupDb(this.dbFile); 85 | 86 | // Setup the API 87 | this.http(); 88 | 89 | // Start snapshot poller 90 | this.snapshotPoller(); 91 | 92 | // Start iterating over the messages in the stream 93 | await this.consumer(); 94 | 95 | // NATS Reconnect listener 96 | this.ncListener(); 97 | 98 | // Handle SIGINT 99 | Deno.addSignalListener( 100 | "SIGINT", 101 | () => sigHandler(this.inSnapShot, this.sub, this.db), 102 | ); 103 | } 104 | 105 | // Get the latest sequence number 106 | getSeq(): number { 107 | const stmt = this.db.prepare(`SELECT seq FROM _nqlite_ where id = 1`); 108 | const seq = stmt.get()!.seq; 109 | stmt.finalize(); 110 | return seq as number; 111 | } 112 | 113 | // Set the latest sequence number 114 | setSeq(seq: number): void { 115 | this.db.prepare(`UPDATE _nqlite_ SET seq = ? where id = 1`).run(seq); 116 | } 117 | 118 | // Execute a statement 119 | execute(s: ParseRes): Res { 120 | const res: Res = { results: [{}], time: 0 }; 121 | 122 | // Check for error 123 | if (s.error) { 124 | res.results[0].error = s.error; 125 | res.time = performance.now() - s.t; 126 | return res; 127 | } 128 | 129 | // Check for simple bulk query 130 | if (s.bulkItems.length && s.simple) { 131 | for (const p of s.bulkItems) this.db.prepare(p).run(); 132 | res.time = performance.now() - s.t; 133 | res.results[0].last_insert_id = this.db.lastInsertRowId; 134 | return res; 135 | } 136 | 137 | // Check for bulk paramaterized/named query 138 | if (s.bulkParams.length) { 139 | for (const p of s.bulkParams) this.db.prepare(p.query).run(...p.params); 140 | res.results[0].last_insert_id = this.db.lastInsertRowId; 141 | res.time = performance.now() - s.t; 142 | return res; 143 | } 144 | 145 | const stmt = this.db.prepare(s.query); 146 | 147 | // If this is a read statement set the last last_insert_id and rows_affected 148 | if (s.isRead) { 149 | res.results[0].rows = s.simple ? stmt.all() : stmt.all(...s.params); 150 | res.time = performance.now() - s.t; 151 | stmt.finalize(); 152 | return res; 153 | } 154 | 155 | // Must not be a read statement 156 | res.results[0].rows_affected = s.simple 157 | ? stmt.run() 158 | : stmt.run(...s.params); 159 | res.results[0].last_insert_id = this.db.lastInsertRowId; 160 | res.time = performance.now() - s.t; 161 | return res; 162 | } 163 | 164 | // Setup reconnect listener 165 | async ncListener(): Promise { 166 | for await (const s of this.nc.status()) { 167 | console.log(`[NATS]: ${s.type} -> ${s.data}`); 168 | } 169 | } 170 | 171 | // Setup ephemeral consumer 172 | async consumer(): Promise { 173 | // Get the latest sequence number 174 | const seq = this.getSeq() + 1; 175 | 176 | console.log("Attempt start sequence ->", seq); 177 | 178 | const opts = consumerOpts().manualAck().ackExplicit().maxAckPending(10) 179 | .deliverTo(createInbox()).startSequence(seq).idleHeartbeat(10000); 180 | 181 | // Get the latest sequence number in the stream and subscribe if possible 182 | try { 183 | const s = await this.jsm.streams.info(this.app); 184 | 185 | // If s.state.last_seq is greater than seq + snapThreshold * 2 we are too far behind and we need to die 186 | // const snapDouble = this.snapThreshold * 2; 187 | // if (s.state.last_seq > seq + snapDouble) { 188 | // console.log( 189 | // `Too far behind to catch up: ${s.state.last_seq} > ${seq} + ${snapDouble}`, 190 | // ); 191 | // Deno.exit(1); 192 | // } 193 | 194 | console.log("Catching up to last seq ->", s.state.last_seq); 195 | 196 | this.sub = await this.js.subscribe(this.subject, opts); 197 | this.iterator(this.sub, s.state.last_seq); 198 | } catch (e) { 199 | console.log("Error getting stream info/subscribing", e.message); 200 | // Add a small backoff 201 | await new Promise((resolve) => setTimeout(resolve, 1000)); 202 | return this.consumer(); 203 | } 204 | } 205 | 206 | // Publish a message to NATS 207 | async publish(s: ParseRes): Promise { 208 | const res: Res = { results: [{}], time: 0 }; 209 | 210 | // Check for error 211 | if (s.error) { 212 | res.error = s.error; 213 | res.time = performance.now() - s.t; 214 | return res; 215 | } 216 | 217 | // Publish the message 218 | const pub: PubAck = await this.js.publish( 219 | this.subject, 220 | this.sc.encode(JSON.stringify(s.data)), 221 | ); 222 | res.results[0].nats = pub; 223 | res.time = performance.now() - s.t; 224 | return res; 225 | } 226 | 227 | // Handle NATS push consumer messages 228 | async iterator(sub: JetStreamSubscription, lastSeq: number) { 229 | try { 230 | for await (const m of sub) { 231 | const data = JSON.parse(this.sc.decode(m.data)); 232 | 233 | try { 234 | const res = parse(data, performance.now()); 235 | 236 | // Handle errors 237 | if (res.error) { 238 | console.log("Parse error:", res.error); 239 | m.ack(); 240 | this.setSeq(m.seq); 241 | continue; 242 | } 243 | 244 | this.execute(res); 245 | } catch (e) { 246 | console.log("Execute error: ", e.message, "Query: ", data); 247 | } 248 | 249 | m.ack(); 250 | this.setSeq(m.seq); 251 | 252 | // Check for last sequence 253 | if (lastSeq && m.seq === lastSeq) { 254 | console.log("Caught up to last seq ->", lastSeq); 255 | } 256 | } 257 | } catch (e) { 258 | console.log("Iterator error: ", e.message); 259 | await this.consumer(); 260 | } 261 | } 262 | 263 | // Snapshot poller 264 | async snapshotPoller() { 265 | console.log("Starting snapshot poller"); 266 | while (true) { 267 | this.inSnapShot = false; 268 | // Wait for the interval to pass 269 | await new Promise((resolve) => 270 | setTimeout(resolve, this.snapInterval * 60 * 60 * 1000) 271 | ); 272 | 273 | // Now wait for a random amount of time between 1 and 5 minutes 274 | await new Promise((resolve) => 275 | setTimeout(resolve, Math.random() * 5 * 60 * 1000) 276 | ); 277 | 278 | this.inSnapShot = true; 279 | 280 | try { 281 | // Unsubscribe from the stream so we stop receiving db updates 282 | console.log("Drained subscription..."); 283 | await this.sub.drain(); 284 | await this.sub.destroy(); 285 | 286 | // VACUUM the database to free up space 287 | console.log("VACUUM..."); 288 | this.db.exec("VACUUM"); 289 | 290 | // Check if we should run a snapshot 291 | const run = await snapshotCheck( 292 | this.os, 293 | this.getSeq(), 294 | this.snapThreshold, 295 | ); 296 | if (!run) { 297 | await this.consumer(); 298 | continue; 299 | } 300 | 301 | // Snapshot the database to object store and/or external storage 302 | let seq = this.getSeq(); 303 | if (await snapshot(this.os, this.dbFile)) { 304 | // Purge previous messages from the stream older than seq - snapThreshold 305 | seq = seq - this.snapThreshold; 306 | await this.jsm.streams.purge(this.app, { 307 | filter: this.subject, 308 | seq: seq < 0 ? 0 : seq, 309 | }); 310 | } 311 | 312 | // Attempting to backup the database to external storage 313 | if (this.externalBackup === "http") { 314 | await httpBackup(this.dbFile, this.externalBackupUrl); 315 | } 316 | } catch (e) { 317 | console.log("Error during snapshot polling:", e.message); 318 | } 319 | 320 | // Resubscribe to the stream 321 | console.log(`Subscribing to stream after snapshot attempt`); 322 | await this.consumer(); 323 | } 324 | } 325 | 326 | // Handle API Routing 327 | http() { 328 | const api = new Hono(); 329 | 330 | // GET /health 331 | api.get("/health", (c: Context): Response => { 332 | return c.json({ status: "ok" }); 333 | }); 334 | 335 | // GET /db/query 336 | api.get("/db/query", (c: Context): Response => { 337 | const res: Res = { results: [{}], time: 0 }; 338 | const perf = performance.now(); 339 | 340 | if (!c.req.query("q")) { 341 | res.results[0].error = "Missing query"; 342 | return c.json(res, 400); 343 | } 344 | 345 | const arr = new Array(); 346 | arr.push(c.req.query("q")!); 347 | 348 | // turn arr to JSON type 349 | const r = JSON.stringify(arr); 350 | 351 | try { 352 | const data = parse(JSON.parse(r), perf); 353 | return c.json(this.execute(data)); 354 | } catch (e) { 355 | res.results[0].error = e.message; 356 | return c.json(res, 400); 357 | } 358 | }); 359 | 360 | // POST /db/query 361 | api.post("/db/query", async (c: Context): Promise => { 362 | const res: Res = { results: [{}], time: 0 }; 363 | const perf = performance.now(); 364 | 365 | if (!c.req.body) { 366 | res.results[0].error = "Missing body"; 367 | return c.json(res, 400); 368 | } 369 | 370 | try { 371 | // data should be an array of SQL statements or a multidimensional array of SQL statements 372 | const data = parse(await c.req.json(), perf); 373 | 374 | // Handle errors 375 | if (data.error) { 376 | res.results[0].error = data.error; 377 | return c.json(res, 400); 378 | } 379 | 380 | return data.isRead 381 | ? c.json(this.execute(data)) 382 | : c.json(await this.publish(data)); 383 | } catch (e) { 384 | res.results[0].error = e.message; 385 | return c.json(res, 400); 386 | } 387 | }); 388 | 389 | // Serve the API on port 4001 390 | serve(api.fetch, { port: 4001 }); 391 | } 392 | } 393 | 394 | export type { Options }; 395 | -------------------------------------------------------------------------------- /parse.ts: -------------------------------------------------------------------------------- 1 | import { ParseRes } from "./types.ts"; 2 | 3 | export function parse(data: JSON, t: number): ParseRes { 4 | const res: ParseRes = { 5 | error: "", 6 | simple: true, 7 | query: "", 8 | params: [], 9 | t, 10 | data, 11 | isRead: false, 12 | bulkItems: [], 13 | bulkParams: [], 14 | }; 15 | 16 | // If this is not an array, return error 17 | if (!Array.isArray(data)) { 18 | res.error = "Invalid. Not an array"; 19 | console.log(data); 20 | return res; 21 | } 22 | 23 | // If array is empty, return error 24 | if (!data.length) { 25 | res.error = "Empty array"; 26 | console.log(data); 27 | return res; 28 | } 29 | 30 | // Handle simple query 31 | if (!Array.isArray(data[0])) { 32 | // Check if this is really a simple query 33 | if (data.length === 1) { 34 | res.query = data[0] as string; 35 | res.isRead = isReadQuery(res.query); 36 | return res; 37 | } 38 | 39 | // Must be a bulk query 40 | // Make sure it's not a read query 41 | if (isReadBulk(data)) { 42 | res.error = "Invalid Bulk. SELECT query in bulk request"; 43 | console.log(data); 44 | return res; 45 | } 46 | 47 | // Make sure data is an array of strings 48 | if (data.find((d) => typeof d !== "string")) { 49 | res.error = "Invalid Bulk. Not an array of strings"; 50 | console.log(data); 51 | return res; 52 | } 53 | 54 | res.bulkItems = data; 55 | return res; 56 | } 57 | 58 | // Check for array more than 2 levels deep 59 | if (Array.isArray(data[0][0])) { 60 | res.error = 61 | "Invalid Paramaratized/Named Statement. Array more than 2 levels deep"; 62 | console.log(data); 63 | return res; 64 | } 65 | 66 | // At this point, we know it's a paramarized/named statement 67 | res.simple = false; 68 | 69 | // Check for bulk paramarized/named statements (second array is an array) 70 | if (data.length > 1 && Array.isArray(data[1])) { 71 | // Build the bulkItems array 72 | for (const i of data) { 73 | const paramRes = paramQueryRes(i); 74 | const { error, query, params, isRead } = paramRes; 75 | 76 | // If error in paramarized/named statement, return error 77 | if (error) { 78 | res.error = error; 79 | console.log(data); 80 | return res; 81 | } 82 | 83 | // If this is a read query, return error 84 | if (isRead) { 85 | res.error = "Invalid Bulk. SELECT query in bulk request"; 86 | console.log(data); 87 | return res; 88 | } 89 | 90 | res.bulkParams.push({ query, params }); 91 | } 92 | 93 | return res; 94 | } 95 | 96 | // Must be regular (non bulk) paramarized/named statement 97 | const paramRes = paramQueryRes(data[0]); 98 | const { error, query, params, isRead } = paramRes; 99 | 100 | // If error in paramarized/named statement, return error 101 | if (error) { 102 | res.error = error; 103 | console.log(data); 104 | return res; 105 | } 106 | 107 | // Shift the first item off the array as the SQL statement 108 | res.query = query; 109 | res.isRead = isRead; 110 | res.params = params; 111 | return res; 112 | } 113 | 114 | function isReadBulk(data: string[]): boolean { 115 | const found = data.find((q) => isReadQuery(q)); 116 | return found ? true : false; 117 | } 118 | 119 | function isReadQuery(q: string): boolean { 120 | return q.toLowerCase().startsWith("select"); 121 | } 122 | 123 | function paramQueryRes(data: string[]) { 124 | const res = { 125 | error: "", 126 | query: "", 127 | params: [] as string[], 128 | isRead: false, 129 | }; 130 | 131 | // Grab the first item in the array 132 | const params = Array.from(data); 133 | 134 | // If item has fewer than 2 items, return error 135 | if (params.length < 2) { 136 | res.error = "Invalid Paramaratized/Named Statement. Not enough items"; 137 | console.log(params); 138 | return res; 139 | } 140 | 141 | // Shift the first item off the array as the SQL statement 142 | res.query = params.shift() as string; 143 | res.isRead = isReadQuery(res.query); 144 | res.params = params; 145 | return res; 146 | } 147 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Authenticator, 3 | Codec, 4 | JetStreamClient, 5 | JetStreamManager, 6 | NatsConnection, 7 | ObjectStore, 8 | } from "nats"; 9 | import { RestBindParameters } from "sqlite3"; 10 | 11 | export type NatsInit = { 12 | url: string; 13 | app: string; 14 | creds: string; 15 | token: string; 16 | }; 17 | 18 | export type NatsConf = { 19 | servers: string; 20 | authenticator?: Authenticator; 21 | token?: string; 22 | maxReconnectAttempts?: number; 23 | }; 24 | 25 | export type NatsRes = { 26 | nc: NatsConnection; 27 | sc: Codec; 28 | js: JetStreamClient; 29 | os: ObjectStore; 30 | jsm: JetStreamManager; 31 | }; 32 | 33 | export type ParseRes = { 34 | error: string; 35 | simple: boolean; 36 | query: string; 37 | params: RestBindParameters; 38 | t: number; 39 | data: JSON; 40 | isRead: boolean; 41 | bulkItems: string[]; 42 | bulkParams: bulkParams[]; 43 | }; 44 | 45 | type bulkParams = { 46 | query: string; 47 | params: RestBindParameters; 48 | }; 49 | 50 | export type Res = { 51 | error?: string; 52 | results: Array>; 53 | time: number; 54 | }; 55 | 56 | export type Options = { 57 | url: string; 58 | creds: string; 59 | token: string; 60 | dataDir: string; 61 | externalBackup: string; 62 | externalBackupUrl: string; 63 | }; 64 | -------------------------------------------------------------------------------- /util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | connect, 3 | credsAuthenticator, 4 | JetStreamSubscription, 5 | ObjectStore, 6 | StreamInfo, 7 | StringCodec, 8 | } from "nats"; 9 | import { connect as wsConnect } from "natsws"; 10 | import { Database } from "sqlite3"; 11 | import { NatsConf, NatsInit, NatsRes } from "./types.ts"; 12 | 13 | // NATS initialization function 14 | export async function setupNats(conf: NatsInit): Promise { 15 | const { app, creds, token, url } = conf; 16 | 17 | const natsOpts: NatsConf = { servers: url, maxReconnectAttempts: -1 }; 18 | if (token) natsOpts.token = token; 19 | if (creds) { 20 | natsOpts.authenticator = credsAuthenticator(Deno.readFileSync(creds)); 21 | } 22 | 23 | console.log("Connecting to NATS"); 24 | const nc = url.startsWith("ws") 25 | ? await wsConnect(natsOpts) 26 | : await connect(natsOpts); 27 | console.log("Connected to NATS Server:", nc.getServer()); 28 | 29 | // Create a jetstream manager 30 | const jsm = await nc.jetstreamManager(); 31 | const sc = StringCodec(); 32 | 33 | // Get the list of streams 34 | const streams = await jsm.streams.list().next(); 35 | 36 | let stream = streams.find((s: StreamInfo) => s.config.name === app); 37 | 38 | // Create stream if it doesn't exist 39 | if (!stream) { 40 | console.log("Creating stream"); 41 | stream = await jsm.streams.add({ name: app, subjects: [`${app}.*`] }); 42 | 43 | // Try to update the stream to 3 replicas 44 | try { 45 | await jsm.streams.update(app, { num_replicas: 3 }); 46 | } catch (e) { 47 | console.log("Could not update stream to 3 replicas:", e.message); 48 | } 49 | } 50 | 51 | // Create a jetstream client 52 | const js = nc.jetstream(); 53 | 54 | console.log("Creating object store if it don't exist"); 55 | const os = await js.views.os(app); 56 | 57 | // Try to update the object store to 3 replicas 58 | try { 59 | await jsm.streams.update(`OBJ_${app}`, { num_replicas: 3 }); 60 | } catch (e) { 61 | console.log("Could not update object store to 3 replicas:", e.message); 62 | } 63 | 64 | console.log("NATS initialized"); 65 | 66 | return { nc, sc, js, os, jsm }; 67 | } 68 | 69 | export async function bootstrapDataDir(dataDir: string) { 70 | console.log("Bootstrapping data directory:", dataDir); 71 | 72 | try { 73 | await Deno.remove(dataDir, { recursive: true }); 74 | } catch (e) { 75 | console.log(e.message); 76 | } 77 | 78 | try { 79 | await Deno.mkdir(dataDir, { recursive: true }); 80 | } catch (e) { 81 | console.log(e.message); 82 | } 83 | } 84 | 85 | export function setupDb(file: string): Database { 86 | const db = new Database(file); 87 | 88 | db.exec("pragma locking_mode = exclusive"); 89 | db.exec("pragma auto_vacuum = none"); 90 | db.exec("pragma journal_mode = wal"); 91 | db.exec("pragma synchronous = normal"); 92 | db.exec("pragma temp_store = memory"); 93 | 94 | const version = db.prepare("select sqlite_version()").value<[string]>()!; 95 | 96 | console.log(`SQLite version: ${version}`); 97 | 98 | // Create sequence table if it doesn't exist 99 | console.log("Creating sequence table if it doesn't exist"); 100 | db.exec( 101 | `CREATE TABLE IF NOT EXISTS _nqlite_ (id INTEGER PRIMARY KEY, seq NOT NULL)`, 102 | ); 103 | 104 | // Insert the first sequence number if it doesn't exist 105 | db.exec(`INSERT OR IGNORE INTO _nqlite_ (id, seq) VALUES (1,0)`); 106 | 107 | return db; 108 | } 109 | 110 | export async function restore(os: ObjectStore, db: string): Promise { 111 | // See if snapshot exists in object store 112 | const o = await os.get("snapshot"); 113 | 114 | if (!o) { 115 | console.log("No snapshot object to restore"); 116 | return false; 117 | } 118 | 119 | console.log( 120 | `Restoring from snapshot taken: ${o.info.mtime}`, 121 | ); 122 | 123 | // Get the object 124 | await fromReadableStream(o.data, db); 125 | 126 | // Convert bytes to megabytes 127 | const mb = (o.info.size / 1024 / 1024).toFixed(2); 128 | 129 | console.log(`Restored from snapshot: ${mb}Mb`); 130 | return true; 131 | } 132 | 133 | async function fromReadableStream( 134 | rs: ReadableStream, 135 | file: string, 136 | ): Promise { 137 | const reader = rs.getReader(); 138 | while (true) { 139 | const { done, value } = await reader.read(); 140 | if (done) break; 141 | // Add the chunk to the array 142 | if (value && value.length) { 143 | // Write and concat the chunks to the file 144 | await Deno.writeFile(file, value, { append: true }); 145 | } 146 | } 147 | 148 | // Close the reader 149 | reader.releaseLock(); 150 | } 151 | 152 | function readableStreamFrom(data: Uint8Array): ReadableStream { 153 | return new ReadableStream({ 154 | pull(controller) { 155 | // the readable stream adds data 156 | controller.enqueue(data); 157 | controller.close(); 158 | }, 159 | }); 160 | } 161 | 162 | export async function snapshot( 163 | os: ObjectStore, 164 | db: string, 165 | ): Promise { 166 | try { 167 | // Put the sqlite file in the object store 168 | const info = await os.put( 169 | { name: "snapshot" }, 170 | readableStreamFrom(Deno.readFileSync(db)), 171 | ); 172 | 173 | // Convert bytes to megabytes 174 | const mb = (info.size / 1024 / 1024).toFixed(2); 175 | 176 | console.log( 177 | `Snapshot stored in object store: ${mb}Mb`, 178 | ); 179 | return true; 180 | } catch (e) { 181 | console.log("Error during snapshot:", e.message); 182 | return false; 183 | } 184 | } 185 | 186 | export async function snapshotCheck( 187 | os: ObjectStore, 188 | seq: number, 189 | threshold: number, 190 | ): Promise { 191 | console.log( 192 | `Checking if we need to snapshot (seq: ${seq}, threshold: ${threshold})`, 193 | ); 194 | 195 | try { 196 | const snapInfo = await os.info("snapshot"); 197 | 198 | if (!snapInfo) console.log("No snapshot found in object store"); 199 | 200 | // Check if we need to snapshot 201 | if (snapInfo) { 202 | const processed = seq - Number(snapInfo.description); 203 | console.log("Messages processed since last snapshot ->", processed); 204 | if (processed < threshold) { 205 | console.log( 206 | `Skipping snapshot, threshold not met: ${processed} < ${threshold}`, 207 | ); 208 | return false; 209 | } 210 | 211 | // Check if another is in progress or created in the last minute 212 | const now = new Date().getTime(); 213 | const last = new Date(snapInfo.mtime).getTime(); 214 | if (now - last < 60 * 1000) { 215 | const diff = Math.floor((now - last) / 1000); 216 | console.log(`Skipping snapshot, latest snapshot ${diff} seconds ago`); 217 | return false; 218 | } 219 | } 220 | 221 | // Check if no snapshot exists and we are below the threshold 222 | if (!snapInfo && seq < threshold) { 223 | console.log( 224 | `Skipping snapshot, threshold not met: ${seq} < ${threshold}`, 225 | ); 226 | return false; 227 | } 228 | } catch (e) { 229 | console.log("Error during snapshot check:", e.message); 230 | return false; 231 | } 232 | 233 | return true; 234 | } 235 | 236 | export async function httpBackup(db: string, url: string): Promise { 237 | // Backup to HTTP using the fetch API 238 | try { 239 | const res = await fetch(url, { 240 | method: "POST", 241 | body: Deno.readFileSync(db), 242 | }); 243 | console.log("HTTP backup response:", res.status, res.statusText); 244 | if (res.status !== 200) return false; 245 | const mb = (Deno.statSync(db).size / 1024 / 1024).toFixed(2); 246 | console.log(`Snapshot stored via http: ${mb}Mb`); 247 | return true; 248 | } catch (e) { 249 | console.log("Error during http backup:", e.message); 250 | return false; 251 | } 252 | } 253 | 254 | export async function httpRestore(db: string, url: string): Promise { 255 | // Restore from HTTP using the fetch API 256 | try { 257 | const res = await fetch(url); 258 | console.log("HTTP restore response:", res.status, res.statusText); 259 | if (res.status !== 200) return false; 260 | const file = await Deno.open(db, { write: true, create: true }); 261 | await res.body?.pipeTo(file.writable); 262 | const mb = (Deno.statSync(db).size / 1024 / 1024).toFixed(2); 263 | console.log(`Restored from http snapshot: ${mb}Mb`); 264 | return true; 265 | } catch (e) { 266 | console.log("Error during http restore:", e.message); 267 | return false; 268 | } 269 | } 270 | 271 | export async function sigHandler( 272 | inSnap: boolean, 273 | sub: JetStreamSubscription, 274 | db: Database, 275 | ): Promise { 276 | // Check if inSnapShot is true 277 | if (inSnap) { 278 | console.log("SIGINT received while in snapshot. Waiting 10 seconds..."); 279 | await new Promise((resolve) => setTimeout(resolve, 10000)); 280 | } 281 | 282 | console.log("About to die! Draining subscription..."); 283 | await sub.drain(); 284 | await sub.destroy(); 285 | console.log("Closing the database"); 286 | db.close(); 287 | Deno.exit(); 288 | } 289 | --------------------------------------------------------------------------------