├── .env ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum └── idb ├── base_object_store.go ├── cursor.go ├── cursor_test.go ├── db.go ├── db_factory.go ├── db_factory_test.go ├── db_test.go ├── doc.go ├── doc_test.go ├── dom_exception.go ├── dom_exception_test.go ├── events.go ├── index.go ├── index_test.go ├── internal ├── assert │ └── assert.go └── jscache │ ├── cacher.go │ └── strings.go ├── key_range.go ├── key_range_test.go ├── object_store.go ├── object_store_test.go ├── open_db_request.go ├── request.go ├── request_test.go ├── strings.go ├── transaction.go ├── transaction_test.go └── wasm_tags_test.go /.env: -------------------------------------------------------------------------------- 1 | # Env file defines useful environment variables for editors. 2 | GOOS=js 3 | GOARCH=wasm 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.18.x 16 | - name: Lint 17 | run: make lint 18 | 19 | test: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | go: 24 | - 1.18.x 25 | - 1.19.x 26 | name: Test with Go ${{ matrix.go }} 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-go@v3 30 | with: 31 | go-version: ${{ matrix.go }} 32 | check-latest: true 33 | - name: Test 34 | run: make test 35 | - name: Publish Test Coverage 36 | run: | 37 | set -ex 38 | go install github.com/mattn/goveralls@v0.0.11 39 | goveralls -coverprofile="cover.out" -service=github 40 | env: 41 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cover.out 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | # Default linters, plus these: 4 | - exportloopref 5 | - gocognit 6 | - goconst 7 | - gocritic 8 | - gofmt 9 | - gosec 10 | - misspell 11 | - paralleltest 12 | - revive 13 | 14 | run: 15 | build-tags: 16 | - js,wasm 17 | 18 | issues: 19 | exclude: 20 | # Disable scopelint errors on table driven tests 21 | - Using the variable on range scope `tc` in function literal 22 | include: 23 | # Re-enable default excluded rules 24 | - EXC0012 25 | - EXC0013 26 | - EXC0014 27 | - EXC0015 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BROWSERTEST_VERSION = v0.7 2 | LINT_VERSION = 1.50.1 3 | GO_BIN = $(shell printf '%s/bin' "$$(go env GOPATH)") 4 | 5 | .PHONY: all 6 | all: lint test 7 | 8 | .PHONY: lint-deps 9 | lint-deps: 10 | @if ! which golangci-lint >/dev/null || [[ "$$(golangci-lint version 2>&1)" != *${LINT_VERSION}* ]]; then \ 11 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GO_BIN}" v${LINT_VERSION}; \ 12 | fi 13 | @if ! which jsguard >/dev/null; then \ 14 | go install github.com/hack-pad/safejs/jsguard/cmd/jsguard; \ 15 | fi 16 | 17 | .PHONY: lint 18 | lint: lint-deps 19 | GOOS=js GOARCH=wasm "${GO_BIN}/golangci-lint" run 20 | GOOS=js GOARCH=wasm "${GO_BIN}/jsguard" -test=false ./... 21 | 22 | .PHONY: test-deps 23 | test-deps: 24 | @if [ ! -f "${GO_BIN}/go_js_wasm_exec" ]; then \ 25 | set -ex; \ 26 | go install github.com/agnivade/wasmbrowsertest@${BROWSERTEST_VERSION}; \ 27 | ln -s "${GO_BIN}/wasmbrowsertest" "${GO_BIN}/go_js_wasm_exec"; \ 28 | fi 29 | 30 | .PHONY: test 31 | test: test-deps 32 | GOOS=js GOARCH=wasm go test -coverprofile=cover.out ./... 33 | go test -race ./... 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-indexeddb [![Go Reference](https://pkg.go.dev/badge/github.com/hack-pad/go-indexeddb/idb.svg)][reference] [![CI](https://github.com/hack-pad/go-indexeddb/actions/workflows/ci.yml/badge.svg)](https://github.com/hack-pad/go-indexeddb/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/hack-pad/go-indexeddb/badge.svg?branch=main)](https://coveralls.io/github/hack-pad/go-indexeddb?branch=main) 2 | 3 | An IndexedDB driver with bindings for Go code compiled to WebAssembly. 4 | 5 | Package `idb` is a low-level Go driver that provides type-safe bindings to IndexedDB in Wasm programs. 6 | The primary focus is to align with the IndexedDB spec, followed by ease of use. 7 | 8 | To get started, get the global indexedDB instance with idb.Global(). See the [reference][] for examples and full documentation. 9 | 10 | ```bash 11 | go get github.com/hack-pad/go-indexeddb@latest 12 | ``` 13 | ```go 14 | import "github.com/hack-pad/go-indexeddb/idb" 15 | ``` 16 | 17 | [reference]: https://pkg.go.dev/github.com/hack-pad/go-indexeddb/idb 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hack-pad/go-indexeddb 2 | 3 | go 1.18 4 | 5 | require github.com/hack-pad/safejs v0.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= 2 | github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= 3 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 4 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 5 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= 6 | -------------------------------------------------------------------------------- /idb/base_object_store.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "github.com/hack-pad/safejs" 8 | ) 9 | 10 | // baseObjectStore is the common implementation for both object stores and indexes. 11 | type baseObjectStore struct { 12 | txn *Transaction 13 | jsObjectStore safejs.Value 14 | } 15 | 16 | func wrapBaseObjectStore(txn *Transaction, jsObjectStore safejs.Value) *baseObjectStore { 17 | if txn == nil { 18 | txn = (*Transaction)(nil) 19 | } 20 | return &baseObjectStore{ 21 | txn: txn, 22 | jsObjectStore: jsObjectStore, 23 | } 24 | } 25 | 26 | // Count returns a UintRequest, and, in a separate thread, returns the total number of records in the store or index. 27 | func (b *baseObjectStore) Count() (*UintRequest, error) { 28 | reqValue, err := b.jsObjectStore.Call("count") 29 | if err != nil { 30 | return nil, tryAsDOMException(err) 31 | } 32 | req := wrapRequest(b.txn, reqValue) 33 | return newUintRequest(req), nil 34 | } 35 | 36 | // CountKey returns a UintRequest, and, in a separate thread, returns the total number of records that match the provided key. 37 | func (b *baseObjectStore) CountKey(key safejs.Value) (*UintRequest, error) { 38 | reqValue, err := b.jsObjectStore.Call("count", key) 39 | if err != nil { 40 | return nil, tryAsDOMException(err) 41 | } 42 | req := wrapRequest(b.txn, reqValue) 43 | return newUintRequest(req), nil 44 | } 45 | 46 | // CountRange returns a UintRequest, and, in a separate thread, returns the total number of records that match the provided KeyRange. 47 | func (b *baseObjectStore) CountRange(keyRange *KeyRange) (*UintRequest, error) { 48 | reqValue, err := b.jsObjectStore.Call("count", keyRange.jsKeyRange) 49 | if err != nil { 50 | return nil, tryAsDOMException(err) 51 | } 52 | req := wrapRequest(b.txn, reqValue) 53 | return newUintRequest(req), nil 54 | } 55 | 56 | // GetAllKeys returns an ArrayRequest that retrieves record keys for all objects in the object store or index. 57 | func (b *baseObjectStore) GetAllKeys() (*ArrayRequest, error) { 58 | reqValue, err := b.jsObjectStore.Call("getAllKeys") 59 | if err != nil { 60 | return nil, tryAsDOMException(err) 61 | } 62 | req := wrapRequest(b.txn, reqValue) 63 | return newArrayRequest(req), nil 64 | } 65 | 66 | // GetAllKeysRange returns an ArrayRequest that retrieves record keys for all objects in the object store or index matching the specified query. If maxCount is 0, retrieves all objects matching the query. 67 | func (b *baseObjectStore) GetAllKeysRange(query *KeyRange, maxCount uint) (*ArrayRequest, error) { 68 | args := []interface{}{query.jsKeyRange} 69 | if maxCount > 0 { 70 | args = append(args, maxCount) 71 | } 72 | reqValue, err := b.jsObjectStore.Call("getAllKeys", args...) 73 | if err != nil { 74 | return nil, tryAsDOMException(err) 75 | } 76 | req := wrapRequest(b.txn, reqValue) 77 | return newArrayRequest(req), nil 78 | } 79 | 80 | // Get returns a Request, and, in a separate thread, returns the objects selected by the specified key. This is for retrieving specific records from an object store or index. 81 | func (b *baseObjectStore) Get(key safejs.Value) (*Request, error) { 82 | reqValue, err := b.jsObjectStore.Call("get", key) 83 | if err != nil { 84 | return nil, tryAsDOMException(err) 85 | } 86 | return wrapRequest(b.txn, reqValue), nil 87 | } 88 | 89 | // GetKey returns a Request, and, in a separate thread retrieves and returns the record key for the object matching the specified parameter. 90 | func (b *baseObjectStore) GetKey(value safejs.Value) (*Request, error) { 91 | reqValue, err := b.jsObjectStore.Call("getKey", value) 92 | if err != nil { 93 | return nil, tryAsDOMException(err) 94 | } 95 | return wrapRequest(b.txn, reqValue), nil 96 | } 97 | 98 | // OpenCursor returns a CursorWithValueRequest, and, in a separate thread, returns a new CursorWithValue. Used for iterating through an object store or index by primary key with a cursor. 99 | func (b *baseObjectStore) OpenCursor(direction CursorDirection) (*CursorWithValueRequest, error) { 100 | reqValue, err := b.jsObjectStore.Call("openCursor", safejs.Null(), direction.jsValue()) 101 | if err != nil { 102 | return nil, tryAsDOMException(err) 103 | } 104 | req := wrapRequest(b.txn, reqValue) 105 | return newCursorWithValueRequest(req), nil 106 | } 107 | 108 | // OpenCursorKey is the same as OpenCursor, but opens a cursor over the given key instead. 109 | func (b *baseObjectStore) OpenCursorKey(key safejs.Value, direction CursorDirection) (*CursorWithValueRequest, error) { 110 | reqValue, err := b.jsObjectStore.Call("openCursor", key, direction.jsValue()) 111 | if err != nil { 112 | return nil, tryAsDOMException(err) 113 | } 114 | req := wrapRequest(b.txn, reqValue) 115 | return newCursorWithValueRequest(req), nil 116 | } 117 | 118 | // OpenCursorRange is the same as OpenCursor, but opens a cursor over the given range instead. 119 | func (b *baseObjectStore) OpenCursorRange(keyRange *KeyRange, direction CursorDirection) (*CursorWithValueRequest, error) { 120 | reqValue, err := b.jsObjectStore.Call("openCursor", keyRange.jsKeyRange, direction.jsValue()) 121 | if err != nil { 122 | return nil, tryAsDOMException(err) 123 | } 124 | req := wrapRequest(b.txn, reqValue) 125 | return newCursorWithValueRequest(req), nil 126 | } 127 | 128 | // OpenKeyCursor returns a CursorRequest, and, in a separate thread, returns a new Cursor. Used for iterating through all keys in an object store or index. 129 | func (b *baseObjectStore) OpenKeyCursor(direction CursorDirection) (*CursorRequest, error) { 130 | reqValue, err := b.jsObjectStore.Call("openKeyCursor", safejs.Null(), direction.jsValue()) 131 | if err != nil { 132 | return nil, tryAsDOMException(err) 133 | } 134 | req := wrapRequest(b.txn, reqValue) 135 | return newCursorRequest(req), nil 136 | } 137 | 138 | // OpenKeyCursorKey is the same as OpenKeyCursor, but opens a cursor over the given key instead. 139 | func (b *baseObjectStore) OpenKeyCursorKey(key safejs.Value, direction CursorDirection) (*CursorRequest, error) { 140 | reqValue, err := b.jsObjectStore.Call("openKeyCursor", key, direction.jsValue()) 141 | if err != nil { 142 | return nil, tryAsDOMException(err) 143 | } 144 | req := wrapRequest(b.txn, reqValue) 145 | return newCursorRequest(req), nil 146 | } 147 | 148 | // OpenKeyCursorRange is the same as OpenKeyCursor, but opens a cursor over the given key range instead. 149 | func (b *baseObjectStore) OpenKeyCursorRange(keyRange *KeyRange, direction CursorDirection) (*CursorRequest, error) { 150 | reqValue, err := b.jsObjectStore.Call("openKeyCursor", keyRange.jsKeyRange, direction.jsValue()) 151 | if err != nil { 152 | return nil, tryAsDOMException(err) 153 | } 154 | req := wrapRequest(b.txn, reqValue) 155 | return newCursorRequest(req), nil 156 | } 157 | -------------------------------------------------------------------------------- /idb/cursor.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | 9 | "github.com/hack-pad/go-indexeddb/idb/internal/jscache" 10 | "github.com/hack-pad/safejs" 11 | ) 12 | 13 | var ( 14 | jsObjectStore safejs.Value 15 | cursorDirectionCache jscache.Strings 16 | ) 17 | 18 | func init() { 19 | var err error 20 | jsObjectStore, err = safejs.Global().Get("IDBObjectStore") 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | // CursorDirection is the direction of traversal of the cursor 27 | type CursorDirection int 28 | 29 | const ( 30 | // CursorNext direction causes the cursor to be opened at the start of the source. 31 | CursorNext CursorDirection = iota 32 | // CursorNextUnique direction causes the cursor to be opened at the start of the source. For every key with duplicate values, only the first record is yielded. 33 | CursorNextUnique 34 | // CursorPrevious direction causes the cursor to be opened at the end of the source. 35 | CursorPrevious 36 | // CursorPreviousUnique direction causes the cursor to be opened at the end of the source. For every key with duplicate values, only the first record is yielded. 37 | CursorPreviousUnique 38 | ) 39 | 40 | func parseCursorDirection(s string) CursorDirection { 41 | switch s { 42 | case "nextunique": 43 | return CursorNextUnique 44 | case "prev": 45 | return CursorPrevious 46 | case "prevunique": 47 | return CursorPreviousUnique 48 | default: 49 | return CursorNext 50 | } 51 | } 52 | 53 | func (d CursorDirection) String() string { 54 | switch d { 55 | case CursorNextUnique: 56 | return "nextunique" 57 | case CursorPrevious: 58 | return "prev" 59 | case CursorPreviousUnique: 60 | return "prevunique" 61 | default: 62 | return "next" 63 | } 64 | } 65 | 66 | func (d CursorDirection) jsValue() safejs.Value { 67 | return cursorDirectionCache.Value(d.String()) 68 | } 69 | 70 | // Cursor represents a cursor for traversing or iterating over multiple records in a Database 71 | type Cursor struct { 72 | txn *Transaction 73 | jsCursor safejs.Value 74 | iterated bool // set to true when an iteration method is called, like Continue 75 | } 76 | 77 | func wrapCursor(txn *Transaction, jsCursor safejs.Value) *Cursor { 78 | if txn == nil { 79 | txn = (*Transaction)(nil) 80 | } 81 | return &Cursor{ 82 | txn: txn, 83 | jsCursor: jsCursor, 84 | } 85 | } 86 | 87 | // Source returns the ObjectStore or Index that the cursor is iterating 88 | func (c *Cursor) Source() (objectStore *ObjectStore, index *Index, err error) { 89 | jsSource, err := c.jsCursor.Get("source") 90 | if err != nil { 91 | return 92 | } 93 | if isInstance, _ := jsSource.InstanceOf(jsObjectStore); isInstance { 94 | objectStore = wrapObjectStore(c.txn, jsSource) 95 | } else if isInstance, _ := jsSource.InstanceOf(jsIDBIndex); isInstance { 96 | index = wrapIndex(c.txn, jsSource) 97 | } 98 | return 99 | } 100 | 101 | // Direction returns the direction of traversal of the cursor 102 | func (c *Cursor) Direction() (CursorDirection, error) { 103 | direction, err := c.jsCursor.Get("direction") 104 | if err != nil { 105 | return 0, err 106 | } 107 | directionStr, err := direction.String() 108 | return parseCursorDirection(directionStr), err 109 | } 110 | 111 | // Key returns the key for the record at the cursor's position. If the cursor is outside its range, this is set to undefined. 112 | func (c *Cursor) Key() (js.Value, error) { 113 | value, err := c.jsCursor.Get("key") 114 | return safejs.Unsafe(value), err 115 | } 116 | 117 | // PrimaryKey returns the cursor's current effective primary key. If the cursor is currently being iterated or has iterated outside its range, this is set to undefined. 118 | func (c *Cursor) PrimaryKey() (js.Value, error) { 119 | value, err := c.jsCursor.Get("primaryKey") 120 | return safejs.Unsafe(value), err 121 | } 122 | 123 | // Request returns the Request that was used to obtain the cursor. 124 | func (c *Cursor) Request() (*Request, error) { 125 | reqValue, err := c.jsCursor.Get("request") 126 | if err != nil { 127 | return nil, err 128 | } 129 | return wrapRequest(c.txn, reqValue), nil 130 | } 131 | 132 | // Advance sets the number of times a cursor should move its position forward. 133 | func (c *Cursor) Advance(count uint) error { 134 | c.iterated = true 135 | _, err := c.jsCursor.Call("advance", count) 136 | return tryAsDOMException(err) 137 | } 138 | 139 | // Continue advances the cursor to the next position along its direction. 140 | func (c *Cursor) Continue() error { 141 | c.iterated = true 142 | _, err := c.jsCursor.Call("continue") 143 | return tryAsDOMException(err) 144 | } 145 | 146 | // ContinueKey advances the cursor to the next position along its direction. 147 | func (c *Cursor) ContinueKey(key js.Value) error { 148 | c.iterated = true 149 | _, err := c.jsCursor.Call("continue", key) 150 | return tryAsDOMException(err) 151 | } 152 | 153 | // ContinuePrimaryKey sets the cursor to the given index key and primary key given as arguments. Returns an error if the source is not an index. 154 | func (c *Cursor) ContinuePrimaryKey(key, primaryKey js.Value) error { 155 | c.iterated = true 156 | _, err := c.jsCursor.Call("continuePrimaryKey", key, primaryKey) 157 | return tryAsDOMException(err) 158 | } 159 | 160 | // Delete returns an AckRequest, and, in a separate thread, deletes the record at the cursor's position, without changing the cursor's position. This can be used to delete specific records. 161 | func (c *Cursor) Delete() (*AckRequest, error) { 162 | reqValue, err := c.jsCursor.Call("delete") 163 | if err != nil { 164 | return nil, tryAsDOMException(err) 165 | } 166 | req := wrapRequest(c.txn, reqValue) 167 | return newAckRequest(req), nil 168 | } 169 | 170 | // Update returns a Request, and, in a separate thread, updates the value at the current position of the cursor in the object store. This can be used to update specific records. 171 | func (c *Cursor) Update(value js.Value) (*Request, error) { 172 | reqValue, err := c.jsCursor.Call("update", value) 173 | if err != nil { 174 | return nil, tryAsDOMException(err) 175 | } 176 | return wrapRequest(c.txn, reqValue), nil 177 | } 178 | 179 | // CursorWithValue represents a cursor for traversing or iterating over multiple records in a database. It is the same as the Cursor, except that it includes the value property. 180 | type CursorWithValue struct { 181 | *Cursor 182 | } 183 | 184 | func newCursorWithValue(cursor *Cursor) *CursorWithValue { 185 | return &CursorWithValue{cursor} 186 | } 187 | 188 | func wrapCursorWithValue(txn *Transaction, jsCursor safejs.Value) *CursorWithValue { 189 | return newCursorWithValue(wrapCursor(txn, jsCursor)) 190 | } 191 | 192 | // Value returns the value of the current cursor 193 | func (c *CursorWithValue) Value() (js.Value, error) { 194 | value, err := c.jsCursor.Get("value") 195 | return safejs.Unsafe(value), err 196 | } 197 | -------------------------------------------------------------------------------- /idb/cursor_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "syscall/js" 9 | "testing" 10 | 11 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 12 | ) 13 | 14 | var ( 15 | someKeyStoreData = [][]interface{}{ 16 | {"some id 1", map[string]interface{}{"primary": "some value 1"}}, 17 | {"some id 2", map[string]interface{}{"primary": "some value 2"}}, 18 | {"some id 3", map[string]interface{}{"primary": "some value 3"}}, 19 | {"some id 4", map[string]interface{}{"primary": "some value 4"}}, 20 | {"some id 5", map[string]interface{}{"primary": "some value 5"}}, 21 | } 22 | ) 23 | 24 | func someKeyStore(tb testing.TB) (*ObjectStore, *Index) { 25 | tb.Helper() 26 | db := testDB(tb, func(db *Database) { 27 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 28 | assert.NoError(tb, err) 29 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{}) 30 | assert.NoError(tb, err) 31 | }) 32 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 33 | assert.NoError(tb, err) 34 | store, err := txn.ObjectStore("mystore") 35 | assert.NoError(tb, err) 36 | index, err := store.Index("myindex") 37 | assert.NoError(tb, err) 38 | 39 | for _, object := range someKeyStoreData { 40 | key, value := object[0], object[1] 41 | _, err := store.AddKey(js.ValueOf(key), js.ValueOf(value)) 42 | assert.NoError(tb, err) 43 | } 44 | return store, index 45 | } 46 | 47 | func TestCursorSource(t *testing.T) { 48 | t.Parallel() 49 | store, _ := someKeyStore(t) 50 | cursor, err := store.OpenCursor(CursorNext) 51 | assert.NoError(t, err) 52 | 53 | cursorStore, cursorIndex, err := cursor.Source() 54 | assert.NoError(t, err) 55 | assert.Zero(t, cursorIndex) 56 | assert.Equal(t, store, cursorStore) 57 | } 58 | 59 | func TestCursorDirection(t *testing.T) { 60 | t.Parallel() 61 | for _, direction := range []CursorDirection{ 62 | CursorNext, 63 | CursorNextUnique, 64 | CursorPrevious, 65 | CursorPreviousUnique, 66 | } { 67 | t.Log("Direction:", direction) // disabled parallel subtests here, due to an issue in paralleltest linter 68 | store, _ := someKeyStore(t) 69 | 70 | req, err := store.OpenCursor(direction) 71 | assert.NoError(t, err) 72 | cursor, err := req.Await(context.Background()) 73 | assert.NoError(t, err) 74 | 75 | actualDirection, err := cursor.Direction() 76 | assert.NoError(t, err) 77 | assert.Equal(t, direction, actualDirection) 78 | } 79 | } 80 | 81 | func TestCursorKey(t *testing.T) { 82 | t.Parallel() 83 | store, _ := someKeyStore(t) 84 | req, err := store.OpenCursor(CursorNext) 85 | assert.NoError(t, err) 86 | 87 | iterIndex := 0 88 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 89 | expectKey := someKeyStoreData[iterIndex][0] 90 | key, err := cursor.Key() 91 | assert.NoError(t, err) 92 | assert.Equal(t, js.ValueOf(expectKey), key) 93 | iterIndex++ 94 | return err 95 | })) 96 | assert.Equal(t, len(someKeyStoreData), iterIndex) 97 | } 98 | 99 | func TestCursorPrimaryKey(t *testing.T) { 100 | t.Parallel() 101 | store, _ := someKeyStore(t) 102 | req, err := store.OpenCursor(CursorNext) 103 | assert.NoError(t, err) 104 | 105 | iterIndex := 0 106 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 107 | expectKey := someKeyStoreData[iterIndex][0] 108 | key, err := cursor.PrimaryKey() 109 | assert.NoError(t, err) 110 | assert.Equal(t, js.ValueOf(expectKey), key) 111 | iterIndex++ 112 | return err 113 | })) 114 | assert.Equal(t, len(someKeyStoreData), iterIndex) 115 | } 116 | 117 | func TestCursorRequest(t *testing.T) { 118 | t.Parallel() 119 | store, _ := someKeyStore(t) 120 | req, err := store.OpenCursor(CursorNext) 121 | assert.NoError(t, err) 122 | 123 | cursor, err := req.Await(context.Background()) 124 | assert.NoError(t, err) 125 | cursorReq, err := cursor.Request() 126 | assert.NoError(t, err) 127 | assert.Equal(t, req.jsRequest, cursorReq.jsRequest) 128 | } 129 | 130 | func TestCursorAdvance(t *testing.T) { 131 | t.Parallel() 132 | store, _ := someKeyStore(t) 133 | req, err := store.OpenCursor(CursorNext) 134 | assert.NoError(t, err) 135 | 136 | iterIndex := 0 137 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 138 | expectKey := someKeyStoreData[iterIndex*2][0] 139 | key, err := cursor.Key() 140 | assert.NoError(t, err) 141 | assert.Equal(t, js.ValueOf(expectKey), key) 142 | err = cursor.Advance(2) 143 | assert.NoError(t, err) 144 | iterIndex++ 145 | return err 146 | })) 147 | assert.Equal(t, len(someKeyStoreData)/2+len(someKeyStoreData)%2, iterIndex) 148 | } 149 | 150 | func TestCursorContinue(t *testing.T) { 151 | t.Parallel() 152 | store, _ := someKeyStore(t) 153 | req, err := store.OpenCursor(CursorNext) 154 | assert.NoError(t, err) 155 | 156 | iterIndex := 0 157 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 158 | expectKey := someKeyStoreData[iterIndex][0] 159 | key, err := cursor.Key() 160 | assert.NoError(t, err) 161 | assert.Equal(t, js.ValueOf(expectKey), key) 162 | err = cursor.Continue() 163 | assert.NoError(t, err) 164 | iterIndex++ 165 | return err 166 | })) 167 | assert.Equal(t, len(someKeyStoreData), iterIndex) 168 | } 169 | 170 | func TestCursorContinueKey(t *testing.T) { 171 | t.Parallel() 172 | store, _ := someKeyStore(t) 173 | req, err := store.OpenCursor(CursorNext) 174 | assert.NoError(t, err) 175 | 176 | iterIndex := 0 177 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 178 | expectKey := someKeyStoreData[iterIndex*2][0] 179 | key, err := cursor.Key() 180 | assert.NoError(t, err) 181 | assert.Equal(t, js.ValueOf(expectKey), key) 182 | 183 | nextIndex := (iterIndex + 1) * 2 184 | if nextIndex >= len(someKeyStoreData) { 185 | return ErrCursorStopIter 186 | } 187 | nextKey := someKeyStoreData[nextIndex][0] 188 | err = cursor.ContinueKey(js.ValueOf(nextKey)) 189 | assert.NoError(t, err) 190 | iterIndex++ 191 | return err 192 | })) 193 | assert.Equal(t, len(someKeyStoreData)/2, iterIndex) 194 | } 195 | 196 | func TestCursorContinuePrimaryKey(t *testing.T) { 197 | t.Parallel() 198 | _, index := someKeyStore(t) 199 | req, err := index.OpenCursor(CursorNext) 200 | assert.NoError(t, err) 201 | 202 | getPrimaryKey := func(ix int) string { 203 | return someKeyStoreData[ix][1].(map[string]interface{})["primary"].(string) 204 | } 205 | 206 | iterIndex := 0 207 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 208 | expectKey := getPrimaryKey(iterIndex * 2) 209 | key, err := cursor.Key() 210 | assert.NoError(t, err) 211 | assert.Equal(t, js.ValueOf(expectKey), key) 212 | 213 | nextIndex := (iterIndex + 1) * 2 214 | if nextIndex >= len(someKeyStoreData) { 215 | return ErrCursorStopIter 216 | } 217 | nextKey := someKeyStoreData[nextIndex][0] 218 | nextIndexKey := getPrimaryKey(nextIndex) 219 | err = cursor.ContinuePrimaryKey(js.ValueOf(nextIndexKey), js.ValueOf(nextKey)) 220 | assert.NoError(t, err) 221 | iterIndex++ 222 | return err 223 | })) 224 | assert.Equal(t, len(someKeyStoreData)/2, iterIndex) 225 | } 226 | 227 | func TestCursorDelete(t *testing.T) { 228 | t.Parallel() 229 | store, _ := someKeyStore(t) 230 | req, err := store.OpenCursor(CursorNext) 231 | assert.NoError(t, err) 232 | 233 | iterIndex := 0 234 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 235 | expectKey := someKeyStoreData[iterIndex][0] 236 | key, err := cursor.Key() 237 | assert.NoError(t, err) 238 | assert.Equal(t, js.ValueOf(expectKey), key) 239 | _, err = cursor.Delete() 240 | assert.NoError(t, err) 241 | iterIndex++ 242 | return err 243 | })) 244 | assert.Equal(t, len(someKeyStoreData), iterIndex) 245 | 246 | // ensure empty after deleting everything 247 | txn, err := store.Transaction() 248 | assert.NoError(t, err) 249 | storeName, err := store.Name() 250 | assert.NoError(t, err) 251 | emptyStore, err := txn.ObjectStore(storeName) 252 | assert.NoError(t, err) 253 | countReq, err := emptyStore.Count() 254 | assert.NoError(t, err) 255 | count, err := countReq.Await(context.Background()) 256 | assert.NoError(t, err) 257 | assert.Zero(t, count) 258 | } 259 | 260 | func TestCursorUpdateAndValue(t *testing.T) { 261 | t.Parallel() 262 | store, _ := someKeyStore(t) 263 | req, err := store.OpenCursor(CursorNext) 264 | assert.NoError(t, err) 265 | 266 | // set all keys to their iteration index 267 | iterIndex := 0 268 | assert.NoError(t, req.Iter(context.Background(), func(cursor *CursorWithValue) error { 269 | expectKey := someKeyStoreData[iterIndex][0] 270 | key, err := cursor.Key() 271 | assert.NoError(t, err) 272 | assert.Equal(t, js.ValueOf(expectKey), key) 273 | _, err = cursor.Update(js.ValueOf(iterIndex)) 274 | assert.NoError(t, err) 275 | iterIndex++ 276 | return err 277 | })) 278 | assert.Equal(t, len(someKeyStoreData), iterIndex) 279 | 280 | // ensure values equal indices after updating everything 281 | txn, err := store.Transaction() 282 | assert.NoError(t, err) 283 | storeName, err := store.Name() 284 | assert.NoError(t, err) 285 | updatedStore, err := txn.ObjectStore(storeName) 286 | assert.NoError(t, err) 287 | cursorReq, err := updatedStore.OpenCursor(CursorNext) 288 | assert.NoError(t, err) 289 | ix := 0 290 | assert.NoError(t, cursorReq.Iter(context.Background(), func(cursor *CursorWithValue) error { 291 | value, err := cursor.Value() 292 | assert.NoError(t, err) 293 | assert.Equal(t, js.ValueOf(ix), value) 294 | ix++ 295 | return err 296 | })) 297 | assert.Equal(t, len(someKeyStoreData), ix) 298 | } 299 | -------------------------------------------------------------------------------- /idb/db.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "github.com/hack-pad/go-indexeddb/idb/internal/jscache" 8 | "github.com/hack-pad/safejs" 9 | ) 10 | 11 | // Database provides a connection to a database. You can use a Database object to open a transaction on your database then create, manipulate, and delete objects (data) in that database. 12 | type Database struct { 13 | jsDB safejs.Value 14 | callStrings jscache.Strings 15 | } 16 | 17 | func wrapDatabase(jsDB safejs.Value) *Database { 18 | return &Database{jsDB: jsDB} 19 | } 20 | 21 | // Name returns the name of the connected database. 22 | func (db *Database) Name() (string, error) { 23 | value, err := db.jsDB.Get("name") 24 | if err != nil { 25 | return "", err 26 | } 27 | return value.String() 28 | } 29 | 30 | // Version returns the version of the connected database. 31 | func (db *Database) Version() (uint, error) { 32 | value, err := db.jsDB.Get("version") 33 | if err != nil { 34 | return 0, err 35 | } 36 | intValue, err := value.Int() 37 | return uint(intValue), err 38 | } 39 | 40 | // ObjectStoreNames returns a list of the names of the object stores currently in the connected database. 41 | func (db *Database) ObjectStoreNames() ([]string, error) { 42 | array, err := db.jsDB.Get("objectStoreNames") 43 | if err != nil { 44 | return nil, err 45 | } 46 | return stringsFromArray(array) 47 | } 48 | 49 | // CreateObjectStore creates and returns a new object store or index. 50 | func (db *Database) CreateObjectStore(name string, options ObjectStoreOptions) (*ObjectStore, error) { 51 | jsObjectStore, err := db.jsDB.Call("createObjectStore", name, map[string]interface{}{ 52 | "autoIncrement": options.AutoIncrement, 53 | "keyPath": options.KeyPath, 54 | }) 55 | if err != nil { 56 | return nil, tryAsDOMException(err) 57 | } 58 | return wrapObjectStore(nil, jsObjectStore), nil 59 | } 60 | 61 | // DeleteObjectStore destroys the object store with the given name in the connected database, along with any indexes that reference it. 62 | func (db *Database) DeleteObjectStore(name string) error { 63 | _, err := db.jsDB.Call("deleteObjectStore", name) 64 | return tryAsDOMException(err) 65 | } 66 | 67 | // Close closes the connection to a database. 68 | func (db *Database) Close() error { 69 | _, err := db.jsDB.Call("close") 70 | return tryAsDOMException(err) 71 | } 72 | 73 | // Transaction returns a transaction object containing the Transaction.ObjectStore() method, which you can use to access your object store. 74 | func (db *Database) Transaction(mode TransactionMode, objectStoreName string, objectStoreNames ...string) (_ *Transaction, err error) { 75 | return db.TransactionWithOptions(TransactionOptions{Mode: mode}, objectStoreName, objectStoreNames...) 76 | } 77 | 78 | // TransactionOptions contains all available options for creating and starting a Transaction 79 | type TransactionOptions struct { 80 | Mode TransactionMode 81 | Durability TransactionDurability 82 | } 83 | 84 | // TransactionWithOptions returns a transaction object containing the Transaction.ObjectStore() method, which you can use to access your object store. 85 | func (db *Database) TransactionWithOptions(options TransactionOptions, objectStoreName string, objectStoreNames ...string) (*Transaction, error) { 86 | objectStoreNames = append([]string{objectStoreName}, objectStoreNames...) // require at least one name 87 | 88 | optionsMap := make(map[string]interface{}) 89 | if options.Durability != DurabilityDefault { 90 | optionsMap["durability"] = options.Durability.jsValue() 91 | } 92 | 93 | args := []interface{}{sliceFromStrings(objectStoreNames), options.Mode.jsValue()} 94 | if len(optionsMap) > 0 { 95 | args = append(args, optionsMap) 96 | } 97 | 98 | jsTxn, err := db.jsDB.Call("transaction", args...) 99 | if err != nil { 100 | return nil, tryAsDOMException(err) 101 | } 102 | return wrapTransaction(db, jsTxn), nil 103 | } 104 | -------------------------------------------------------------------------------- /idb/db_factory.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "sync" 10 | "syscall/js" 11 | 12 | "github.com/hack-pad/safejs" 13 | ) 14 | 15 | // Factory lets applications asynchronously access the indexed databases. A typical program will call Global() to access window.indexedDB. 16 | type Factory struct { 17 | jsFactory safejs.Value 18 | } 19 | 20 | var ( 21 | global *Factory 22 | globalErr error 23 | globalOnce sync.Once 24 | ) 25 | 26 | // Global returns the global IndexedDB instance. 27 | // Can be called multiple times, will always return the same result (or error if one occurs). 28 | func Global() *Factory { 29 | globalOnce.Do(func() { 30 | var jsFactory safejs.Value 31 | jsFactory, globalErr = safejs.Global().Get("indexedDB") 32 | if globalErr != nil { 33 | return 34 | } 35 | var truthy bool 36 | truthy, globalErr = jsFactory.Truthy() 37 | if globalErr != nil { 38 | return 39 | } 40 | if truthy { 41 | global, globalErr = WrapFactory(safejs.Unsafe(jsFactory)) 42 | } else { 43 | globalErr = errors.New("Global JS variable 'indexedDB' is not defined") 44 | } 45 | }) 46 | if globalErr != nil { 47 | panic(globalErr) 48 | } 49 | return global 50 | } 51 | 52 | // WrapFactory wraps the given IDBFactory object 53 | func WrapFactory(jsFactory js.Value) (*Factory, error) { 54 | return &Factory{ 55 | jsFactory: safejs.Safe(jsFactory), 56 | }, nil 57 | } 58 | 59 | // Open requests to open a connection to a database. 60 | func (f *Factory) Open(upgradeCtx context.Context, name string, version uint, upgrader Upgrader) (*OpenDBRequest, error) { 61 | args := []interface{}{name} 62 | if version > 0 { 63 | args = append(args, version) 64 | } 65 | reqValue, err := f.jsFactory.Call("open", args...) 66 | if err != nil { 67 | return nil, tryAsDOMException(err) 68 | } 69 | req := wrapRequest(nil, reqValue) 70 | return newOpenDBRequest(upgradeCtx, req, upgrader) 71 | } 72 | 73 | // DeleteDatabase requests the deletion of a database. 74 | func (f *Factory) DeleteDatabase(name string) (*AckRequest, error) { 75 | reqValue, err := f.jsFactory.Call("deleteDatabase", name) 76 | if err != nil { 77 | return nil, tryAsDOMException(err) 78 | } 79 | req := wrapRequest(nil, reqValue) 80 | return newAckRequest(req), nil 81 | } 82 | 83 | // CompareKeys compares two keys and returns a result indicating which one is greater in value. 84 | func (f *Factory) CompareKeys(a, b js.Value) (int, error) { 85 | compare, err := f.jsFactory.Call("cmp", a, b) 86 | if err != nil { 87 | return 0, tryAsDOMException(err) 88 | } 89 | return compare.Int() 90 | } 91 | -------------------------------------------------------------------------------- /idb/db_factory_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "syscall/js" 10 | "testing" 11 | 12 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 13 | "github.com/hack-pad/safejs" 14 | ) 15 | 16 | const testDBPrefix = "go-indexeddb-test-" 17 | 18 | func TestGlobal(t *testing.T) { 19 | t.Parallel() 20 | var dbFactory *Factory 21 | assert.NotPanics(t, func() { 22 | dbFactory = Global() 23 | }) 24 | 25 | indexedDB, err := safejs.Global().Get("indexedDB") 26 | assert.NoError(t, err) 27 | assert.Equal(t, &Factory{indexedDB}, dbFactory) 28 | } 29 | 30 | func testFactory(tb testing.TB) *Factory { 31 | tb.Helper() 32 | dbFactory := Global() 33 | tb.Cleanup(func() { 34 | databaseNames := testGetDatabases(tb, dbFactory) 35 | var requests []*AckRequest 36 | for _, name := range databaseNames { 37 | if strings.HasPrefix(name, testDBPrefix) { 38 | req, err := dbFactory.DeleteDatabase(name) 39 | assert.NoError(tb, err) 40 | requests = append(requests, req) 41 | } 42 | } 43 | for _, req := range requests { 44 | assert.NoError(tb, req.Await(context.Background())) 45 | } 46 | }) 47 | return dbFactory 48 | } 49 | 50 | func testGetDatabases(tb testing.TB, dbFactory *Factory) []string { 51 | tb.Helper() 52 | done := make(chan struct{}) 53 | var names []string 54 | var fn safejs.Func 55 | fn, err := safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) interface{} { 56 | defer fn.Release() 57 | arr := args[0] 58 | assert.NoError(tb, iterArray(arr, func(_ int, value safejs.Value) (keepGoing bool, visitErr error) { 59 | nameValue, err := value.Get("name") 60 | assert.NoError(tb, err) 61 | name, err := nameValue.String() 62 | assert.NoError(tb, err) 63 | names = append(names, name) 64 | return true, nil 65 | })) 66 | close(done) 67 | return nil 68 | }) 69 | if err != nil { 70 | assert.NoError(tb, err) 71 | } 72 | databasesPromise, err := dbFactory.jsFactory.Call("databases") 73 | if err != nil { 74 | assert.NoError(tb, err) 75 | } 76 | _, err = databasesPromise.Call("then", fn) 77 | if err != nil { 78 | assert.NoError(tb, err) 79 | } 80 | <-done 81 | return names 82 | } 83 | 84 | func TestFactoryOpenNewDB(t *testing.T) { // nolint:paralleltest // Deletes all databases, should not run in parallel. 85 | dbFactory := testFactory(t) 86 | req, err := dbFactory.Open(context.Background(), testDBPrefix+"mydb", 0, func(db *Database, oldVersion, newVersion uint) error { 87 | assert.Equal(t, uint(0), oldVersion) 88 | assert.Equal(t, uint(1), newVersion) 89 | return nil 90 | }) 91 | if !assert.NoError(t, err) { 92 | t.FailNow() 93 | } 94 | db, err := req.Await(context.Background()) 95 | assert.NoError(t, err) 96 | assert.NotZero(t, db) 97 | assert.NoError(t, db.Close()) 98 | } 99 | 100 | func TestFactoryOpenExistingDB(t *testing.T) { // nolint:paralleltest // Deletes all databases, should not run in parallel. 101 | dbFactory := testFactory(t) 102 | _, err := dbFactory.Open(context.Background(), testDBPrefix+"mydb", 1, func(db *Database, oldVersion, newVersion uint) error { 103 | return nil 104 | }) 105 | if !assert.NoError(t, err) { 106 | t.FailNow() 107 | } 108 | 109 | req, err := dbFactory.Open(context.Background(), testDBPrefix+"mydb", 1, func(db *Database, oldVersion, newVersion uint) error { 110 | t.Error("Should not call upgrade") 111 | return nil 112 | }) 113 | if !assert.NoError(t, err) { 114 | t.FailNow() 115 | } 116 | db, err := req.Await(context.Background()) 117 | assert.NoError(t, err) 118 | assert.NotZero(t, db) 119 | assert.NoError(t, db.Close()) 120 | } 121 | 122 | func TestFactoryDeleteMissingDatabase(t *testing.T) { // nolint:paralleltest // Deletes all databases, should not run in parallel. 123 | dbFactory := testFactory(t) 124 | req, err := dbFactory.DeleteDatabase("does not exist") 125 | assert.NoError(t, err) 126 | err = req.Await(context.Background()) 127 | assert.NoError(t, err) 128 | } 129 | 130 | func TestFactoryDeleteDatabase(t *testing.T) { // nolint:paralleltest // Deletes all databases, should not run in parallel. 131 | dbFactory := testFactory(t) 132 | var db *Database 133 | { 134 | req, err := dbFactory.Open(context.Background(), testDBPrefix+"mydb", 0, func(db *Database, oldVersion, newVersion uint) error { 135 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 136 | assert.NoError(t, err) 137 | return nil 138 | }) 139 | assert.NoError(t, err) 140 | db, err = req.Await(context.Background()) 141 | assert.NoError(t, err) 142 | names, err := db.ObjectStoreNames() 143 | assert.NoError(t, err) 144 | assert.Equal(t, []string{"mystore"}, names) 145 | if t.Failed() { 146 | t.FailNow() 147 | } 148 | } 149 | 150 | req, err := dbFactory.DeleteDatabase(testDBPrefix + "mydb") 151 | assert.NoError(t, err) 152 | err = req.Await(context.Background()) 153 | assert.NoError(t, err) 154 | 155 | // database should be closed and unusable now 156 | _, err = db.Transaction(TransactionReadOnly, "mystore") 157 | assert.Error(t, err) 158 | assert.NoError(t, db.Close()) 159 | } 160 | 161 | func TestFactoryCompareKeys(t *testing.T) { 162 | t.Parallel() 163 | 164 | t.Run("normal keys", func(t *testing.T) { 165 | t.Parallel() 166 | dbFactory := testFactory(t) 167 | compare, err := dbFactory.CompareKeys(js.ValueOf("a"), js.ValueOf("b")) 168 | assert.NoError(t, err) 169 | assert.Equal(t, -1, compare) 170 | }) 171 | 172 | t.Run("bad keys", func(t *testing.T) { 173 | t.Parallel() 174 | dbFactory := testFactory(t) 175 | _, err := dbFactory.CompareKeys(js.ValueOf(map[string]interface{}{"a": "a"}), js.ValueOf("b")) 176 | assert.Error(t, err) 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /idb/db_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "crypto/rand" 9 | "fmt" 10 | "math/big" 11 | "syscall/js" 12 | "testing" 13 | 14 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 15 | ) 16 | 17 | func testDB(tb testing.TB, initFunc func(*Database)) *Database { 18 | tb.Helper() 19 | dbFactory := Global() 20 | 21 | n, err := rand.Int(rand.Reader, big.NewInt(1000)) 22 | assert.NoError(tb, err) 23 | name := fmt.Sprintf("%s%s/%d", testDBPrefix, tb.Name(), n.Int64()) 24 | req, err := dbFactory.Open(context.Background(), name, 0, func(db *Database, oldVersion, newVersion uint) error { 25 | initFunc(db) 26 | return nil 27 | }) 28 | if !assert.NoError(tb, err) { 29 | tb.FailNow() 30 | } 31 | db, err := req.Await(context.Background()) 32 | if !assert.NoError(tb, err) { 33 | tb.FailNow() 34 | } 35 | tb.Cleanup(func() { 36 | assert.NoError(tb, db.Close()) 37 | req, err := dbFactory.DeleteDatabase(name) 38 | assert.NoError(tb, err) 39 | assert.NoError(tb, req.Await(context.Background())) 40 | }) 41 | return db 42 | } 43 | 44 | func TestDatabaseName(t *testing.T) { 45 | t.Parallel() 46 | db := testDB(t, func(db *Database) {}) 47 | name, err := db.Name() 48 | assert.NoError(t, err) 49 | assert.Contains(t, name, t.Name()) 50 | } 51 | 52 | func TestDatabaseVersion(t *testing.T) { 53 | t.Parallel() 54 | db := testDB(t, func(db *Database) {}) 55 | version, err := db.Version() 56 | assert.NoError(t, err) 57 | assert.Equal(t, uint(1), version) 58 | } 59 | 60 | func TestDatabaseCreateObjectStore(t *testing.T) { 61 | t.Parallel() 62 | 63 | t.Run("default options", func(t *testing.T) { 64 | t.Parallel() 65 | db := testDB(t, func(db *Database) { 66 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 67 | assert.NoError(t, err) 68 | }) 69 | names, err := db.ObjectStoreNames() 70 | assert.NoError(t, err) 71 | assert.Equal(t, []string{"mystore"}, names) 72 | }) 73 | 74 | t.Run("set keypath and auto-increment", func(t *testing.T) { 75 | t.Parallel() 76 | const storeName = "mystore" 77 | db := testDB(t, func(db *Database) { 78 | _, err := db.CreateObjectStore(storeName, ObjectStoreOptions{ 79 | KeyPath: js.ValueOf("primary"), 80 | AutoIncrement: true, 81 | }) 82 | assert.NoError(t, err) 83 | }) 84 | txn, err := db.Transaction(TransactionReadOnly, storeName) 85 | assert.NoError(t, err) 86 | store, err := txn.ObjectStore(storeName) 87 | assert.NoError(t, err) 88 | 89 | keyPath, err := store.KeyPath() 90 | assert.NoError(t, err) 91 | assert.Equal(t, js.ValueOf("primary"), keyPath) 92 | autoIncrement, err := store.AutoIncrement() 93 | assert.NoError(t, err) 94 | assert.Equal(t, true, autoIncrement) 95 | }) 96 | } 97 | 98 | func TestDatabaseDeleteObjectStore(t *testing.T) { 99 | t.Parallel() 100 | 101 | t.Run("delete object store", func(t *testing.T) { 102 | t.Parallel() 103 | testDB(t, func(db *Database) { 104 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 105 | assert.NoError(t, err) 106 | 107 | assert.NoError(t, db.DeleteObjectStore("mystore")) 108 | names, err := db.ObjectStoreNames() 109 | assert.NoError(t, err) 110 | assert.Equal(t, []string(nil), names) 111 | }) 112 | }) 113 | 114 | t.Run("not upgrading", func(t *testing.T) { 115 | t.Parallel() 116 | db := testDB(t, func(db *Database) {}) 117 | assert.Error(t, db.DeleteObjectStore("mystore")) 118 | }) 119 | } 120 | 121 | func TestDatabaseTransaction(t *testing.T) { 122 | t.Parallel() 123 | 124 | for _, tc := range []struct { 125 | name string 126 | makeTxn func(*Database) (*Transaction, error) 127 | }{ 128 | { 129 | name: "simple readwrite", 130 | makeTxn: func(db *Database) (*Transaction, error) { 131 | return db.Transaction(TransactionReadWrite, "store1", "store2") 132 | }, 133 | }, 134 | { 135 | name: "readwrite with durability", 136 | makeTxn: func(db *Database) (*Transaction, error) { 137 | return db.TransactionWithOptions(TransactionOptions{ 138 | Mode: TransactionReadWrite, 139 | Durability: DurabilityRelaxed, 140 | }, "store1", "store2") 141 | }, 142 | }, 143 | } { 144 | tc := tc // keep loop-local copy of test case for parallel runs 145 | t.Run(tc.name, func(t *testing.T) { 146 | t.Parallel() 147 | db := testDB(t, func(db *Database) { 148 | _, err := db.CreateObjectStore("store1", ObjectStoreOptions{}) 149 | assert.NoError(t, err) 150 | _, err = db.CreateObjectStore("store2", ObjectStoreOptions{}) 151 | assert.NoError(t, err) 152 | }) 153 | // set values in 2 stores 154 | txn, err := tc.makeTxn(db) 155 | assert.NoError(t, err) 156 | store1, err := txn.ObjectStore("store1") 157 | assert.NoError(t, err) 158 | _, err = store1.PutKey(js.ValueOf("key1"), js.ValueOf("value1")) 159 | assert.NoError(t, err) 160 | store2, err := txn.ObjectStore("store2") 161 | assert.NoError(t, err) 162 | _, err = store2.PutKey(js.ValueOf("key2"), js.ValueOf("value2")) 163 | assert.NoError(t, err) 164 | 165 | // verify 1 of the values is correct 166 | req, err := store1.GetAllKeys() 167 | assert.NoError(t, err) 168 | 169 | // wait for the whole txn to complete 170 | assert.NoError(t, txn.Await(context.Background())) 171 | result, err := req.Result() 172 | assert.NoError(t, err) 173 | assert.Equal(t, []js.Value{js.ValueOf("key1")}, result) 174 | }) 175 | } 176 | } 177 | 178 | func TestDatabaseClose(t *testing.T) { 179 | t.Parallel() 180 | 181 | db := testDB(t, func(db *Database) { 182 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 183 | assert.NoError(t, err) 184 | }) 185 | _, err := db.Transaction(TransactionReadOnly, "mystore") 186 | assert.NoError(t, err) 187 | assert.NoError(t, db.Close()) 188 | 189 | _, err = db.Transaction(TransactionReadOnly, "mystore") 190 | assert.Error(t, err) 191 | } 192 | -------------------------------------------------------------------------------- /idb/doc.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | /* 5 | Package idb is a low-level driver that provides type-safe bindings to IndexedDB in Wasm programs. 6 | The primary focus is to align with the IndexedDB spec, followed by ease of use. 7 | 8 | To get started, get the global indexedDB instance with idb.Global(). See below for examples. 9 | */ 10 | package idb 11 | -------------------------------------------------------------------------------- /idb/doc_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | // nolint:errcheck 5 | package idb_test 6 | 7 | import ( 8 | "context" 9 | "syscall/js" 10 | 11 | "github.com/hack-pad/go-indexeddb/idb" 12 | ) 13 | 14 | var ( 15 | Books = []string{ 16 | "Hitchhiker's Guide to the Galaxy", 17 | "Leaves of Grass", 18 | "The Great Gatsby", 19 | "The Hobbit", 20 | } 21 | ) 22 | 23 | func Example() { 24 | // Create the 'library' database, then create a 'books' object store during setup. 25 | // The setup func can also upgrade the database from older versions. 26 | ctx := context.Background() 27 | openRequest, _ := idb.Global().Open(ctx, "library", 1, func(db *idb.Database, oldVersion, newVersion uint) error { 28 | db.CreateObjectStore("books", idb.ObjectStoreOptions{}) 29 | return nil 30 | }) 31 | db, _ := openRequest.Await(ctx) 32 | 33 | { // Store some books in the library database. 34 | txn, _ := db.Transaction(idb.TransactionReadWrite, "books") 35 | store, _ := txn.ObjectStore("books") 36 | for _, bookTitle := range Books { 37 | store.Add(js.ValueOf(bookTitle)) 38 | } 39 | txn.Await(ctx) 40 | } 41 | 42 | { // Iterate through the books and print their titles. 43 | txn, _ := db.Transaction(idb.TransactionReadOnly, "books") 44 | store, _ := txn.ObjectStore("books") 45 | cursorRequest, _ := store.OpenCursor(idb.CursorNext) 46 | cursorRequest.Iter(ctx, func(cursor *idb.CursorWithValue) error { 47 | value, _ := cursor.Value() 48 | println(value.String()) 49 | return nil 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /idb/dom_exception.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | 9 | "github.com/hack-pad/safejs" 10 | ) 11 | 12 | func tryAsDOMException(err error) error { 13 | switch err := err.(type) { 14 | case js.Error: 15 | return domExceptionAsError(safejs.Safe(err.Value)) 16 | default: 17 | return err 18 | } 19 | } 20 | 21 | func domExceptionAsError(jsDOMException safejs.Value) error { 22 | truthy, err := jsDOMException.Truthy() 23 | if err != nil || !truthy { 24 | return err 25 | } 26 | domException, err := parseJSDOMException(jsDOMException) 27 | if err != nil { 28 | return err 29 | } 30 | return domException 31 | } 32 | 33 | // DOMException is a JavaScript DOMException with a standard name. 34 | // Use errors.Is() to compare by name. 35 | type DOMException struct { 36 | name string 37 | message string 38 | } 39 | 40 | // NewDOMException returns a new DOMException with the given name. 41 | // Only useful for errors.Is() comparisons with errors returned from idb. 42 | func NewDOMException(name string) DOMException { 43 | return DOMException{name: name} 44 | } 45 | 46 | func parseJSDOMException(jsDOMException safejs.Value) (DOMException, error) { 47 | name, err := jsDOMException.Get("name") 48 | if err != nil { 49 | return DOMException{}, err 50 | } 51 | nameStr, err := name.String() 52 | if err != nil { 53 | return DOMException{}, err 54 | } 55 | message, err := jsDOMException.Get("message") 56 | if err != nil { 57 | return DOMException{}, err 58 | } 59 | messageStr, err := message.String() 60 | if err != nil { 61 | return DOMException{}, err 62 | } 63 | return DOMException{ 64 | name: nameStr, 65 | message: messageStr, 66 | }, nil 67 | } 68 | 69 | func (e DOMException) Error() string { 70 | if e.message == "" { 71 | return e.name 72 | } 73 | return e.name + ": " + e.message 74 | } 75 | 76 | // Is returns true target is a DOMException and matches this DOMException's name. Use 'errors.Is()' to call it. 77 | func (e DOMException) Is(target error) bool { 78 | targetDOMException, ok := target.(DOMException) 79 | return ok && targetDOMException.name == e.name 80 | } 81 | -------------------------------------------------------------------------------- /idb/dom_exception_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | "testing" 9 | 10 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 11 | "github.com/hack-pad/safejs" 12 | ) 13 | 14 | var domException safejs.Value 15 | 16 | func init() { 17 | var err error 18 | domException, err = safejs.Global().Get("DOMException") 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func TestTryAsDOMException(t *testing.T) { 25 | t.Parallel() 26 | exceptionJS, err := domException.New("message", "name") 27 | assert.NoError(t, err) 28 | exception := js.Error{Value: safejs.Unsafe(exceptionJS)} 29 | assert.Equal(t, DOMException{ 30 | name: "name", 31 | message: "message", 32 | }, tryAsDOMException(exception)) 33 | } 34 | 35 | func TestDOMExceptionAsError(t *testing.T) { 36 | t.Parallel() 37 | exceptionJS, err := domException.New("message", "name") 38 | assert.NoError(t, err) 39 | exception := domExceptionAsError(exceptionJS) 40 | assert.Equal(t, DOMException{ 41 | name: "name", 42 | message: "message", 43 | }, exception) 44 | 45 | assert.Equal(t, "name: message", exception.Error()) 46 | 47 | assert.ErrorIs(t, exception, DOMException{name: "name"}) 48 | assert.NotErrorIs(t, exception, DOMException{name: "other name"}) 49 | } 50 | -------------------------------------------------------------------------------- /idb/events.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | const ( 7 | addEventListener = "addEventListener" 8 | removeEventListener = "removeEventListener" 9 | ) 10 | -------------------------------------------------------------------------------- /idb/index.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | 9 | "github.com/hack-pad/safejs" 10 | ) 11 | 12 | // IndexOptions contains all options used to create an Index 13 | type IndexOptions struct { 14 | // Unique disallows duplicate values for a single key. 15 | Unique bool 16 | // MultiEntry adds an entry in the index for each array element when the keyPath resolves to an Array. If false, adds one single entry containing the Array. 17 | MultiEntry bool 18 | } 19 | 20 | // Index provides asynchronous access to an index in a database. An index is a kind of object store for looking up records in another object store, called the referenced object store. You use this to retrieve data. 21 | type Index struct { 22 | base *baseObjectStore // don't embed to avoid generated docs with the wrong receiver type (Index vs *Index) 23 | } 24 | 25 | func wrapIndex(txn *Transaction, jsIndex safejs.Value) *Index { 26 | return &Index{wrapBaseObjectStore(txn, jsIndex)} 27 | } 28 | 29 | // ObjectStore returns the object store referenced by this index. 30 | func (i *Index) ObjectStore() (*ObjectStore, error) { 31 | store, err := i.base.jsObjectStore.Get("objectStore") 32 | if err != nil { 33 | return nil, err 34 | } 35 | return wrapObjectStore(i.base.txn, store), nil 36 | } 37 | 38 | // Name returns the name of this index 39 | func (i *Index) Name() (string, error) { 40 | name, err := i.base.jsObjectStore.Get("name") 41 | if err != nil { 42 | return "", err 43 | } 44 | return name.String() 45 | } 46 | 47 | // KeyPath returns the key path of this index. If js.Null(), this index is not auto-populated. 48 | func (i *Index) KeyPath() (js.Value, error) { 49 | value, err := i.base.jsObjectStore.Get("keyPath") 50 | return safejs.Unsafe(value), err 51 | } 52 | 53 | // MultiEntry affects how the index behaves when the result of evaluating the index's key path yields an array. If true, there is one record in the index for each item in an array of keys. If false, then there is one record for each key that is an array. 54 | func (i *Index) MultiEntry() (bool, error) { 55 | multiEntry, err := i.base.jsObjectStore.Get("multiEntry") 56 | if err != nil { 57 | return false, err 58 | } 59 | return multiEntry.Bool() 60 | } 61 | 62 | // Unique indicates this index does not allow duplicate values for a key. 63 | func (i *Index) Unique() (bool, error) { 64 | unique, err := i.base.jsObjectStore.Get("unique") 65 | if err != nil { 66 | return false, err 67 | } 68 | return unique.Bool() 69 | } 70 | 71 | // Count returns a UintRequest, and, in a separate thread, returns the total number of records in the index. 72 | func (i *Index) Count() (*UintRequest, error) { 73 | return i.base.Count() 74 | } 75 | 76 | // CountKey returns a UintRequest, and, in a separate thread, returns the total number of records that match the provided key. 77 | func (i *Index) CountKey(key js.Value) (*UintRequest, error) { 78 | return i.base.CountKey(safejs.Safe(key)) 79 | } 80 | 81 | // CountRange returns a UintRequest, and, in a separate thread, returns the total number of records that match the provided KeyRange. 82 | func (i *Index) CountRange(keyRange *KeyRange) (*UintRequest, error) { 83 | return i.base.CountRange(keyRange) 84 | } 85 | 86 | // GetAllKeys returns an ArrayRequest that retrieves record keys for all objects in the index. 87 | func (i *Index) GetAllKeys() (*ArrayRequest, error) { 88 | return i.base.GetAllKeys() 89 | } 90 | 91 | // GetAllKeysRange returns an ArrayRequest that retrieves record keys for all objects in the index matching the specified query. If maxCount is 0, retrieves all objects matching the query. 92 | func (i *Index) GetAllKeysRange(query *KeyRange, maxCount uint) (*ArrayRequest, error) { 93 | return i.base.GetAllKeysRange(query, maxCount) 94 | } 95 | 96 | // Get returns a Request, and, in a separate thread, returns objects selected by the specified key. This is for retrieving specific records from an index. 97 | func (i *Index) Get(key js.Value) (*Request, error) { 98 | return i.base.Get(safejs.Safe(key)) 99 | } 100 | 101 | // GetKey returns a Request, and, in a separate thread retrieves and returns the record key for the object matching the specified parameter. 102 | func (i *Index) GetKey(value js.Value) (*Request, error) { 103 | return i.base.GetKey(safejs.Safe(value)) 104 | } 105 | 106 | // OpenCursor returns a CursorWithValueRequest, and, in a separate thread, returns a new CursorWithValue. Used for iterating through an index by primary key with a cursor. 107 | func (i *Index) OpenCursor(direction CursorDirection) (*CursorWithValueRequest, error) { 108 | return i.base.OpenCursor(direction) 109 | } 110 | 111 | // OpenCursorKey is the same as OpenCursor, but opens a cursor over the given key instead. 112 | func (i *Index) OpenCursorKey(key js.Value, direction CursorDirection) (*CursorWithValueRequest, error) { 113 | return i.base.OpenCursorKey(safejs.Safe(key), direction) 114 | } 115 | 116 | // OpenCursorRange is the same as OpenCursor, but opens a cursor over the given range instead. 117 | func (i *Index) OpenCursorRange(keyRange *KeyRange, direction CursorDirection) (*CursorWithValueRequest, error) { 118 | return i.base.OpenCursorRange(keyRange, direction) 119 | } 120 | 121 | // OpenKeyCursor returns a CursorRequest, and, in a separate thread, returns a new Cursor. Used for iterating through all keys in an object store. 122 | func (i *Index) OpenKeyCursor(direction CursorDirection) (*CursorRequest, error) { 123 | return i.base.OpenKeyCursor(direction) 124 | } 125 | 126 | // OpenKeyCursorKey is the same as OpenKeyCursor, but opens a cursor over the given key instead. 127 | func (i *Index) OpenKeyCursorKey(key js.Value, direction CursorDirection) (*CursorRequest, error) { 128 | return i.base.OpenKeyCursorKey(safejs.Safe(key), direction) 129 | } 130 | 131 | // OpenKeyCursorRange is the same as OpenKeyCursor, but opens a cursor over the given key range instead. 132 | func (i *Index) OpenKeyCursorRange(keyRange *KeyRange, direction CursorDirection) (*CursorRequest, error) { 133 | return i.base.OpenKeyCursorRange(keyRange, direction) 134 | } 135 | -------------------------------------------------------------------------------- /idb/index_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | "testing" 9 | 10 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 11 | ) 12 | 13 | func TestIndexObjectStore(t *testing.T) { 14 | t.Parallel() 15 | db := testDB(t, func(db *Database) { 16 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 17 | assert.NoError(t, err) 18 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{}) 19 | assert.NoError(t, err) 20 | }) 21 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 22 | assert.NoError(t, err) 23 | store, err := txn.ObjectStore("mystore") 24 | assert.NoError(t, err) 25 | index, err := store.Index("myindex") 26 | assert.NoError(t, err) 27 | 28 | indexStore, err := index.ObjectStore() 29 | assert.NoError(t, err) 30 | assert.Equal(t, store, indexStore) 31 | } 32 | 33 | func TestIndexName(t *testing.T) { 34 | t.Parallel() 35 | db := testDB(t, func(db *Database) { 36 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 37 | assert.NoError(t, err) 38 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{}) 39 | assert.NoError(t, err) 40 | }) 41 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 42 | assert.NoError(t, err) 43 | store, err := txn.ObjectStore("mystore") 44 | assert.NoError(t, err) 45 | index, err := store.Index("myindex") 46 | assert.NoError(t, err) 47 | 48 | name, err := index.Name() 49 | assert.NoError(t, err) 50 | assert.Equal(t, "myindex", name) 51 | } 52 | 53 | func TestIndexKeyPath(t *testing.T) { 54 | t.Parallel() 55 | db := testDB(t, func(db *Database) { 56 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 57 | assert.NoError(t, err) 58 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{}) 59 | assert.NoError(t, err) 60 | }) 61 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 62 | assert.NoError(t, err) 63 | store, err := txn.ObjectStore("mystore") 64 | assert.NoError(t, err) 65 | index, err := store.Index("myindex") 66 | assert.NoError(t, err) 67 | 68 | keyPath, err := index.KeyPath() 69 | assert.NoError(t, err) 70 | assert.Equal(t, js.ValueOf("primary"), keyPath) 71 | } 72 | 73 | func TestIndexMultiEntry(t *testing.T) { 74 | t.Parallel() 75 | db := testDB(t, func(db *Database) { 76 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 77 | assert.NoError(t, err) 78 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{ 79 | MultiEntry: true, 80 | }) 81 | assert.NoError(t, err) 82 | }) 83 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 84 | assert.NoError(t, err) 85 | store, err := txn.ObjectStore("mystore") 86 | assert.NoError(t, err) 87 | index, err := store.Index("myindex") 88 | assert.NoError(t, err) 89 | 90 | multiEntry, err := index.MultiEntry() 91 | assert.NoError(t, err) 92 | assert.Equal(t, true, multiEntry) 93 | } 94 | 95 | func TestIndexUnique(t *testing.T) { 96 | t.Parallel() 97 | db := testDB(t, func(db *Database) { 98 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 99 | assert.NoError(t, err) 100 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{ 101 | Unique: true, 102 | }) 103 | assert.NoError(t, err) 104 | }) 105 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 106 | assert.NoError(t, err) 107 | store, err := txn.ObjectStore("mystore") 108 | assert.NoError(t, err) 109 | index, err := store.Index("myindex") 110 | assert.NoError(t, err) 111 | 112 | unique, err := index.Unique() 113 | assert.NoError(t, err) 114 | assert.Equal(t, true, unique) 115 | } 116 | -------------------------------------------------------------------------------- /idb/internal/assert/assert.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | // Package assert contains small assertion test functions to assist in writing clean tests. 5 | package assert 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | // Error asserts err is not nil 17 | func Error(tb testing.TB, err error) bool { 18 | tb.Helper() 19 | if err == nil { 20 | tb.Error("Expected error, got nil") 21 | return false 22 | } 23 | return true 24 | } 25 | 26 | // NoError asserts err is nil 27 | func NoError(tb testing.TB, err error) bool { 28 | tb.Helper() 29 | if err != nil { 30 | tb.Errorf("Unexpected error: %+v", err) 31 | return false 32 | } 33 | return true 34 | } 35 | 36 | // ErrorIs asserts err matches target 37 | func ErrorIs(tb testing.TB, err, target error) bool { 38 | tb.Helper() 39 | if !errors.Is(err, target) { 40 | tb.Error("Expected error to match", target, ", got:", err) 41 | return false 42 | } 43 | return true 44 | } 45 | 46 | // NotErrorIs asserts err does match target 47 | func NotErrorIs(tb testing.TB, err, target error) bool { 48 | tb.Helper() 49 | if errors.Is(err, target) { 50 | tb.Error("Expected error not to match", target, ", got:", err) 51 | return false 52 | } 53 | return true 54 | } 55 | 56 | // Zero asserts value is the zero value 57 | func Zero(tb testing.TB, value interface{}) bool { 58 | tb.Helper() 59 | if !reflect.ValueOf(value).IsZero() { 60 | tb.Errorf("Value should be zero, got: %#v", value) 61 | return false 62 | } 63 | return true 64 | } 65 | 66 | // NotZero asserts value is not the zero value 67 | func NotZero(tb testing.TB, value interface{}) bool { 68 | tb.Helper() 69 | if reflect.ValueOf(value).IsZero() { 70 | tb.Error("Value should not be zero") 71 | return false 72 | } 73 | return true 74 | } 75 | 76 | // Equal asserts actual is equal to expected 77 | func Equal(tb testing.TB, expected, actual interface{}) bool { 78 | tb.Helper() 79 | if !reflect.DeepEqual(expected, actual) { 80 | tb.Errorf("%+v != %+v\nExpected: %#v\nActual: %#v", expected, actual, expected, actual) 81 | return false 82 | } 83 | return true 84 | } 85 | 86 | // NotEqual asserts actual is not equal to expected 87 | func NotEqual(tb testing.TB, expected, actual interface{}) bool { 88 | tb.Helper() 89 | if reflect.DeepEqual(expected, actual) { 90 | tb.Errorf("Should not be equal: %+v\nExpected: %#v\nActual: %#v", actual, expected, actual) 91 | return false 92 | } 93 | return true 94 | } 95 | 96 | func contains(tb testing.TB, collection, item interface{}) bool { 97 | collectionVal := reflect.ValueOf(collection) 98 | switch collectionVal.Kind() { 99 | case reflect.Slice: 100 | length := collectionVal.Len() 101 | for i := 0; i < length; i++ { 102 | candidateItem := collectionVal.Index(i).Interface() 103 | if reflect.DeepEqual(candidateItem, item) { 104 | return true 105 | } 106 | } 107 | return false 108 | case reflect.String: 109 | itemVal := reflect.ValueOf(item) 110 | if itemVal.Kind() != reflect.String { 111 | tb.Errorf("Invalid item type for string collection. Expected string, got: %T", item) 112 | return false 113 | } 114 | return strings.Contains(collection.(string), item.(string)) 115 | default: 116 | tb.Errorf("Invalid collection type. Expected slice, got: %T", collection) 117 | return false 118 | } 119 | } 120 | 121 | // Contains asserts item is contained by collection 122 | func Contains(tb testing.TB, collection, item interface{}) bool { 123 | tb.Helper() 124 | 125 | if !contains(tb, collection, item) { 126 | tb.Errorf("Collection does not contain expected item:\nCollection: %#v\nExpected item: %#v", collection, item) 127 | return false 128 | } 129 | return true 130 | } 131 | 132 | // NotContains asserts item is not contained by collection 133 | func NotContains(tb testing.TB, collection, item interface{}) bool { 134 | tb.Helper() 135 | 136 | if contains(tb, collection, item) { 137 | tb.Errorf("Collection contains unexpected item:\nCollection: %#v\nUnexpected item: %#v", collection, item) 138 | return false 139 | } 140 | return true 141 | } 142 | 143 | // Eventually asserts fn() returns true within totalWait time, checking at the given interval 144 | func Eventually(tb testing.TB, fn func(context.Context) bool, totalWait time.Duration, checkInterval time.Duration) bool { 145 | tb.Helper() 146 | 147 | ctx, cancel := context.WithTimeout(context.Background(), totalWait) 148 | defer cancel() 149 | for { 150 | success := fn(ctx) 151 | if success { 152 | return true 153 | } 154 | timer := time.NewTimer(checkInterval) 155 | select { 156 | case <-ctx.Done(): 157 | timer.Stop() 158 | tb.Errorf("Condition did not become true within %s", totalWait) 159 | return false 160 | case <-timer.C: 161 | } 162 | } 163 | } 164 | 165 | // Panics asserts fn() panics 166 | func Panics(tb testing.TB, fn func()) (panicked bool) { 167 | tb.Helper() 168 | defer func() { 169 | val := recover() 170 | if val != nil { 171 | panicked = true 172 | } else { 173 | tb.Error("Function should panic") 174 | } 175 | }() 176 | fn() 177 | return false 178 | } 179 | 180 | // NotPanics asserts fn() does not panic 181 | func NotPanics(tb testing.TB, fn func()) bool { 182 | tb.Helper() 183 | defer func() { 184 | val := recover() 185 | if val != nil { 186 | tb.Errorf("Function should not panic, got: %#v", val) 187 | } 188 | }() 189 | fn() 190 | return true 191 | } 192 | -------------------------------------------------------------------------------- /idb/internal/jscache/cacher.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | // Package jscache caches expensive JavaScript results, like string encoding 5 | package jscache 6 | 7 | import ( 8 | "github.com/hack-pad/safejs" 9 | ) 10 | 11 | type cacher struct { 12 | cache map[string]safejs.Value 13 | } 14 | 15 | func (c *cacher) value(key string, valueFn func() safejs.Value) safejs.Value { 16 | if val, ok := c.cache[key]; ok { 17 | return val 18 | } 19 | if c.cache == nil { 20 | c.cache = make(map[string]safejs.Value) 21 | } 22 | val := valueFn() 23 | c.cache[key] = val 24 | return val 25 | } 26 | -------------------------------------------------------------------------------- /idb/internal/jscache/strings.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package jscache 5 | 6 | import ( 7 | "github.com/hack-pad/safejs" 8 | ) 9 | 10 | var ( 11 | jsReflectGet safejs.Value 12 | ) 13 | 14 | func init() { 15 | jsReflect, err := safejs.Global().Get("Reflect") 16 | if err != nil { 17 | panic(err) 18 | } 19 | jsReflectGet, err = jsReflect.Get("get") 20 | if err != nil { 21 | panic(err) 22 | } 23 | } 24 | 25 | // Strings caches encoding strings as safejs.Value's. 26 | // String encoding today is quite CPU intensive, so caching commonly used strings helps with performance. 27 | type Strings struct { 28 | cacher 29 | } 30 | 31 | // Value retrieves the safejs.Value for the given string 32 | func (c *Strings) Value(s string) safejs.Value { 33 | return c.value(s, identityStringGetter{s}.value) 34 | } 35 | 36 | // GetProperty retrieves the given object's property, using a cached string value if available. Saves on the performance cost of 2 round trips to JS. 37 | func (c *Strings) GetProperty(obj safejs.Value, key string) (safejs.Value, error) { 38 | jsKey := c.Value(key) 39 | return jsReflectGet.Invoke(obj, jsKey) 40 | } 41 | 42 | type identityStringGetter struct { 43 | s string 44 | } 45 | 46 | func (i identityStringGetter) value() safejs.Value { 47 | value, err := safejs.ValueOf(i.s) 48 | if err != nil { 49 | panic(err) 50 | } 51 | return value 52 | } 53 | -------------------------------------------------------------------------------- /idb/key_range.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | 9 | "github.com/hack-pad/safejs" 10 | ) 11 | 12 | var ( 13 | jsIDBKeyRange safejs.Value 14 | ) 15 | 16 | func init() { 17 | var err error 18 | jsIDBKeyRange, err = safejs.Global().Get("IDBKeyRange") 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | // KeyRange represents a continuous interval over some data type that is used for keys. Records can be retrieved from ObjectStore and Index objects using keys or a range of keys. 25 | type KeyRange struct { 26 | jsKeyRange safejs.Value 27 | } 28 | 29 | func wrapKeyRange(jsKeyRange safejs.Value) *KeyRange { 30 | return &KeyRange{jsKeyRange} 31 | } 32 | 33 | // NewKeyRangeBound creates a new key range with the specified upper and lower bounds. 34 | // The bounds can be open (that is, the bounds exclude the endpoint values) or closed (that is, the bounds include the endpoint values). 35 | func NewKeyRangeBound(lower, upper js.Value, lowerOpen, upperOpen bool) (*KeyRange, error) { 36 | keyRange, err := jsIDBKeyRange.Call("bound", lower, upper, lowerOpen, upperOpen) 37 | if err != nil { 38 | return nil, tryAsDOMException(err) 39 | } 40 | return wrapKeyRange(keyRange), nil 41 | } 42 | 43 | // NewKeyRangeLowerBound creates a new key range with only a lower bound. 44 | func NewKeyRangeLowerBound(lower js.Value, open bool) (*KeyRange, error) { 45 | keyRange, err := jsIDBKeyRange.Call("lowerBound", lower, open) 46 | if err != nil { 47 | return nil, tryAsDOMException(err) 48 | } 49 | return wrapKeyRange(keyRange), nil 50 | } 51 | 52 | // NewKeyRangeUpperBound creates a new key range with only an upper bound. 53 | func NewKeyRangeUpperBound(upper js.Value, open bool) (*KeyRange, error) { 54 | keyRange, err := jsIDBKeyRange.Call("upperBound", upper, open) 55 | if err != nil { 56 | return nil, tryAsDOMException(err) 57 | } 58 | return wrapKeyRange(keyRange), nil 59 | } 60 | 61 | // NewKeyRangeOnly creates a new key range containing a single value. 62 | func NewKeyRangeOnly(only js.Value) (*KeyRange, error) { 63 | keyRange, err := jsIDBKeyRange.Call("only", only) 64 | if err != nil { 65 | return nil, tryAsDOMException(err) 66 | } 67 | return wrapKeyRange(keyRange), nil 68 | } 69 | 70 | // Lower returns the lower bound of the key range. 71 | func (k *KeyRange) Lower() (js.Value, error) { 72 | lower, err := k.jsKeyRange.Get("lower") 73 | return safejs.Unsafe(lower), err 74 | } 75 | 76 | // Upper returns the upper bound of the key range. 77 | func (k *KeyRange) Upper() (js.Value, error) { 78 | upper, err := k.jsKeyRange.Get("upper") 79 | return safejs.Unsafe(upper), err 80 | } 81 | 82 | // LowerOpen returns false if the lower-bound value is included in the key range. 83 | func (k *KeyRange) LowerOpen() (bool, error) { 84 | lowerOpen, err := k.jsKeyRange.Get("lowerOpen") 85 | if err != nil { 86 | return false, err 87 | } 88 | return lowerOpen.Bool() 89 | } 90 | 91 | // UpperOpen returns false if the upper-bound value is included in the key range. 92 | func (k *KeyRange) UpperOpen() (bool, error) { 93 | upperOpen, err := k.jsKeyRange.Get("upperOpen") 94 | if err != nil { 95 | return false, err 96 | } 97 | return upperOpen.Bool() 98 | } 99 | 100 | // Includes returns a boolean indicating whether a specified key is inside the key range. 101 | func (k *KeyRange) Includes(key js.Value) (bool, error) { 102 | includes, err := k.jsKeyRange.Call("includes", key) 103 | if err != nil { 104 | return false, tryAsDOMException(err) 105 | } 106 | return includes.Bool() 107 | } 108 | -------------------------------------------------------------------------------- /idb/key_range_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "fmt" 8 | "syscall/js" 9 | "testing" 10 | 11 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 12 | ) 13 | 14 | func TestNewKeyRangeBound(t *testing.T) { 15 | t.Parallel() 16 | keyRangeClosedOpen, err := NewKeyRangeBound(js.ValueOf(0), js.ValueOf(100), false, true) 17 | assert.NoError(t, err) 18 | for _, tc := range []struct { 19 | name string // auto-filled, satisfies paralleltest linter 20 | input int 21 | expectIncludes bool 22 | }{ 23 | {input: -1, expectIncludes: false}, 24 | {input: 0, expectIncludes: true}, 25 | {input: 50, expectIncludes: true}, 26 | {input: 100, expectIncludes: false}, 27 | } { 28 | tc.name = fmt.Sprint("closed open ", tc.input) 29 | tc := tc // keep loop-local copy of test case for parallel runs 30 | t.Run(fmt.Sprint(tc.input), func(t *testing.T) { 31 | t.Parallel() 32 | includes, err := keyRangeClosedOpen.Includes(js.ValueOf(tc.input)) 33 | assert.NoError(t, err) 34 | assert.Equal(t, tc.expectIncludes, includes) 35 | }) 36 | } 37 | 38 | keyRangeOpenClosed, err := NewKeyRangeBound(js.ValueOf(0), js.ValueOf(100), true, false) 39 | assert.NoError(t, err) 40 | for _, tc := range []struct { 41 | name string // auto-filled, satisfies paralleltest linter 42 | input int 43 | expectIncludes bool 44 | }{ 45 | {input: -1, expectIncludes: false}, 46 | {input: 0, expectIncludes: false}, 47 | {input: 50, expectIncludes: true}, 48 | {input: 100, expectIncludes: true}, 49 | } { 50 | tc.name = fmt.Sprint("open closed ", tc.input) 51 | tc := tc // keep loop-local copy of test case for parallel runs 52 | t.Run(tc.name, func(t *testing.T) { 53 | t.Parallel() 54 | includes, err := keyRangeOpenClosed.Includes(js.ValueOf(tc.input)) 55 | assert.NoError(t, err) 56 | assert.Equal(t, tc.expectIncludes, includes) 57 | }) 58 | } 59 | } 60 | 61 | func TestNewKeyRangeLowerBound(t *testing.T) { 62 | t.Parallel() 63 | keyRangeOpen, err := NewKeyRangeLowerBound(js.ValueOf(0), true) 64 | assert.NoError(t, err) 65 | for _, tc := range []struct { 66 | input int 67 | expectIncludes bool 68 | }{ 69 | {input: -1, expectIncludes: false}, 70 | {input: 0, expectIncludes: false}, 71 | {input: 100, expectIncludes: true}, 72 | } { 73 | tc := tc // keep loop-local copy of test case for parallel runs 74 | t.Run(fmt.Sprint("open ", tc.input), func(t *testing.T) { 75 | t.Parallel() 76 | includes, err := keyRangeOpen.Includes(js.ValueOf(tc.input)) 77 | assert.NoError(t, err) 78 | assert.Equal(t, tc.expectIncludes, includes) 79 | }) 80 | } 81 | 82 | keyRangeClosed, err := NewKeyRangeLowerBound(js.ValueOf(0), false) 83 | assert.NoError(t, err) 84 | for _, tc := range []struct { 85 | name string // auto-filled, satisfies paralleltest linter 86 | input int 87 | expectIncludes bool 88 | }{ 89 | {input: -1, expectIncludes: false}, 90 | {input: 0, expectIncludes: true}, 91 | {input: 100, expectIncludes: true}, 92 | } { 93 | tc.name = fmt.Sprint("closed ", tc.input) 94 | tc := tc // keep loop-local copy of test case for parallel runs 95 | t.Run(tc.name, func(t *testing.T) { 96 | t.Parallel() 97 | includes, err := keyRangeClosed.Includes(js.ValueOf(tc.input)) 98 | assert.NoError(t, err) 99 | assert.Equal(t, tc.expectIncludes, includes) 100 | }) 101 | } 102 | } 103 | 104 | func TestNewKeyRangeUpperBound(t *testing.T) { 105 | t.Parallel() 106 | keyRangeOpen, err := NewKeyRangeUpperBound(js.ValueOf(100), true) 107 | assert.NoError(t, err) 108 | for _, tc := range []struct { 109 | name string // auto-filled, satisfies paralleltest linter 110 | input int 111 | expectIncludes bool 112 | }{ 113 | {input: 0, expectIncludes: true}, 114 | {input: 100, expectIncludes: false}, 115 | {input: 101, expectIncludes: false}, 116 | } { 117 | tc := tc // keep loop-local copy of test case for parallel runs 118 | t.Run(fmt.Sprint("open ", tc.input), func(t *testing.T) { 119 | t.Parallel() 120 | includes, err := keyRangeOpen.Includes(js.ValueOf(tc.input)) 121 | assert.NoError(t, err) 122 | assert.Equal(t, tc.expectIncludes, includes) 123 | }) 124 | } 125 | 126 | keyRangeClosed, err := NewKeyRangeUpperBound(js.ValueOf(100), false) 127 | assert.NoError(t, err) 128 | for _, tc := range []struct { 129 | name string // auto-filled, satisfies paralleltest linter 130 | input int 131 | expectIncludes bool 132 | }{ 133 | {input: 0, expectIncludes: true}, 134 | {input: 100, expectIncludes: true}, 135 | {input: 101, expectIncludes: false}, 136 | } { 137 | tc.name = fmt.Sprint("closed ", tc.input) 138 | tc := tc // keep loop-local copy of test case for parallel runs 139 | t.Run(tc.name, func(t *testing.T) { 140 | t.Parallel() 141 | includes, err := keyRangeClosed.Includes(js.ValueOf(tc.input)) 142 | assert.NoError(t, err) 143 | assert.Equal(t, tc.expectIncludes, includes) 144 | }) 145 | } 146 | } 147 | 148 | func TestNewKeyRangeOnly(t *testing.T) { 149 | t.Parallel() 150 | keyRange, err := NewKeyRangeOnly(js.ValueOf(100)) 151 | assert.NoError(t, err) 152 | for _, tc := range []struct { 153 | name string // auto-filled, satisfies paralleltest linter 154 | input int 155 | expectIncludes bool 156 | }{ 157 | {input: 0, expectIncludes: false}, 158 | {input: 100, expectIncludes: true}, 159 | } { 160 | tc.name = fmt.Sprint(tc.input) 161 | tc := tc // keep loop-local copy of test case for parallel runs 162 | t.Run(tc.name, func(t *testing.T) { 163 | t.Parallel() 164 | includes, err := keyRange.Includes(js.ValueOf(tc.input)) 165 | assert.NoError(t, err) 166 | assert.Equal(t, tc.expectIncludes, includes) 167 | }) 168 | } 169 | } 170 | 171 | func TestKeyRangeBoundProperties(t *testing.T) { 172 | t.Parallel() 173 | keyRange, err := NewKeyRangeBound(js.ValueOf(0), js.ValueOf(100), false, true) 174 | assert.NoError(t, err) 175 | 176 | lower, err := keyRange.Lower() 177 | assert.NoError(t, err) 178 | assert.Equal(t, js.ValueOf(0), lower) 179 | 180 | lowerOpen, err := keyRange.LowerOpen() 181 | assert.NoError(t, err) 182 | assert.Equal(t, false, lowerOpen) 183 | 184 | upper, err := keyRange.Upper() 185 | assert.NoError(t, err) 186 | assert.Equal(t, js.ValueOf(100), upper) 187 | 188 | upperOpen, err := keyRange.UpperOpen() 189 | assert.NoError(t, err) 190 | assert.Equal(t, true, upperOpen) 191 | } 192 | -------------------------------------------------------------------------------- /idb/object_store.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "syscall/js" 8 | 9 | "github.com/hack-pad/safejs" 10 | ) 11 | 12 | // ObjectStoreOptions contains all available options for creating an ObjectStore 13 | type ObjectStoreOptions struct { 14 | KeyPath js.Value 15 | AutoIncrement bool 16 | } 17 | 18 | // ObjectStore represents an object store in a database. Records within an object store are sorted according to their keys. This sorting enables fast insertion, look-up, and ordered retrieval. 19 | type ObjectStore struct { 20 | base *baseObjectStore // don't embed to avoid generated docs with the wrong receiver type (ObjectStore vs *ObjectStore) 21 | } 22 | 23 | func wrapObjectStore(txn *Transaction, jsObjectStore safejs.Value) *ObjectStore { 24 | return &ObjectStore{wrapBaseObjectStore(txn, jsObjectStore)} 25 | } 26 | 27 | // IndexNames returns a list of the names of indexes on objects in this object store. 28 | func (o *ObjectStore) IndexNames() ([]string, error) { 29 | indexNames, err := o.base.jsObjectStore.Get("indexNames") 30 | if err != nil { 31 | return nil, err 32 | } 33 | return stringsFromArray(indexNames) 34 | } 35 | 36 | // KeyPath returns the key path of this object store. If this returns js.Null(), the application must provide a key for each modification operation. 37 | func (o *ObjectStore) KeyPath() (js.Value, error) { 38 | keyPath, err := o.base.jsObjectStore.Get("keyPath") 39 | return safejs.Unsafe(keyPath), err 40 | } 41 | 42 | // Name returns the name of this object store. 43 | func (o *ObjectStore) Name() (string, error) { 44 | name, err := o.base.jsObjectStore.Get("name") 45 | if err != nil { 46 | return "", err 47 | } 48 | return name.String() 49 | } 50 | 51 | // Transaction returns the Transaction object to which this object store belongs. 52 | func (o *ObjectStore) Transaction() (*Transaction, error) { 53 | if o.base.txn == (*Transaction)(nil) { 54 | return nil, errNotInTransaction 55 | } 56 | return o.base.txn, nil 57 | } 58 | 59 | // AutoIncrement returns the value of the auto increment flag for this object store. 60 | func (o *ObjectStore) AutoIncrement() (bool, error) { 61 | autoIncrement, err := o.base.jsObjectStore.Get("autoIncrement") 62 | if err != nil { 63 | return false, err 64 | } 65 | return autoIncrement.Bool() 66 | } 67 | 68 | // Add returns an AckRequest, and, in a separate thread, creates a structured clone of the value, and stores the cloned value in the object store. This is for adding new records to an object store. 69 | func (o *ObjectStore) Add(value js.Value) (*AckRequest, error) { 70 | reqValue, err := o.base.jsObjectStore.Call("add", value) 71 | if err != nil { 72 | return nil, tryAsDOMException(err) 73 | } 74 | req := wrapRequest(o.base.txn, reqValue) 75 | return newAckRequest(req), nil 76 | } 77 | 78 | // AddKey is the same as Add, but includes the key to use to identify the record. 79 | func (o *ObjectStore) AddKey(key, value js.Value) (*AckRequest, error) { 80 | reqValue, err := o.base.jsObjectStore.Call("add", value, key) 81 | if err != nil { 82 | return nil, tryAsDOMException(err) 83 | } 84 | req := wrapRequest(o.base.txn, reqValue) 85 | return newAckRequest(req), nil 86 | } 87 | 88 | // Clear returns an AckRequest, then clears this object store in a separate thread. This is for deleting all current records out of an object store. 89 | func (o *ObjectStore) Clear() (*AckRequest, error) { 90 | reqValue, err := o.base.jsObjectStore.Call("clear") 91 | if err != nil { 92 | return nil, tryAsDOMException(err) 93 | } 94 | req := wrapRequest(o.base.txn, reqValue) 95 | return newAckRequest(req), nil 96 | } 97 | 98 | // Count returns a UintRequest, and, in a separate thread, returns the total number of records in the store. 99 | func (o *ObjectStore) Count() (*UintRequest, error) { 100 | return o.base.Count() 101 | } 102 | 103 | // CountKey returns a UintRequest, and, in a separate thread, returns the total number of records that match the provided key. 104 | func (o *ObjectStore) CountKey(key js.Value) (*UintRequest, error) { 105 | return o.base.CountKey(safejs.Safe(key)) 106 | } 107 | 108 | // CountRange returns a UintRequest, and, in a separate thread, returns the total number of records that match the provided KeyRange. 109 | func (o *ObjectStore) CountRange(keyRange *KeyRange) (*UintRequest, error) { 110 | return o.base.CountRange(keyRange) 111 | } 112 | 113 | // CreateIndex creates a new index during a version upgrade, returning a new Index object in the connected database. 114 | func (o *ObjectStore) CreateIndex(name string, keyPath js.Value, options IndexOptions) (*Index, error) { 115 | jsIndex, err := o.base.jsObjectStore.Call("createIndex", name, keyPath, map[string]interface{}{ 116 | "unique": options.Unique, 117 | "multiEntry": options.MultiEntry, 118 | }) 119 | if err != nil { 120 | return nil, tryAsDOMException(err) 121 | } 122 | return wrapIndex(o.base.txn, jsIndex), nil 123 | } 124 | 125 | // Delete returns an AckRequest, and, in a separate thread, deletes the store object selected by the specified key. This is for deleting individual records out of an object store. 126 | func (o *ObjectStore) Delete(key js.Value) (*AckRequest, error) { 127 | reqValue, err := o.base.jsObjectStore.Call("delete", key) 128 | if err != nil { 129 | return nil, tryAsDOMException(err) 130 | } 131 | req := wrapRequest(o.base.txn, reqValue) 132 | return newAckRequest(req), nil 133 | } 134 | 135 | // DeleteIndex destroys the specified index in the connected database, used during a version upgrade. 136 | func (o *ObjectStore) DeleteIndex(name string) error { 137 | _, err := o.base.jsObjectStore.Call("deleteIndex", name) 138 | return tryAsDOMException(err) 139 | } 140 | 141 | // GetAllKeys returns an ArrayRequest that retrieves record keys for all objects in the object store. 142 | func (o *ObjectStore) GetAllKeys() (*ArrayRequest, error) { 143 | return o.base.GetAllKeys() 144 | } 145 | 146 | // GetAllKeysRange returns an ArrayRequest that retrieves record keys for all objects in the object store matching the specified query. If maxCount is 0, retrieves all objects matching the query. 147 | func (o *ObjectStore) GetAllKeysRange(query *KeyRange, maxCount uint) (*ArrayRequest, error) { 148 | return o.base.GetAllKeysRange(query, maxCount) 149 | } 150 | 151 | // Get returns a Request, and, in a separate thread, returns the objects selected by the specified key. This is for retrieving specific records from an object store. 152 | func (o *ObjectStore) Get(key js.Value) (*Request, error) { 153 | return o.base.Get(safejs.Safe(key)) 154 | } 155 | 156 | // GetKey returns a Request, and, in a separate thread retrieves and returns the record key for the object matching the specified parameter. 157 | func (o *ObjectStore) GetKey(value js.Value) (*Request, error) { 158 | return o.base.GetKey(safejs.Safe(value)) 159 | } 160 | 161 | // Index opens an index from this object store after which it can, for example, be used to return a sequence of records sorted by that index using a cursor. 162 | func (o *ObjectStore) Index(name string) (*Index, error) { 163 | jsIndex, err := o.base.jsObjectStore.Call("index", name) 164 | if err != nil { 165 | return nil, tryAsDOMException(err) 166 | } 167 | return wrapIndex(o.base.txn, jsIndex), nil 168 | } 169 | 170 | // Put returns a Request, and, in a separate thread, creates a structured clone of the value, and stores the cloned value in the object store. This is for updating existing records in an object store when the transaction's mode is readwrite. 171 | func (o *ObjectStore) Put(value js.Value) (*Request, error) { 172 | reqValue, err := o.base.jsObjectStore.Call("put", value) 173 | if err != nil { 174 | return nil, tryAsDOMException(err) 175 | } 176 | return wrapRequest(o.base.txn, reqValue), nil 177 | } 178 | 179 | // PutKey is the same as Put, but includes the key to use to identify the record. 180 | func (o *ObjectStore) PutKey(key, value js.Value) (*Request, error) { 181 | reqValue, err := o.base.jsObjectStore.Call("put", value, key) 182 | if err != nil { 183 | return nil, tryAsDOMException(err) 184 | } 185 | return wrapRequest(o.base.txn, reqValue), nil 186 | } 187 | 188 | // OpenCursor returns a CursorWithValueRequest, and, in a separate thread, returns a new CursorWithValue. Used for iterating through an object store by primary key with a cursor. 189 | func (o *ObjectStore) OpenCursor(direction CursorDirection) (*CursorWithValueRequest, error) { 190 | return o.base.OpenCursor(direction) 191 | } 192 | 193 | // OpenCursorKey is the same as OpenCursor, but opens a cursor over the given key instead. 194 | func (o *ObjectStore) OpenCursorKey(key js.Value, direction CursorDirection) (*CursorWithValueRequest, error) { 195 | return o.base.OpenCursorKey(safejs.Safe(key), direction) 196 | } 197 | 198 | // OpenCursorRange is the same as OpenCursor, but opens a cursor over the given range instead. 199 | func (o *ObjectStore) OpenCursorRange(keyRange *KeyRange, direction CursorDirection) (*CursorWithValueRequest, error) { 200 | return o.base.OpenCursorRange(keyRange, direction) 201 | } 202 | 203 | // OpenKeyCursor returns a CursorRequest, and, in a separate thread, returns a new Cursor. Used for iterating through all keys in an object store. 204 | func (o *ObjectStore) OpenKeyCursor(direction CursorDirection) (*CursorRequest, error) { 205 | return o.base.OpenKeyCursor(direction) 206 | } 207 | 208 | // OpenKeyCursorKey is the same as OpenKeyCursor, but opens a cursor over the given key instead. 209 | func (o *ObjectStore) OpenKeyCursorKey(key js.Value, direction CursorDirection) (*CursorRequest, error) { 210 | return o.base.OpenKeyCursorKey(safejs.Safe(key), direction) 211 | } 212 | 213 | // OpenKeyCursorRange is the same as OpenKeyCursor, but opens a cursor over the given key range instead. 214 | func (o *ObjectStore) OpenKeyCursorRange(keyRange *KeyRange, direction CursorDirection) (*CursorRequest, error) { 215 | return o.base.OpenKeyCursorRange(keyRange, direction) 216 | } 217 | -------------------------------------------------------------------------------- /idb/object_store_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "syscall/js" 9 | "testing" 10 | 11 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 12 | ) 13 | 14 | func TestObjectStoreIndexNames(t *testing.T) { 15 | t.Parallel() 16 | db := testDB(t, func(db *Database) { 17 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 18 | assert.NoError(t, err) 19 | _, err = store.CreateIndex("myindex", js.ValueOf("indexKey"), IndexOptions{}) 20 | assert.NoError(t, err) 21 | }) 22 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 23 | assert.NoError(t, err) 24 | store, err := txn.ObjectStore("mystore") 25 | assert.NoError(t, err) 26 | 27 | names, err := store.IndexNames() 28 | assert.NoError(t, err) 29 | assert.Equal(t, []string{"myindex"}, names) 30 | } 31 | 32 | func TestObjectStoreKeyPath(t *testing.T) { 33 | t.Parallel() 34 | db := testDB(t, func(db *Database) { 35 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{ 36 | KeyPath: js.ValueOf("primary"), 37 | }) 38 | assert.NoError(t, err) 39 | }) 40 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 41 | assert.NoError(t, err) 42 | store, err := txn.ObjectStore("mystore") 43 | assert.NoError(t, err) 44 | 45 | keyPath, err := store.KeyPath() 46 | assert.NoError(t, err) 47 | assert.Equal(t, js.ValueOf("primary"), keyPath) 48 | } 49 | 50 | func TestObjectStoreName(t *testing.T) { 51 | t.Parallel() 52 | db := testDB(t, func(db *Database) { 53 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 54 | assert.NoError(t, err) 55 | }) 56 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 57 | assert.NoError(t, err) 58 | store, err := txn.ObjectStore("mystore") 59 | assert.NoError(t, err) 60 | 61 | name, err := store.Name() 62 | assert.NoError(t, err) 63 | assert.Equal(t, "mystore", name) 64 | } 65 | 66 | func TestObjectStoreAutoIncrement(t *testing.T) { 67 | t.Parallel() 68 | db := testDB(t, func(db *Database) { 69 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{ 70 | AutoIncrement: true, 71 | }) 72 | assert.NoError(t, err) 73 | }) 74 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 75 | assert.NoError(t, err) 76 | store, err := txn.ObjectStore("mystore") 77 | assert.NoError(t, err) 78 | 79 | autoIncrement, err := store.AutoIncrement() 80 | assert.NoError(t, err) 81 | assert.Equal(t, true, autoIncrement) 82 | } 83 | 84 | func TestObjectStoreTransaction(t *testing.T) { 85 | t.Parallel() 86 | db := testDB(t, func(db *Database) { 87 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{ 88 | KeyPath: js.ValueOf("primary"), 89 | }) 90 | assert.NoError(t, err) 91 | }) 92 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 93 | assert.NoError(t, err) 94 | store, err := txn.ObjectStore("mystore") 95 | assert.NoError(t, err) 96 | 97 | txnGet, err := store.Transaction() 98 | assert.NoError(t, err) 99 | assert.Equal(t, txn.jsTransaction, txnGet.jsTransaction) 100 | } 101 | 102 | func TestObjectStoreAdd(t *testing.T) { 103 | t.Parallel() 104 | db := testDB(t, func(db *Database) { 105 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{ 106 | KeyPath: js.ValueOf("id"), 107 | }) 108 | assert.NoError(t, err) 109 | }) 110 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 111 | assert.NoError(t, err) 112 | store, err := txn.ObjectStore("mystore") 113 | assert.NoError(t, err) 114 | 115 | addReq, err := store.Add(js.ValueOf(map[string]interface{}{ 116 | "id": "some id", 117 | })) 118 | assert.NoError(t, err) 119 | getReq, err := store.GetKey(js.ValueOf("some id")) 120 | assert.NoError(t, err) 121 | 122 | assert.NoError(t, addReq.Await(context.Background())) 123 | result, err := getReq.Await(context.Background()) 124 | assert.NoError(t, err) 125 | assert.Equal(t, js.ValueOf("some id"), result) 126 | } 127 | 128 | func TestObjectStoreClear(t *testing.T) { 129 | t.Parallel() 130 | db := testDB(t, func(db *Database) { 131 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 132 | assert.NoError(t, err) 133 | }) 134 | { 135 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 136 | assert.NoError(t, err) 137 | store, err := txn.ObjectStore("mystore") 138 | assert.NoError(t, err) 139 | _, err = store.AddKey(js.ValueOf("some key"), js.ValueOf("some value")) 140 | assert.NoError(t, err) 141 | assert.NoError(t, txn.Await(context.Background())) 142 | } 143 | 144 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 145 | assert.NoError(t, err) 146 | store, err := txn.ObjectStore("mystore") 147 | assert.NoError(t, err) 148 | clearReq, err := store.Clear() 149 | assert.NoError(t, err) 150 | getReq, err := store.GetAllKeys() 151 | assert.NoError(t, err) 152 | 153 | assert.NoError(t, clearReq.Await(context.Background())) 154 | result, err := getReq.Await(context.Background()) 155 | assert.NoError(t, err) 156 | assert.Equal(t, []js.Value(nil), result) 157 | } 158 | 159 | func TestObjectStoreCount(t *testing.T) { 160 | t.Parallel() 161 | 162 | for _, tc := range []struct { 163 | name string 164 | countFn func(*ObjectStore) (*UintRequest, error) 165 | }{ 166 | { 167 | name: "count", 168 | countFn: func(store *ObjectStore) (*UintRequest, error) { 169 | return store.Count() 170 | }, 171 | }, 172 | { 173 | name: "count key", 174 | countFn: func(store *ObjectStore) (*UintRequest, error) { 175 | return store.CountKey(js.ValueOf("some key")) 176 | }, 177 | }, 178 | { 179 | name: "count range", 180 | countFn: func(store *ObjectStore) (*UintRequest, error) { 181 | keyRange, err := NewKeyRangeOnly(js.ValueOf("some key")) 182 | assert.NoError(t, err) 183 | return store.CountRange(keyRange) 184 | }, 185 | }, 186 | } { 187 | tc := tc // keep loop-local copy of test case for parallel runs 188 | t.Run(tc.name, func(t *testing.T) { 189 | t.Parallel() 190 | db := testDB(t, func(db *Database) { 191 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 192 | assert.NoError(t, err) 193 | }) 194 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 195 | assert.NoError(t, err) 196 | store, err := txn.ObjectStore("mystore") 197 | assert.NoError(t, err) 198 | 199 | _, err = store.AddKey(js.ValueOf("some key"), js.ValueOf("some value")) 200 | assert.NoError(t, err) 201 | 202 | req, err := tc.countFn(store) 203 | assert.NoError(t, err) 204 | count, err := req.Await(context.Background()) 205 | assert.NoError(t, err) 206 | 207 | assert.Equal(t, uint(1), count) 208 | }) 209 | } 210 | } 211 | 212 | func TestObjectStoreCreateIndex(t *testing.T) { 213 | t.Parallel() 214 | testDB(t, func(db *Database) { 215 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 216 | assert.NoError(t, err) 217 | index, err := store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{ 218 | Unique: true, 219 | MultiEntry: true, 220 | }) 221 | assert.NoError(t, err) 222 | 223 | unique, err := index.Unique() 224 | assert.NoError(t, err) 225 | assert.Equal(t, true, unique) 226 | multiEntry, err := index.MultiEntry() 227 | assert.NoError(t, err) 228 | assert.Equal(t, true, multiEntry) 229 | }) 230 | } 231 | 232 | func TestObjectStoreDelete(t *testing.T) { 233 | t.Parallel() 234 | db := testDB(t, func(db *Database) { 235 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 236 | assert.NoError(t, err) 237 | }) 238 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 239 | assert.NoError(t, err) 240 | store, err := txn.ObjectStore("mystore") 241 | assert.NoError(t, err) 242 | _, err = store.AddKey(js.ValueOf("some key"), js.ValueOf("some value")) 243 | assert.NoError(t, err) 244 | 245 | _, err = store.Delete(js.ValueOf("some key")) 246 | assert.NoError(t, err) 247 | req, err := store.GetKey(js.ValueOf("some key")) 248 | assert.NoError(t, err) 249 | result, err := req.Await(context.Background()) 250 | assert.NoError(t, err) 251 | assert.Equal(t, js.Undefined(), result) 252 | } 253 | 254 | func TestObjectStoreDeleteIndex(t *testing.T) { 255 | t.Parallel() 256 | testDB(t, func(db *Database) { 257 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 258 | assert.NoError(t, err) 259 | _, err = store.CreateIndex("myindex", js.ValueOf("primary"), IndexOptions{}) 260 | assert.NoError(t, err) 261 | err = store.DeleteIndex("myindex") 262 | assert.NoError(t, err) 263 | names, err := store.IndexNames() 264 | assert.NoError(t, err) 265 | assert.Equal(t, []string(nil), names) 266 | }) 267 | } 268 | 269 | func TestObjectStoreGet(t *testing.T) { 270 | t.Parallel() 271 | 272 | for _, tc := range []struct { 273 | name string 274 | keys map[string]interface{} 275 | getFn func(*ObjectStore) (interface{}, error) 276 | expectResult interface{} 277 | }{ 278 | { 279 | name: "get all keys", 280 | keys: map[string]interface{}{ 281 | "some id": "some value", 282 | "some other id": "some other value", 283 | }, 284 | getFn: func(store *ObjectStore) (interface{}, error) { 285 | return store.GetAllKeys() 286 | }, 287 | expectResult: []js.Value{js.ValueOf("some id"), js.ValueOf("some other id")}, 288 | }, 289 | { 290 | name: "get all keys query", 291 | keys: map[string]interface{}{ 292 | "some id": "some value", 293 | "some other id": "some other value", 294 | }, 295 | getFn: func(store *ObjectStore) (interface{}, error) { 296 | keyRange, err := NewKeyRangeOnly(js.ValueOf("some id")) 297 | assert.NoError(t, err) 298 | return store.GetAllKeysRange(keyRange, 10) 299 | }, 300 | expectResult: []js.Value{js.ValueOf("some id")}, 301 | }, 302 | { 303 | name: "get", 304 | keys: map[string]interface{}{ 305 | "some id": "some value", 306 | }, 307 | getFn: func(store *ObjectStore) (interface{}, error) { 308 | return store.Get(js.ValueOf("some id")) 309 | }, 310 | expectResult: js.ValueOf("some value"), 311 | }, 312 | { 313 | name: "get key", 314 | keys: map[string]interface{}{ 315 | "some id": "some value", 316 | }, 317 | getFn: func(store *ObjectStore) (interface{}, error) { 318 | return store.GetKey(js.ValueOf("some id")) 319 | }, 320 | expectResult: js.ValueOf("some id"), 321 | }, 322 | } { 323 | tc := tc // keep loop-local copy of test case for parallel runs 324 | t.Run(tc.name, func(t *testing.T) { 325 | t.Parallel() 326 | db := testDB(t, func(db *Database) { 327 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 328 | assert.NoError(t, err) 329 | }) 330 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 331 | assert.NoError(t, err) 332 | store, err := txn.ObjectStore("mystore") 333 | assert.NoError(t, err) 334 | for key, value := range tc.keys { 335 | _, err := store.AddKey(js.ValueOf(key), js.ValueOf(value)) 336 | assert.NoError(t, err) 337 | } 338 | req, err := tc.getFn(store) 339 | assert.NoError(t, err) 340 | var result interface{} 341 | switch req := req.(type) { 342 | case *ArrayRequest: 343 | result, err = req.Await(context.Background()) 344 | case *Request: 345 | result, err = req.Await(context.Background()) 346 | default: 347 | t.Fatalf("Invalid return type: %T", req) 348 | } 349 | assert.NoError(t, err) 350 | assert.Equal(t, tc.expectResult, result) 351 | }) 352 | } 353 | } 354 | 355 | func TestObjectStoreIndex(t *testing.T) { 356 | t.Parallel() 357 | db := testDB(t, func(db *Database) { 358 | store, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 359 | assert.NoError(t, err) 360 | _, err = store.CreateIndex("myindex", js.ValueOf("indexKey"), IndexOptions{}) 361 | assert.NoError(t, err) 362 | }) 363 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 364 | assert.NoError(t, err) 365 | store, err := txn.ObjectStore("mystore") 366 | assert.NoError(t, err) 367 | 368 | index, err := store.Index("myindex") 369 | assert.NoError(t, err) 370 | assert.NotZero(t, index) 371 | } 372 | 373 | func TestObjectStorePut(t *testing.T) { 374 | t.Parallel() 375 | db := testDB(t, func(db *Database) { 376 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{ 377 | KeyPath: js.ValueOf("id"), 378 | }) 379 | assert.NoError(t, err) 380 | }) 381 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 382 | assert.NoError(t, err) 383 | store, err := txn.ObjectStore("mystore") 384 | assert.NoError(t, err) 385 | 386 | req, err := store.Put(js.ValueOf(map[string]interface{}{ 387 | "id": "some id", 388 | "value": "some value", 389 | })) 390 | assert.NoError(t, err) 391 | resultKey, err := req.Await(context.Background()) 392 | assert.NoError(t, err) 393 | assert.Equal(t, js.ValueOf("some id"), resultKey) 394 | } 395 | 396 | func TestObjectStorePutKey(t *testing.T) { 397 | t.Parallel() 398 | db := testDB(t, func(db *Database) { 399 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 400 | assert.NoError(t, err) 401 | }) 402 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 403 | assert.NoError(t, err) 404 | store, err := txn.ObjectStore("mystore") 405 | assert.NoError(t, err) 406 | 407 | req, err := store.PutKey(js.ValueOf("some id"), js.ValueOf("some value")) 408 | assert.NoError(t, err) 409 | resultKey, err := req.Await(context.Background()) 410 | assert.NoError(t, err) 411 | assert.Equal(t, js.ValueOf("some id"), resultKey) 412 | } 413 | 414 | func TestObjectStoreOpenCursor(t *testing.T) { 415 | t.Parallel() 416 | 417 | for _, tc := range []struct { 418 | name string 419 | keys map[string]interface{} 420 | cursorFn func(*ObjectStore) (interface{}, error) 421 | expectResults []js.Value 422 | }{ 423 | { 424 | name: "open cursor next", 425 | keys: map[string]interface{}{ 426 | "some id": "some value", 427 | "some other id": "some other value", 428 | }, 429 | cursorFn: func(store *ObjectStore) (interface{}, error) { 430 | return store.OpenCursor(CursorNext) 431 | }, 432 | expectResults: []js.Value{ 433 | js.ValueOf("some value"), 434 | js.ValueOf("some other value"), 435 | }, 436 | }, 437 | { 438 | name: "open cursor previous", 439 | keys: map[string]interface{}{ 440 | "some id": "some value", 441 | "some other id": "some other value", 442 | }, 443 | cursorFn: func(store *ObjectStore) (interface{}, error) { 444 | return store.OpenCursor(CursorPrevious) 445 | }, 446 | expectResults: []js.Value{ 447 | js.ValueOf("some other value"), 448 | js.ValueOf("some value"), 449 | }, 450 | }, 451 | { 452 | name: "open cursor over key", 453 | keys: map[string]interface{}{ 454 | "some id": "some value", 455 | "some other id": "some other value", 456 | }, 457 | cursorFn: func(store *ObjectStore) (interface{}, error) { 458 | return store.OpenCursorKey(js.ValueOf("some id"), CursorNext) 459 | }, 460 | expectResults: []js.Value{ 461 | js.ValueOf("some value"), 462 | }, 463 | }, 464 | { 465 | name: "open cursor over key range", 466 | keys: map[string]interface{}{ 467 | "some id": "some value", 468 | "some other id": "some other value", 469 | }, 470 | cursorFn: func(store *ObjectStore) (interface{}, error) { 471 | keyRange, err := NewKeyRangeLowerBound(js.ValueOf("some more"), true) 472 | assert.NoError(t, err) 473 | return store.OpenCursorRange(keyRange, CursorNext) 474 | }, 475 | expectResults: []js.Value{ 476 | js.ValueOf("some other value"), 477 | }, 478 | }, 479 | { 480 | name: "open key cursor", 481 | keys: map[string]interface{}{ 482 | "some id": "some value", 483 | "some other id": "some other value", 484 | }, 485 | cursorFn: func(store *ObjectStore) (interface{}, error) { 486 | return store.OpenKeyCursor(CursorNext) 487 | }, 488 | expectResults: []js.Value{ 489 | js.ValueOf("some id"), 490 | js.ValueOf("some other id"), 491 | }, 492 | }, 493 | { 494 | name: "open key cursor key", 495 | keys: map[string]interface{}{ 496 | "some id": "some value", 497 | "some other id": "some other value", 498 | }, 499 | cursorFn: func(store *ObjectStore) (interface{}, error) { 500 | return store.OpenKeyCursorKey(js.ValueOf("some id"), CursorNext) 501 | }, 502 | expectResults: []js.Value{ 503 | js.ValueOf("some id"), 504 | }, 505 | }, 506 | { 507 | name: "open key cursor range", 508 | keys: map[string]interface{}{ 509 | "some id": "some value", 510 | "some other id": "some other value", 511 | }, 512 | cursorFn: func(store *ObjectStore) (interface{}, error) { 513 | keyRange, err := NewKeyRangeLowerBound(js.ValueOf("some more"), true) 514 | assert.NoError(t, err) 515 | return store.OpenKeyCursorRange(keyRange, CursorNext) 516 | }, 517 | expectResults: []js.Value{ 518 | js.ValueOf("some other id"), 519 | }, 520 | }, 521 | } { 522 | tc := tc // keep loop-local copy of test case for parallel runs 523 | t.Run(tc.name, func(t *testing.T) { 524 | t.Parallel() 525 | db := testDB(t, func(db *Database) { 526 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 527 | assert.NoError(t, err) 528 | }) 529 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 530 | assert.NoError(t, err) 531 | store, err := txn.ObjectStore("mystore") 532 | assert.NoError(t, err) 533 | for key, value := range tc.keys { 534 | _, err = store.AddKey(js.ValueOf(key), js.ValueOf(value)) 535 | assert.NoError(t, err) 536 | } 537 | 538 | req, err := tc.cursorFn(store) 539 | assert.NoError(t, err) 540 | var results []js.Value 541 | switch req := req.(type) { 542 | case *CursorWithValueRequest: 543 | err := req.Iter(context.Background(), func(cursor *CursorWithValue) error { 544 | value, err := cursor.Value() 545 | if assert.NoError(t, err) { 546 | results = append(results, value) 547 | err = cursor.Continue() 548 | return err 549 | } 550 | return err 551 | }) 552 | assert.NoError(t, err) 553 | case *CursorRequest: 554 | err := req.Iter(context.Background(), func(cursor *Cursor) error { 555 | key, err := cursor.Key() 556 | if assert.NoError(t, err) { 557 | results = append(results, key) 558 | return cursor.Continue() 559 | } 560 | return err 561 | }) 562 | assert.NoError(t, err) 563 | default: 564 | t.Fatalf("Invalid cursor type: %T", req) 565 | } 566 | assert.Equal(t, tc.expectResults, results) 567 | }) 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /idb/open_db_request.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/hack-pad/safejs" 12 | ) 13 | 14 | // OpenDBRequest provides access to the results of requests to open or delete databases (performed using Factory.open and Factory.DeleteDatabase). 15 | type OpenDBRequest struct { 16 | *Request 17 | } 18 | 19 | // Upgrader is a function that can upgrade the given database from an old version to a new one. 20 | type Upgrader func(db *Database, oldVersion, newVersion uint) error 21 | 22 | func newOpenDBRequest(ctx context.Context, req *Request, upgrader Upgrader) (*OpenDBRequest, error) { 23 | ctx, cancel := context.WithCancel(ctx) 24 | 25 | err := req.ListenSuccess(ctx, func() { 26 | defer cancel() 27 | err := openDBListenSuccess(req) 28 | if err != nil { 29 | panic(err) 30 | } 31 | }) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | upgrade, err := safejs.FuncOf(func(this safejs.Value, args []safejs.Value) interface{} { 37 | err := openDBUpgradeNeeded(req, upgrader, args) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return nil 42 | }) 43 | if err != nil { 44 | return nil, err 45 | } 46 | _, err = req.jsRequest.Call(addEventListener, "upgradeneeded", upgrade) 47 | if err != nil { 48 | return nil, tryAsDOMException(err) 49 | } 50 | go func() { 51 | <-ctx.Done() 52 | _, err := req.jsRequest.Call(removeEventListener, "upgradeneeded", upgrade) 53 | if err != nil { 54 | panic(err) 55 | } 56 | upgrade.Release() 57 | }() 58 | return &OpenDBRequest{req}, nil 59 | } 60 | 61 | func openDBListenSuccess(req *Request) error { 62 | jsDB, err := req.result() 63 | if err != nil { 64 | return err 65 | } 66 | versionChange, err := safejs.FuncOf(func(safejs.Value, []safejs.Value) interface{} { 67 | log.Println("Version change detected, closing DB...") 68 | _, closeErr := jsDB.Call("close") 69 | if closeErr != nil { 70 | log.Println("Error closing DB:", closeErr) 71 | } 72 | return nil 73 | }) 74 | if err != nil { 75 | return err 76 | } 77 | _, err = jsDB.Call(addEventListener, "versionchange", versionChange) 78 | return tryAsDOMException(err) 79 | } 80 | 81 | func openDBUpgradeNeeded(req *Request, upgrader Upgrader, args []safejs.Value) error { 82 | event := args[0] 83 | jsDatabase, err := req.result() 84 | if err != nil { 85 | return err 86 | } 87 | db := wrapDatabase(jsDatabase) 88 | oldVersionValue, err := event.Get("oldVersion") 89 | if err != nil { 90 | return err 91 | } 92 | oldVersion, err := oldVersionValue.Int() 93 | if err != nil { 94 | return err 95 | } 96 | newVersionValue, err := event.Get("newVersion") 97 | if err != nil { 98 | return err 99 | } 100 | newVersion, err := newVersionValue.Int() 101 | if err != nil { 102 | return err 103 | } 104 | if oldVersion < 0 || newVersion < 0 { 105 | return fmt.Errorf("Unexpected negative oldVersion or newVersion: %d, %d", oldVersion, newVersion) 106 | } 107 | return upgrader(db, uint(oldVersion), uint(newVersion)) 108 | } 109 | 110 | // Result returns the result of the request. If the request failed and the result is not available, an error is returned. 111 | func (o *OpenDBRequest) Result() (*Database, error) { 112 | db, err := o.Request.result() 113 | if err != nil { 114 | return nil, err 115 | } 116 | return wrapDatabase(db), nil 117 | } 118 | 119 | // Await waits for success or failure, then returns the results. 120 | func (o *OpenDBRequest) Await(ctx context.Context) (*Database, error) { 121 | db, err := o.Request.await(ctx) 122 | if err != nil { 123 | return nil, err 124 | } 125 | return wrapDatabase(db), nil 126 | } 127 | -------------------------------------------------------------------------------- /idb/request.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "syscall/js" 12 | 13 | "github.com/hack-pad/safejs" 14 | ) 15 | 16 | var ( 17 | // ErrCursorStopIter stops iteration when returned from a CursorRequest.Iter() handler 18 | ErrCursorStopIter = errors.New("stop cursor iteration") 19 | ) 20 | 21 | var ( 22 | jsIDBRequest safejs.Value 23 | jsIDBIndex safejs.Value 24 | ) 25 | 26 | func init() { 27 | var err error 28 | jsIDBRequest, err = safejs.Global().Get("IDBRequest") 29 | if err != nil { 30 | panic(err) 31 | } 32 | jsIDBIndex, err = safejs.Global().Get("IDBIndex") 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | // Request provides access to results of asynchronous requests to databases and database objects 39 | // using event listeners. Each reading and writing operation on a database is done using a request. 40 | type Request struct { 41 | txn *Transaction 42 | jsRequest safejs.Value 43 | } 44 | 45 | func wrapRequest(txn *Transaction, jsRequest safejs.Value) *Request { 46 | if isInstance, err := jsRequest.InstanceOf(jsIDBRequest); !isInstance || err != nil { 47 | panic("Invalid JS request type") 48 | } 49 | if txn == nil { 50 | txn = (*Transaction)(nil) 51 | } 52 | return &Request{ 53 | txn: txn, 54 | jsRequest: jsRequest, 55 | } 56 | } 57 | 58 | // Source returns the source of the request, such as an Index or an ObjectStore. If no source exists (such as when calling Factory.Open), it returns nil for both. 59 | func (r *Request) Source() (objectStore *ObjectStore, index *Index, err error) { 60 | jsSource, err := r.jsRequest.Get("source") 61 | if err != nil { 62 | return 63 | } 64 | if isInstance, _ := jsSource.InstanceOf(jsObjectStore); isInstance { 65 | objectStore = wrapObjectStore(r.txn, jsSource) 66 | } else if isInstance, _ := jsSource.InstanceOf(jsIDBIndex); isInstance { 67 | index = wrapIndex(r.txn, jsSource) 68 | } 69 | return 70 | } 71 | 72 | func (r *Request) result() (safejs.Value, error) { 73 | return r.jsRequest.Get("result") 74 | } 75 | 76 | // Result returns the result of the request. If the request failed and the result is not available, an error is returned. 77 | func (r *Request) Result() (js.Value, error) { 78 | value, err := r.result() 79 | return safejs.Unsafe(value), err 80 | } 81 | 82 | // Err returns an error in the event of an unsuccessful request, indicating what went wrong. 83 | func (r *Request) Err() (err error) { 84 | jsErr, err := r.jsRequest.Get("error") 85 | if err != nil { 86 | return err 87 | } 88 | return domExceptionAsError(jsErr) 89 | } 90 | 91 | func (r *Request) await(ctx context.Context) (result safejs.Value, awaitErr error) { 92 | ctx, cancel := context.WithCancel(ctx) 93 | defer cancel() 94 | listenErr := r.Listen(ctx, func() { 95 | result, awaitErr = r.result() 96 | cancel() 97 | }, func() { 98 | awaitErr = r.Err() 99 | cancel() 100 | }) 101 | if listenErr != nil { 102 | return result, listenErr 103 | } 104 | <-ctx.Done() 105 | return result, awaitErr 106 | } 107 | 108 | // Await waits for success or failure, then returns the results. 109 | func (r *Request) Await(ctx context.Context) (js.Value, error) { 110 | result, err := r.await(ctx) 111 | return safejs.Unsafe(result), err 112 | } 113 | 114 | // ReadyState returns the state of the request. Every request starts in the pending state. The state changes to done when the request completes successfully or when an error occurs. 115 | func (r *Request) ReadyState() (string, error) { 116 | readyState, err := r.jsRequest.Get("readyState") 117 | if err != nil { 118 | return "", err 119 | } 120 | return readyState.String() 121 | } 122 | 123 | // Transaction returns the transaction for the request. This can return nil for certain requests, for example those returned from Factory.Open unless an upgrade is needed. (You're just connecting to a database, so there is no transaction to return). 124 | func (r *Request) Transaction() (*Transaction, error) { 125 | if r.txn == (*Transaction)(nil) { 126 | return nil, errNotInTransaction 127 | } 128 | return r.txn, nil 129 | } 130 | 131 | // ListenSuccess invokes the callback when the request succeeds 132 | func (r *Request) ListenSuccess(ctx context.Context, success func()) error { 133 | return r.Listen(ctx, success, nil) 134 | } 135 | 136 | // ListenError invokes the callback when the request fails 137 | func (r *Request) ListenError(ctx context.Context, failed func()) error { 138 | return r.Listen(ctx, nil, failed) 139 | } 140 | 141 | // Listen invokes the success callback when the request succeeds and failed when it fails. 142 | func (r *Request) Listen(ctx context.Context, success, failed func()) error { 143 | if success != nil { 144 | // by default, only listen for 1 value 145 | var cancel context.CancelFunc 146 | ctx, cancel = context.WithCancel(ctx) 147 | originalSuccess := success 148 | success = func() { 149 | defer cancel() 150 | originalSuccess() 151 | } 152 | } 153 | return r.listen(ctx, success, failed) 154 | } 155 | 156 | // listen is like Listen, but doesn't cancel the context after success is called 157 | func (r *Request) listen(ctx context.Context, success, failed func()) error { 158 | ctx, cancel := context.WithCancel(ctx) 159 | panicHandler := func(err error) { 160 | log.Println("Failed resolving request results:", err) 161 | txn, err := r.Transaction() 162 | if err == nil { 163 | _ = txn.Abort() 164 | } 165 | cancel() 166 | ignorePanic(failed) // helps the listener to cancel the outer context 167 | } 168 | 169 | if failed != nil { 170 | errFunc, err := safejs.FuncOf(func(safejs.Value, []safejs.Value) interface{} { 171 | defer catchHandler(panicHandler) 172 | failed() 173 | cancel() 174 | return nil 175 | }) 176 | if err != nil { 177 | panic(err) 178 | } 179 | _, err = r.jsRequest.Call(addEventListener, "error", errFunc) 180 | if err != nil { 181 | return tryAsDOMException(err) 182 | } 183 | go func() { 184 | <-ctx.Done() 185 | _, err := r.jsRequest.Call(removeEventListener, "error", errFunc) 186 | if err != nil { 187 | panic(err) 188 | } 189 | errFunc.Release() 190 | }() 191 | } 192 | if success != nil { 193 | successFunc, err := safejs.FuncOf(func(safejs.Value, []safejs.Value) interface{} { 194 | defer catchHandler(panicHandler) 195 | success() 196 | // don't cancel ctx here, need to allow multiple values for cursors 197 | return nil 198 | }) 199 | if err != nil { 200 | panic(err) 201 | } 202 | _, err = r.jsRequest.Call(addEventListener, "success", successFunc) 203 | if err != nil { 204 | return tryAsDOMException(err) 205 | } 206 | go func() { 207 | <-ctx.Done() 208 | _, err := r.jsRequest.Call(removeEventListener, "success", successFunc) 209 | if err != nil { 210 | panic(err) 211 | } 212 | successFunc.Release() 213 | }() 214 | } 215 | return nil 216 | } 217 | 218 | func catchHandler(fn func(err error)) { 219 | err := recoveryToError(recover()) 220 | if err != nil { 221 | fn(err) 222 | } 223 | } 224 | 225 | func recoveryToError(r interface{}) error { 226 | if r == nil { 227 | return nil 228 | } 229 | switch val := r.(type) { 230 | case error: 231 | return val 232 | case js.Value: 233 | return js.Error{Value: val} 234 | default: 235 | return fmt.Errorf("%+v", val) 236 | } 237 | } 238 | 239 | func ignorePanic(fn func()) { 240 | defer func() { 241 | _ = recover() 242 | }() 243 | fn() 244 | } 245 | 246 | // UintRequest is a Request that retrieves a uint result 247 | type UintRequest struct { 248 | *Request 249 | } 250 | 251 | func newUintRequest(req *Request) *UintRequest { 252 | return &UintRequest{req} 253 | } 254 | 255 | // Result returns the result of the request. If the request failed and the result is not available, an error is returned. 256 | func (u *UintRequest) Result() (uint, error) { 257 | result, err := u.Request.result() 258 | if err != nil { 259 | return 0, err 260 | } 261 | value, err := result.Int() 262 | return uint(value), err 263 | } 264 | 265 | // Await waits for success or failure, then returns the results. 266 | func (u *UintRequest) Await(ctx context.Context) (uint, error) { 267 | result, err := u.Request.await(ctx) 268 | if err != nil { 269 | return 0, err 270 | } 271 | value, err := result.Int() 272 | return uint(value), err 273 | } 274 | 275 | // ArrayRequest is a Request that retrieves an array of js.Values 276 | type ArrayRequest struct { 277 | *Request 278 | } 279 | 280 | func newArrayRequest(req *Request) *ArrayRequest { 281 | return &ArrayRequest{req} 282 | } 283 | 284 | // Result returns the result of the request. If the request failed and the result is not available, an error is returned. 285 | func (a *ArrayRequest) Result() ([]js.Value, error) { 286 | result, err := a.Request.result() 287 | if err != nil { 288 | return nil, err 289 | } 290 | var values []js.Value 291 | err = iterArray(result, func(i int, value safejs.Value) (bool, error) { 292 | values = append(values, safejs.Unsafe(value)) 293 | return true, nil 294 | }) 295 | return values, err 296 | } 297 | 298 | // Await waits for success or failure, then returns the results. 299 | func (a *ArrayRequest) Await(ctx context.Context) ([]js.Value, error) { 300 | result, err := a.Request.await(ctx) 301 | if err != nil { 302 | return nil, err 303 | } 304 | var values []js.Value 305 | err = iterArray(result, func(i int, value safejs.Value) (bool, error) { 306 | values = append(values, safejs.Unsafe(value)) 307 | return true, nil 308 | }) 309 | return values, err 310 | } 311 | 312 | // AckRequest is a Request that doesn't retrieve a value, only used to detect errors. 313 | type AckRequest struct { 314 | *Request 315 | } 316 | 317 | func newAckRequest(req *Request) *AckRequest { 318 | return &AckRequest{req} 319 | } 320 | 321 | // Result is a no-op. This kind of request does not retrieve any data in the result. 322 | func (a *AckRequest) Result() {} // no-op 323 | 324 | // Await waits for success or failure, then returns the results. 325 | func (a *AckRequest) Await(ctx context.Context) error { 326 | _, err := a.Request.await(ctx) 327 | return err 328 | } 329 | 330 | func cursorIter(ctx context.Context, req *Request, iter func(*Cursor) error) error { 331 | ctx, cancel := context.WithCancel(ctx) 332 | var returnErr error 333 | listenErr := req.listen(ctx, func() { 334 | jsCursor, err := req.result() 335 | if err != nil { 336 | returnErr = err 337 | cancel() 338 | return 339 | } 340 | if jsCursor.IsNull() { 341 | cancel() 342 | return 343 | } 344 | cursor := wrapCursor(req.txn, jsCursor) 345 | err = iter(cursor) 346 | if err != nil { 347 | if err != ErrCursorStopIter { 348 | returnErr = err 349 | } 350 | cancel() 351 | return 352 | } 353 | if !cursor.iterated { 354 | err := cursor.Continue() 355 | if err != nil { 356 | returnErr = err 357 | cancel() 358 | return 359 | } 360 | } 361 | }, func() { 362 | returnErr = req.Err() 363 | if returnErr == nil { 364 | returnErr = errors.New("Failed to handle panic in JS callback") 365 | } 366 | cancel() 367 | }) 368 | if listenErr != nil { 369 | return listenErr 370 | } 371 | <-ctx.Done() 372 | return returnErr 373 | } 374 | 375 | // CursorRequest is a Request that retrieves a Cursor 376 | type CursorRequest struct { 377 | *Request 378 | } 379 | 380 | func newCursorRequest(req *Request) *CursorRequest { 381 | return &CursorRequest{req} 382 | } 383 | 384 | // Iter invokes the callback when the request succeeds for each cursor iteration 385 | func (c *CursorRequest) Iter(ctx context.Context, iter func(*Cursor) error) error { 386 | return cursorIter(ctx, c.Request, func(cursor *Cursor) error { 387 | return iter(cursor) 388 | }) 389 | } 390 | 391 | // Result returns the result of the request. If the request failed and the result is not available, an error is returned. 392 | func (c *CursorRequest) Result() (*Cursor, error) { 393 | result, err := c.Request.result() 394 | if err != nil { 395 | return nil, err 396 | } 397 | return wrapCursor(c.txn, result), nil 398 | } 399 | 400 | // Await waits for success or failure, then returns the results. 401 | func (c *CursorRequest) Await(ctx context.Context) (*Cursor, error) { 402 | result, err := c.Request.await(ctx) 403 | if err != nil { 404 | return nil, err 405 | } 406 | return wrapCursor(c.txn, result), nil 407 | } 408 | 409 | // CursorWithValueRequest is a Request that retrieves a CursorWithValue 410 | type CursorWithValueRequest struct { 411 | *Request 412 | } 413 | 414 | func newCursorWithValueRequest(req *Request) *CursorWithValueRequest { 415 | return &CursorWithValueRequest{req} 416 | } 417 | 418 | // Iter invokes the callback when the request succeeds for each cursor iteration 419 | func (c *CursorWithValueRequest) Iter(ctx context.Context, iter func(*CursorWithValue) error) error { 420 | return cursorIter(ctx, c.Request, func(cursor *Cursor) error { 421 | return iter(newCursorWithValue(cursor)) 422 | }) 423 | } 424 | 425 | // Result returns the result of the request. If the request failed and the result is not available, an error is returned. 426 | func (c *CursorWithValueRequest) Result() (*CursorWithValue, error) { 427 | result, err := c.Request.result() 428 | if err != nil { 429 | return nil, err 430 | } 431 | return wrapCursorWithValue(c.txn, result), nil 432 | } 433 | 434 | // Await waits for success or failure, then returns the results. 435 | func (c *CursorWithValueRequest) Await(ctx context.Context) (*CursorWithValue, error) { 436 | result, err := c.Request.await(ctx) 437 | if err != nil { 438 | return nil, err 439 | } 440 | return wrapCursorWithValue(c.txn, result), nil 441 | } 442 | -------------------------------------------------------------------------------- /idb/request_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "sync/atomic" 9 | "syscall/js" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 14 | ) 15 | 16 | var ( 17 | testRequestKey = js.ValueOf("key") 18 | ) 19 | 20 | func testRequest(t *testing.T) (*Transaction, *Request) { 21 | db := testDB(t, func(db *Database) { 22 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 23 | if !assert.NoError(t, err) { 24 | t.FailNow() 25 | } 26 | }) 27 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 28 | if !assert.NoError(t, err) { 29 | t.FailNow() 30 | } 31 | store, err := txn.ObjectStore("mystore") 32 | if !assert.NoError(t, err) { 33 | t.FailNow() 34 | } 35 | req, err := store.PutKey(testRequestKey, js.ValueOf("value")) 36 | if !assert.NoError(t, err) { 37 | t.FailNow() 38 | } 39 | return txn, req 40 | } 41 | 42 | func TestRequestSource(t *testing.T) { 43 | t.Parallel() 44 | _, req := testRequest(t) 45 | store, index, err := req.Source() 46 | assert.NoError(t, err) 47 | assert.NotZero(t, store) 48 | assert.Zero(t, index) 49 | } 50 | 51 | func TestRequestAwait(t *testing.T) { 52 | t.Parallel() 53 | _, req := testRequest(t) 54 | 55 | result, err := req.Await(context.Background()) 56 | assert.Equal(t, testRequestKey, result) 57 | assert.NoError(t, err) 58 | 59 | result, err = req.Result() 60 | assert.NoError(t, err) 61 | assert.Equal(t, testRequestKey, result) 62 | 63 | err = req.Err() 64 | assert.NoError(t, err) 65 | } 66 | 67 | func TestRequestReadyState(t *testing.T) { 68 | t.Parallel() 69 | _, req := testRequest(t) 70 | 71 | result, err := req.Await(context.Background()) 72 | assert.Equal(t, testRequestKey, result) 73 | assert.NoError(t, err) 74 | 75 | state, err := req.ReadyState() 76 | assert.NoError(t, err) 77 | assert.Equal(t, "done", state) 78 | } 79 | 80 | func TestRequestTransaction(t *testing.T) { 81 | t.Parallel() 82 | txn, req := testRequest(t) 83 | 84 | reqTxn, err := req.Transaction() 85 | assert.NoError(t, err) 86 | assert.Equal(t, txn.jsTransaction, reqTxn.jsTransaction) 87 | } 88 | 89 | func TestListen(t *testing.T) { 90 | t.Parallel() 91 | _, req := testRequest(t) 92 | 93 | var successCount int64 94 | err := req.Listen(context.Background(), func() { 95 | atomic.AddInt64(&successCount, 1) 96 | result, err := req.Result() 97 | assert.NoError(t, err) 98 | assert.Equal(t, testRequestKey, result) 99 | }, func() { 100 | t.Error("Failed should not be called:", req.Err()) 101 | }) 102 | assert.NoError(t, err) 103 | 104 | assert.Eventually(t, func(ctx context.Context) bool { 105 | return atomic.LoadInt64(&successCount) > 0 106 | }, time.Second, 50*time.Millisecond) 107 | } 108 | -------------------------------------------------------------------------------- /idb/strings.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "github.com/hack-pad/safejs" 8 | ) 9 | 10 | func sliceFromStrings(strs []string) []interface{} { 11 | values := make([]interface{}, 0, len(strs)) 12 | for _, s := range strs { 13 | values = append(values, s) 14 | } 15 | return values 16 | } 17 | 18 | func stringsFromArray(arr safejs.Value) ([]string, error) { 19 | var strs []string 20 | iterErr := iterArray(arr, func(i int, value safejs.Value) (bool, error) { 21 | str, err := value.String() 22 | if err != nil { 23 | return false, err 24 | } 25 | strs = append(strs, str) 26 | return true, nil 27 | }) 28 | return strs, iterErr 29 | } 30 | 31 | func iterArray(arr safejs.Value, visit func(i int, value safejs.Value) (keepGoing bool, visitErr error)) (err error) { 32 | length, err := arr.Length() 33 | if err != nil { 34 | return err 35 | } 36 | for i := 0; i < length; i++ { 37 | index, err := arr.Index(i) 38 | if err != nil { 39 | return err 40 | } 41 | keepGoing, visitErr := visit(i, index) 42 | if !keepGoing || visitErr != nil { 43 | return visitErr 44 | } 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /idb/transaction.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | 10 | "github.com/hack-pad/go-indexeddb/idb/internal/jscache" 11 | "github.com/hack-pad/safejs" 12 | ) 13 | 14 | var ( 15 | supportsTransactionCommit = checkSupportsTransactionCommit() 16 | 17 | errNotInTransaction = errors.New("Not part of a transaction") 18 | ) 19 | 20 | func checkSupportsTransactionCommit() bool { 21 | idbTransaction, err := safejs.Global().Get("IDBTransaction") 22 | if err != nil { 23 | return false 24 | } 25 | prototype, err := idbTransaction.Get("prototype") 26 | if err != nil { 27 | return false 28 | } 29 | commit, err := prototype.Get("commit") 30 | if err != nil { 31 | return false 32 | } 33 | supported, err := commit.Truthy() 34 | return supported && err == nil 35 | } 36 | 37 | var ( 38 | modeCache jscache.Strings 39 | durabilityCache jscache.Strings 40 | ) 41 | 42 | // TransactionMode defines the mode for isolating access to data in the transaction's current object stores. 43 | type TransactionMode int 44 | 45 | const ( 46 | // TransactionReadOnly allows data to be read but not changed. 47 | TransactionReadOnly TransactionMode = iota 48 | // TransactionReadWrite allows reading and writing of data in existing data stores to be changed. 49 | TransactionReadWrite 50 | ) 51 | 52 | func parseMode(s string) TransactionMode { 53 | switch s { 54 | case "readwrite": 55 | return TransactionReadWrite 56 | default: 57 | return TransactionReadOnly 58 | } 59 | } 60 | 61 | func (m TransactionMode) String() string { 62 | switch m { 63 | case TransactionReadWrite: 64 | return "readwrite" 65 | default: 66 | return "readonly" 67 | } 68 | } 69 | 70 | func (m TransactionMode) jsValue() safejs.Value { 71 | return modeCache.Value(m.String()) 72 | } 73 | 74 | // TransactionDurability is a hint to the user agent of whether to prioritize performance or durability when committing a transaction. 75 | type TransactionDurability int 76 | 77 | const ( 78 | // DurabilityDefault indicates the user agent should use its default durability behavior for the storage bucket. This is the default for transactions if not otherwise specified. 79 | DurabilityDefault TransactionDurability = iota 80 | // DurabilityRelaxed indicates the user agent may consider that the transaction has successfully committed as soon as all outstanding changes have been written to the operating system, without subsequent verification. 81 | DurabilityRelaxed 82 | // DurabilityStrict indicates the user agent may consider that the transaction has successfully committed only after verifying all outstanding changes have been successfully written to a persistent storage medium. 83 | DurabilityStrict 84 | ) 85 | 86 | func parseDurability(s string) TransactionDurability { 87 | switch s { 88 | case "relaxed": 89 | return DurabilityRelaxed 90 | case "strict": 91 | return DurabilityStrict 92 | default: 93 | return DurabilityDefault 94 | } 95 | } 96 | 97 | func (d TransactionDurability) String() string { 98 | switch d { 99 | case DurabilityRelaxed: 100 | return "relaxed" 101 | case DurabilityStrict: 102 | return "strict" 103 | default: 104 | return "default" 105 | } 106 | } 107 | 108 | func (d TransactionDurability) jsValue() safejs.Value { 109 | return durabilityCache.Value(d.String()) 110 | } 111 | 112 | // Transaction provides a static, asynchronous transaction on a database. 113 | // All reading and writing of data is done within transactions. You use Database to start transactions, 114 | // Transaction to set the mode of the transaction (e.g. is it TransactionReadOnly or TransactionReadWrite), 115 | // and you access an ObjectStore to make a request. You can also use a Transaction object to abort transactions. 116 | type Transaction struct { 117 | db *Database 118 | jsTransaction safejs.Value 119 | objectStores map[string]*ObjectStore 120 | } 121 | 122 | func wrapTransaction(db *Database, jsTransaction safejs.Value) *Transaction { 123 | return &Transaction{ 124 | db: db, 125 | jsTransaction: jsTransaction, 126 | objectStores: make(map[string]*ObjectStore, 1), 127 | } 128 | } 129 | 130 | // Database returns the database connection with which this transaction is associated. 131 | func (t *Transaction) Database() (*Database, error) { 132 | return t.db, nil 133 | } 134 | 135 | // Durability returns the durability hint the transaction was created with. 136 | func (t *Transaction) Durability() (TransactionDurability, error) { 137 | durability, err := t.jsTransaction.Get("durability") 138 | if err != nil { 139 | return 0, err 140 | } 141 | durabilityString, err := durability.String() 142 | if err != nil { 143 | return 0, err 144 | } 145 | return parseDurability(durabilityString), nil 146 | } 147 | 148 | // Err returns an error indicating the type of error that occurred when there is an unsuccessful transaction. Returns nil if the transaction is not finished, is finished and successfully committed, or was aborted with Transaction.Abort(). 149 | func (t *Transaction) Err() error { 150 | jsErr, err := t.jsTransaction.Get("error") 151 | if err != nil { 152 | return err 153 | } 154 | return domExceptionAsError(jsErr) 155 | } 156 | 157 | // Abort rolls back all the changes to objects in the database associated with this transaction. 158 | func (t *Transaction) Abort() error { 159 | _, err := t.jsTransaction.Call("abort") 160 | return tryAsDOMException(err) 161 | } 162 | 163 | // Mode returns the mode for isolating access to data in the object stores that are in the scope of the transaction. The default value is TransactionReadOnly. 164 | func (t *Transaction) Mode() (TransactionMode, error) { 165 | mode, err := t.jsTransaction.Get("mode") 166 | if err != nil { 167 | return 0, err 168 | } 169 | modeStr, err := mode.String() 170 | return parseMode(modeStr), err 171 | } 172 | 173 | // ObjectStoreNames returns a list of the names of ObjectStores associated with the transaction. 174 | func (t *Transaction) ObjectStoreNames() ([]string, error) { 175 | objectStoreNames, err := t.jsTransaction.Get("objectStoreNames") 176 | if err != nil { 177 | return nil, err 178 | } 179 | return stringsFromArray(objectStoreNames) 180 | } 181 | 182 | // ObjectStore returns an ObjectStore representing an object store that is part of the scope of this transaction. 183 | func (t *Transaction) ObjectStore(name string) (*ObjectStore, error) { 184 | if store, ok := t.objectStores[name]; ok { 185 | return store, nil 186 | } 187 | jsObjectStore, err := t.jsTransaction.Call("objectStore", name) 188 | if err != nil { 189 | return nil, tryAsDOMException(err) 190 | } 191 | store := wrapObjectStore(t, jsObjectStore) 192 | t.objectStores[name] = store 193 | return store, nil 194 | } 195 | 196 | // Commit for an active transaction, commits the transaction. Note that this doesn't normally have to be called — a transaction will automatically commit when all outstanding requests have been satisfied and no new requests have been made. Commit() can be used to start the commit process without waiting for events from outstanding requests to be dispatched. 197 | func (t *Transaction) Commit() error { 198 | if !supportsTransactionCommit { 199 | return nil 200 | } 201 | 202 | _, err := t.jsTransaction.Call("commit") 203 | return tryAsDOMException(err) 204 | } 205 | 206 | // Await waits for success or failure, then returns the results. 207 | func (t *Transaction) Await(ctx context.Context) error { 208 | err := <-t.listenFinished(ctx) 209 | return tryAsDOMException(err) 210 | } 211 | 212 | // listenFinished listens to this transaction's completion events which eventually resolves with nil or an error. 213 | // Resolves with the first IDBRequest's error 214 | func (t *Transaction) listenFinished(ctx context.Context) <-chan error { 215 | result := make(chan error, 1) 216 | resolveCtx, cancel := context.WithCancel(ctx) 217 | 218 | if err := t.addCancelingEventListener(resolveCtx, cancel, "abort", result, func(safejs.Value) error { 219 | return t.Err() // catch abort errors not already caught by the error event handler, like QuotaExceededError 220 | }); err != nil { 221 | result <- err 222 | return result 223 | } 224 | 225 | if err := t.addCancelingEventListener(resolveCtx, cancel, "complete", result, func(safejs.Value) error { 226 | return nil // transaction was successful 227 | }); err != nil { 228 | result <- err 229 | return result 230 | } 231 | 232 | if err := t.addCancelingEventListener(resolveCtx, cancel, "error", result, func(event safejs.Value) error { 233 | // Error event target is always an IDBRequest, which is guaranteed to be a DOMException with a 'name' property. 234 | properties, err := jsGetNested(event, "target", "error") 235 | if err != nil { 236 | return err 237 | } 238 | return domExceptionAsError(properties[1]) 239 | }); err != nil { 240 | result <- err 241 | return result 242 | } 243 | 244 | go func() { 245 | select { 246 | case <-ctx.Done(): 247 | result <- ctx.Err() 248 | case <-resolveCtx.Done(): 249 | } 250 | }() 251 | return result 252 | } 253 | 254 | func jsGetNested(value safejs.Value, keys ...string) ([]safejs.Value, error) { 255 | if len(keys) == 0 { 256 | return []safejs.Value{value}, nil 257 | } 258 | nextValue, err := value.Get(keys[0]) 259 | if err != nil { 260 | return nil, err 261 | } 262 | values, err := jsGetNested(nextValue, keys[1:]...) 263 | if err != nil { 264 | return nil, err 265 | } 266 | return append([]safejs.Value{nextValue}, values...), nil 267 | } 268 | 269 | // addCancelingEventListener adds an event listener for fn() and cleans it up when the context is canceled. 270 | // The listener only runs if the context has not completed yet, then cancels it. 271 | // 272 | // Sends fn's error return value to result. 273 | // 274 | // Effectively, this means multiple calls to addCancelingEventListener with the same ctx in a single-threaded environment results in exactly one running. 275 | func (t *Transaction) addCancelingEventListener( 276 | ctx context.Context, cancel context.CancelFunc, 277 | eventName string, 278 | result chan<- error, 279 | fn func(event safejs.Value) error, 280 | ) error { 281 | jsFunc, err := safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) interface{} { 282 | select { 283 | case <-ctx.Done(): 284 | default: 285 | var event safejs.Value 286 | if len(args) > 0 { 287 | event = args[0] 288 | } 289 | result <- fn(event) 290 | cancel() 291 | } 292 | return nil 293 | }) 294 | if err != nil { 295 | return err 296 | } 297 | _, err = t.jsTransaction.Call(addEventListener, t.db.callStrings.Value(eventName), jsFunc) 298 | if err != nil { 299 | return tryAsDOMException(err) 300 | } 301 | go func() { 302 | <-ctx.Done() 303 | _, _ = t.jsTransaction.Call(removeEventListener, t.db.callStrings.Value(eventName), jsFunc) // clean up on best-effort basis 304 | jsFunc.Release() 305 | }() 306 | return nil 307 | } 308 | -------------------------------------------------------------------------------- /idb/transaction_test.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | // +build js,wasm 3 | 4 | package idb 5 | 6 | import ( 7 | "context" 8 | "syscall/js" 9 | "testing" 10 | 11 | "github.com/hack-pad/go-indexeddb/idb/internal/assert" 12 | ) 13 | 14 | func TestTransactionDatabase(t *testing.T) { 15 | t.Parallel() 16 | db := testDB(t, func(db *Database) { 17 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 18 | assert.NoError(t, err) 19 | }) 20 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 21 | assert.NoError(t, err) 22 | 23 | txnDB, err := txn.Database() 24 | assert.NoError(t, err) 25 | assert.Equal(t, db.jsDB, txnDB.jsDB) 26 | } 27 | 28 | func TestTransactionDurability(t *testing.T) { 29 | t.Parallel() 30 | const storeName = "mystore" 31 | db := testDB(t, func(db *Database) { 32 | _, err := db.CreateObjectStore(storeName, ObjectStoreOptions{}) 33 | assert.NoError(t, err) 34 | }) 35 | 36 | for _, tc := range []struct { 37 | durability TransactionDurability 38 | expectString string 39 | }{ 40 | { 41 | durability: DurabilityDefault, 42 | expectString: "default", 43 | }, 44 | { 45 | durability: DurabilityRelaxed, 46 | expectString: "relaxed", 47 | }, 48 | { 49 | durability: DurabilityStrict, 50 | expectString: "strict", 51 | }, 52 | } { 53 | tc := tc // enable parallel sub-tests 54 | t.Run(tc.durability.String(), func(t *testing.T) { 55 | t.Parallel() 56 | txn, err := db.TransactionWithOptions(TransactionOptions{ 57 | Durability: tc.durability, 58 | }, storeName) 59 | assert.NoError(t, err) 60 | dur, err := txn.Durability() 61 | assert.NoError(t, err) 62 | assert.Equal(t, tc.durability, dur) 63 | assert.Equal(t, tc.expectString, dur.String()) 64 | }) 65 | } 66 | } 67 | 68 | func TestTransactionAbortErr(t *testing.T) { 69 | t.Parallel() 70 | db := testDB(t, func(db *Database) { 71 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 72 | assert.NoError(t, err) 73 | }) 74 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 75 | assert.NoError(t, err) 76 | store, err := txn.ObjectStore("mystore") 77 | assert.NoError(t, err) 78 | _, err = store.AddKey(js.ValueOf("some id"), js.ValueOf(nil)) 79 | assert.NoError(t, err) 80 | 81 | resultErr := txn.listenFinished(context.Background()) 82 | assert.NoError(t, txn.Abort()) 83 | err = <-resultErr 84 | assert.ErrorIs(t, err, NewDOMException("AbortError")) 85 | err = txn.Err() 86 | assert.NoError(t, err) 87 | } 88 | 89 | func TestTransactionMode(t *testing.T) { 90 | t.Parallel() 91 | db := testDB(t, func(db *Database) { 92 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 93 | assert.NoError(t, err) 94 | }) 95 | 96 | t.Run("read only", func(t *testing.T) { 97 | t.Parallel() 98 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 99 | assert.NoError(t, err) 100 | 101 | mode, err := txn.Mode() 102 | assert.NoError(t, err) 103 | assert.Equal(t, TransactionReadOnly, mode) 104 | }) 105 | 106 | t.Run("read write", func(t *testing.T) { 107 | t.Parallel() 108 | txn, err := db.Transaction(TransactionReadWrite, "mystore") 109 | assert.NoError(t, err) 110 | 111 | mode, err := txn.Mode() 112 | assert.NoError(t, err) 113 | assert.Equal(t, TransactionReadWrite, mode) 114 | }) 115 | } 116 | 117 | func TestTransactionObjectStoreNames(t *testing.T) { 118 | t.Parallel() 119 | db := testDB(t, func(db *Database) { 120 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 121 | assert.NoError(t, err) 122 | }) 123 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 124 | assert.NoError(t, err) 125 | 126 | names, err := txn.ObjectStoreNames() 127 | assert.NoError(t, err) 128 | assert.Equal(t, []string{"mystore"}, names) 129 | } 130 | 131 | func TestTransactionObjectStore(t *testing.T) { 132 | t.Parallel() 133 | db := testDB(t, func(db *Database) { 134 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 135 | assert.NoError(t, err) 136 | }) 137 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 138 | assert.NoError(t, err) 139 | 140 | store, err := txn.ObjectStore("mystore") 141 | assert.NoError(t, err) 142 | assert.NotZero(t, store) 143 | 144 | _, err = txn.ObjectStore("not a store") 145 | assert.Error(t, err) 146 | } 147 | 148 | func TestTransactionCommit(t *testing.T) { 149 | t.Parallel() 150 | db := testDB(t, func(db *Database) { 151 | _, err := db.CreateObjectStore("mystore", ObjectStoreOptions{}) 152 | assert.NoError(t, err) 153 | }) 154 | txn, err := db.Transaction(TransactionReadOnly, "mystore") 155 | assert.NoError(t, err) 156 | 157 | err = txn.Commit() 158 | assert.NoError(t, err) 159 | 160 | assert.NoError(t, txn.Await(context.Background())) 161 | 162 | err = txn.Commit() 163 | assert.Error(t, err) 164 | } 165 | -------------------------------------------------------------------------------- /idb/wasm_tags_test.go: -------------------------------------------------------------------------------- 1 | // +build !js 2 | 3 | package idb 4 | 5 | import ( 6 | "bufio" 7 | "go/build/constraint" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestAllWasmTags(t *testing.T) { 16 | walkErr := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error { 17 | if err != nil || info.IsDir() { 18 | return err 19 | } 20 | if path == "wasm_tags_test.go" { 21 | // ignore this file, since it must run with file system support enabled 22 | return nil 23 | } 24 | 25 | f, err := os.Open(path) 26 | if err != nil { 27 | return err 28 | } 29 | defer f.Close() 30 | scanner := bufio.NewScanner(f) 31 | for scanner.Scan() { 32 | line := strings.TrimSpace(scanner.Text()) 33 | if line == "" { 34 | continue 35 | } 36 | if !strings.HasPrefix(line, "//") { 37 | // hit non-comment line, so no build tags exist (see https://golang.org/cmd/go/#hdr-Build_constraints) 38 | t.Errorf("File %q does not contain a js,wasm build tag", path) 39 | break 40 | } 41 | 42 | expr, err := constraint.Parse(line) 43 | if err != nil { 44 | t.Logf("Build constraint failed to parse line in file %q: %q; %v", path, line, err) 45 | continue 46 | } 47 | if isJSWasm(expr) { 48 | break 49 | } 50 | } 51 | return scanner.Err() 52 | }) 53 | if walkErr != nil { 54 | t.Error("Walk failed:", walkErr) 55 | } 56 | } 57 | 58 | func isJSWasm(expr constraint.Expr) bool { 59 | switch expr := expr.(type) { 60 | case *constraint.AndExpr: 61 | x, y := expr.X.String(), expr.Y.String() 62 | return (x == "js" && y == "wasm") || (x == "wasm" && y == "js") 63 | default: 64 | return false 65 | } 66 | } 67 | --------------------------------------------------------------------------------