|
| 1 | +--- |
| 2 | +title: Operateable |
| 3 | +source: true |
| 4 | +tests: browser/Operateable.test.ts |
| 5 | +publish: true |
| 6 | +order: 0 |
| 7 | +--- |
| 8 | + |
| 9 | +`Operateable` is a class that enriches an [`IDBObjectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore), allowing it to: |
| 10 | +- Perform database operations (add, put, get, delete, etc.) in sequence |
| 11 | +- Track the status of those operations |
| 12 | +- Expose any error that occurs during an operation |
| 13 | + |
| 14 | +::: type="info" |
| 15 | +`Operateable` is designed to be used inside the `effect` callback of [`Transactable`](/docs/logic/classes/transactable)'s `transact`, `readonly`, or `readwrite` methods. `Transactable` manages the database lifecycle (opening, closing, deleting), while `Operateable` manages the work done inside a transaction. |
| 16 | +::: |
| 17 | + |
| 18 | + |
| 19 | +::: |
| 20 | +## Construct an `Operateable` instance |
| 21 | +::: |
| 22 | + |
| 23 | +The `Operateable` constructor accepts two parameters: |
| 24 | + |
| 25 | +::: ariaLabel="Operateable constructor parameters" classes="wide-4" |
| 26 | +| Parameter | Type | Required | Description | |
| 27 | +| --- | --- | --- | --- | |
| 28 | +| `objectStore` | `IDBObjectStore` | yes | The object store that will be made operable. | |
| 29 | +| `options` | Object | no | Options for the `Operateable` instance. No options are currently accepted. | |
| 30 | +::: |
| 31 | + |
| 32 | + |
| 33 | +::: |
| 34 | +## State and methods |
| 35 | +::: |
| 36 | + |
| 37 | +::: ariaLabel="Operateable state and methods" classes="wide-3 wide-4 wide-5" |
| 38 | +| Property | Type | Description | Parameters | Return value | |
| 39 | +| --- | --- | --- | --- | --- | |
| 40 | +| `objectStore` | Getter/Setter | See return value | N/A | <p>The `objectStore` (`IDBObjectStore`) passed to the constructor.</p><p>If you assign a value directly to `objectStore`, a setter will pass the new value to `setObjectStore`.</p> | |
| 41 | +| `status` | Getter | See return value | N/A | <p>The status (String) of the `Operateable` instance.</p><p>See the [Status reference](#status-reference) section for all possible values and their meanings.</p> | |
| 42 | +| `error` | Getter | See return value | N/A | The most recent `Error` encountered, populated after `operateerrored`. | |
| 43 | +| `setObjectStore(objectStore)` | Function | Stops all active listeners and sets a new object store. | The new `objectStore` (`IDBObjectStore`) | The `Operateable` instance | |
| 44 | +| `operate(descriptors)` | Function | Runs a sequence of operations against the object store. | An array of `OperationDescriptor` objects. See the [Operation descriptors](#operation-descriptors) section for more guidance. | The `Operateable` instance | |
| 45 | +| `add(descriptor)` | Function | Runs an `add` operation against the object store. | An `add` descriptor object, i.e. an `OperationDescriptor` object for the `add` operation, omitting the `operation` key. | The `Operateable` instance | |
| 46 | +| `put(descriptor)` | Function | Runs a `put` operation against the object store. | A `put` descriptor object, i.e. an `OperationDescriptor` object for the `put` operation, omitting the `operation` key. | The `Operateable` instance | |
| 47 | +| `get(descriptor)` | Function | Runs a `get` operation against the object store. | A `get` descriptor object, i.e. an `OperationDescriptor` object for the `get` operation, omitting the `operation` key. | The `Operateable` instance | |
| 48 | +| `getKey(descriptor)` | Function | Runs a `getKey` operation against the object store. | A `getKey` descriptor object, i.e. an `OperationDescriptor` object for the `getKey` operation, omitting the `operation` key. | The `Operateable` instance | |
| 49 | +| `getAll(descriptor)` | Function | Runs a `getAll` operation against the object store. | A `getAll` descriptor object, i.e. an `OperationDescriptor` object for the `getAll` operation, omitting the `operation` key. | The `Operateable` instance | |
| 50 | +| `getAllKeys(descriptor)` | Function | Runs a `getAllKeys` operation against the object store. | A `getAllKeys` descriptor object, i.e. an `OperationDescriptor` object for the `getAllKeys` operation, omitting the `operation` key. | The `Operateable` instance | |
| 51 | +| `getAllRecords(descriptor)` | Function | Runs a `getAllRecords` operation against the object store. | A `getAllRecords` descriptor object, i.e. an `OperationDescriptor` object for the `getAllRecords` operation, omitting the `operation` key. | The `Operateable` instance | |
| 52 | +| `delete(descriptor)` | Function | Runs a `delete` operation against the object store. | A `delete` descriptor object, i.e. an `OperationDescriptor` object for the `delete` operation, omitting the `operation` key. | The `Operateable` instance | |
| 53 | +| `clear(descriptor)` | Function | Runs a `clear` operation against the object store. | A `clear` descriptor object, i.e. an `OperationDescriptor` object for the `clear` operation, omitting the `operation` key. | The `Operateable` instance | |
| 54 | +| `count(descriptor)` | Function | Runs a `count` operation against the object store. | A `count` descriptor object, i.e. an `OperationDescriptor` object for the `count` operation, omitting the `operation` key. | The `Operateable` instance | |
| 55 | +| `openCursor(descriptor)` | Function | Runs an `openCursor` operation against the object store. | An `openCursor` descriptor object, i.e. an `OperationDescriptor` object for the `openCursor` operation, omitting the `operation` key. | The `Operateable` instance | |
| 56 | +| `openKeyCursor(descriptor)` | Function | Runs an `openKeyCursor` operation against the object store. | An `openKeyCursor` descriptor object, i.e. an `OperationDescriptor` object for the `openKeyCursor` operation, omitting the `operation` key. | The `Operateable` instance | |
| 57 | +| `stop()` | Function | Stops all active event listeners. | None | The `Operateable` instance | |
| 58 | +::: |
| 59 | + |
| 60 | + |
| 61 | +::: |
| 62 | +### Status reference |
| 63 | +::: |
| 64 | + |
| 65 | +::: ariaLabel="Operateable status reference" classes="wide-2" |
| 66 | +| Status | Description | |
| 67 | +| --- | --- | |
| 68 | +| `constructing` | The instance is being constructed. | |
| 69 | +| `ready` | Construction is complete; no operations have been run yet. | |
| 70 | +| `operating` | `operate()` has been called and operations are in progress. | |
| 71 | +| `operated` | All operations completed successfully. | |
| 72 | +| `operateerrored` | An operation encountered an error. | |
| 73 | +::: |
| 74 | + |
| 75 | + |
| 76 | +::: |
| 77 | +### Operation descriptors |
| 78 | +::: |
| 79 | + |
| 80 | +`operate` accepts an array of operation descriptors (objects). Each descriptor has an `operation` property that determines which [`IDBObjectStore`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore) method is called, as well as additional properties specific to that method. |
| 81 | + |
| 82 | +Operations run sequentially: each operation waits for the previous one to succeed before starting. |
| 83 | + |
| 84 | +For read operations, an optional `effect` callback receives the result of the operation. |
| 85 | + |
| 86 | +::: ariaLabel="Operation descriptors" classes="wide-2 wide-3" |
| 87 | +| `operation` | Other properties | Description | |
| 88 | +| --- | --- | --- | |
| 89 | +| `add` | `value: any`, `key?: IDBValidKey` | Adds a new record. Errors if the key already exists. | |
| 90 | +| `put` | `value: any`, `key?: IDBValidKey` | Adds or replaces a record. | |
| 91 | +| `get` | `query: IDBValidKey \| IDBKeyRange`, `effect?: (result: any) => void` | Retrieves a record by key or key range. | |
| 92 | +| `getKey` | `query: IDBValidKey \| IDBKeyRange`, `effect?: (result: IDBValidKey \| undefined) => void` | Retrieves the primary key of the first record matching the query. | |
| 93 | +| `getAll` | `query?: IDBValidKey \| IDBKeyRange \| null`, `count?: number`, `effect?: (result: any[]) => void` | Retrieves all records matching the query. | |
| 94 | +| `getAllKeys` | `query?: IDBValidKey \| IDBKeyRange \| null`, `count?: number`, `effect?: (result: IDBValidKey[]) => void` | Retrieves all primary keys matching the query. | |
| 95 | +| `getAllRecords` | `query?: IDBValidKey \| IDBKeyRange \| null`, `count?: number`, `effect?: (result: any[]) => void` | Retrieves all records as `{ key, primaryKey, value }` objects. Experimental — browser support may vary. | |
| 96 | +| `delete` | `query: IDBValidKey \| IDBKeyRange` | Deletes the record(s) matching the query. | |
| 97 | +| `clear` | (none) | Deletes all records in the object store. | |
| 98 | +| `count` | `query?: IDBValidKey \| IDBKeyRange`, `effect?: (result: number) => void` | Counts the records matching the query. | |
| 99 | +| `openCursor` | `query?: IDBValidKey \| IDBKeyRange \| null`, `direction?: IDBCursorDirection`, `effect?: (cursor: IDBCursorWithValue \| null) => void` | Opens a cursor to iterate over records. | |
| 100 | +| `openKeyCursor` | `query?: IDBValidKey \| IDBKeyRange \| null`, `direction?: IDBCursorDirection`, `effect?: (cursor: IDBCursor \| null) => void` | Opens a cursor to iterate over keys only. | |
| 101 | +::: |
| 102 | + |
| 103 | + |
| 104 | +::: |
| 105 | +## Using with `Transactable` |
| 106 | +::: |
| 107 | + |
| 108 | +`Operateable` is designed to pair with [`Transactable`](/docs/logic/classes/transactable): |
| 109 | + |
| 110 | +::: |
| 111 | +```js |
| 112 | +const transactable = new Transactable('baleada') |
| 113 | + |
| 114 | +transactable.open({ |
| 115 | + version: 1, |
| 116 | + upgradeEffect: db => { |
| 117 | + db.createObjectStore('ingredients', { keyPath: 'id' }) |
| 118 | + }, |
| 119 | +}) |
| 120 | + |
| 121 | +// Asynchronously, transactable status will change to 'opened'. |
| 122 | +// Later in the application's lifecycle, you can run database |
| 123 | +// transactions, like this transaction in `readwrite` mode: |
| 124 | +transactable.readwrite( |
| 125 | + // Pass a callback to perform side effects when the |
| 126 | + // transaction is open: |
| 127 | + transaction => { |
| 128 | + // Inside of transaction effect callbacks, construct an |
| 129 | + // Operateable instance around an object store: |
| 130 | + const operateable = new Operateable(transaction.objectStore('ingredients')) |
| 131 | + |
| 132 | + // Easily run sequences of database operations without |
| 133 | + // closing the transaction: |
| 134 | + operateable.operate([ |
| 135 | + { operation: 'add', value: { id: 1, name: 'Tortilla' } }, |
| 136 | + { operation: 'add', value: { id: 2, name: 'Beans' } }, |
| 137 | + { |
| 138 | + operation: 'getAll', |
| 139 | + effect: ingredients => { |
| 140 | + console.log(ingredients) |
| 141 | + }, |
| 142 | + }, |
| 143 | + ]) |
| 144 | + |
| 145 | + // Or, synchronously request individual operations, which |
| 146 | + // the browser will run independently and asynchronously: |
| 147 | + operateable.add({ value: { id: 3, name: 'Egg' } }) |
| 148 | + operateable.openCursor({ effect: cursor => { |
| 149 | + // Handle database cursor |
| 150 | + }}) |
| 151 | + }, |
| 152 | + { storeNames: ['ingredients'] } |
| 153 | +) |
| 154 | +``` |
| 155 | +::: |
| 156 | + |
| 157 | + |
| 158 | +::: |
| 159 | +## Using with TypeScript |
| 160 | +::: |
| 161 | + |
| 162 | +`Operateable` can strongly type the values you read from and write to your object store. To get this working, use the `createDefineObjectStore` helper function: |
| 163 | + |
| 164 | +::: |
| 165 | +```ts |
| 166 | +import { Operateable, createDefineObjectStore } from '@baleada/logic' |
| 167 | + |
| 168 | +// Define a TypeScript type for your database, where the keys |
| 169 | +// are store names and the indexed types are object schemas: |
| 170 | +type MyDatabase = { |
| 171 | + ingredients: { id?: number, name: string }, |
| 172 | + ... |
| 173 | +} |
| 174 | + |
| 175 | +// Create your type inference helper: |
| 176 | +const defineObjectStore = createDefineObjectStore<MyDatabase>() |
| 177 | + |
| 178 | +// Use your type inference helper when retrieving the object |
| 179 | +// store that you'll pass to `Operateable`: |
| 180 | +transactable.readwrite( |
| 181 | + transaction => { |
| 182 | + // `ingredientsObjectStore` is now strongly typed. |
| 183 | + // TypeScipt knows the exact shape of stored objects. |
| 184 | + const ingredientsObjectStore = defineObjectStore(transaction, 'ingredients') |
| 185 | + |
| 186 | + // Pass the store to `Operateable`, which will infer its type: |
| 187 | + const operateable = new Operateable(ingredientsObjectStore) |
| 188 | + |
| 189 | + // Now, all of Operateable's methods are type-safe: |
| 190 | + operateable |
| 191 | + .operate([ |
| 192 | + { |
| 193 | + operation: 'add', |
| 194 | + value: { |
| 195 | + id: 1, |
| 196 | + // Type error: `title` does not exist on this type |
| 197 | + // (should be `name` instead) |
| 198 | + title: 'Tortilla' |
| 199 | + } |
| 200 | + }, |
| 201 | + ]) |
| 202 | + .openCursor({ |
| 203 | + effect: cursor => { |
| 204 | + // Type error: `title` does not exist on this type |
| 205 | + // (should be `name` instead) |
| 206 | + console.log(cursor.value.title) |
| 207 | + |
| 208 | + // `cursor.value.name` is strongly typed as `string` |
| 209 | + console.log(cursor.value.name) |
| 210 | + } |
| 211 | + }) |
| 212 | + }, |
| 213 | + { storeNames: ['ingredients'] } |
| 214 | +) |
| 215 | +```` |
| 216 | +::: |
| 217 | + |
| 218 | +::: |
| 219 | +## API design compliance |
| 220 | +::: |
| 221 | + |
| 222 | +::: ariaLabel="Operateable's API design compliance" classes="wide-1 wide-3" |
| 223 | +| Spec | Compliance status | Notes | |
| 224 | +| --- | --- | --- | |
| 225 | +| Access functionality by constructing an instance | <BrandApiDesignSpecCheckmark /> | | |
| 226 | +| Constructor accepts two parameters: a piece of state, and an `options` object. | <BrandApiDesignSpecCheckmark /> | | |
| 227 | +| Constructor does not access the DOM | <BrandApiDesignSpecCheckmark /> | The `IDBObjectStore` is passed in; no DOM access occurs during construction. | |
| 228 | +| Takes the form of a JavaScript Object | <BrandApiDesignSpecCheckmark /> | | |
| 229 | +| State and methods are accessible through properties of the object | <BrandApiDesignSpecCheckmark /> | | |
| 230 | +| Methods always return the instance | <BrandApiDesignSpecCheckmark /> | | |
| 231 | +| Stores the constructor's state in a public getter named after the state's type | <BrandApiDesignSpecCheckmark /> | `objectStore` | |
| 232 | +| Has a public method you can use to set a new value for that public getter | <BrandApiDesignSpecCheckmark /> | `setObjectStore` | |
| 233 | +| Has a setter for that getter so you can assign a new value directly | <BrandApiDesignSpecCheckmark /> | | |
| 234 | +| Any other public getters that should be set by you in some cases also have setters and `set<Property>` methods | <BrandApiDesignSpecCheckmark /> | none | |
| 235 | +| Has at least one additional getter property that you can't (and shouldn't) set directly | <BrandApiDesignSpecCheckmark /> | `status`, `error` | |
| 236 | +| Has one or more public methods that expose core functionality | <BrandApiDesignSpecCheckmark /> | `operate`, `stop` | |
| 237 | +| Either has no side effects or has side effects that can be cleaned up with a `stop` method | <BrandApiDesignSpecCheckmark /> | | |
| 238 | +| Uses the sentence template to decide what state type should be accepted by a constructor | <BrandApiDesignSpecCheckmark /> | "An object store can be operated on." | |
| 239 | +| Constructor does not accept options that only customize the behavior of public methods, it allows those options to be passed to the method itself as a parameter. | <BrandApiDesignSpecCheckmark /> | | |
| 240 | +| Named after its core action, proper-cased and suffixed with `able` | <BrandApiDesignSpecCheckmark /> | | |
| 241 | +::: |
0 commit comments