Skip to content

Commit 8246c31

Browse files
committed
feat(logic): document Operateable
1 parent 9869d69 commit 8246c31

2 files changed

Lines changed: 245 additions & 4 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
:::

src/prose/logic/classes/Transactable.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ The `Transactable` constructor accepts two parameters:
4242
| `setName(newName)` | Function | Stops all active listeners and sets a new database name. | The new `name` (String) | The `Transactable` instance |
4343
| `open(options)` | Function | Opens the IndexedDB database. See the [`open` options](#open-options) section. | See below | The `Transactable` instance |
4444
| `transact(effect, options)` | Function | Runs a transaction against the open database. See the [`transact` options](#transact-options) section. | See below | The `Transactable` instance |
45-
| `readonly(effect, options)` | Function | Shorthand for `transact(effect, { ...options, mode: 'readonly' })`. | Same as `transact`, except `mode` is not accepted. | The `Transactable` instance |
46-
| `readwrite(effect, options)` | Function | Shorthand for `transact(effect, { ...options, mode: 'readwrite' })`. | Same as `transact`, except `mode` is not accepted. | The `Transactable` instance |
47-
| `versionchange(effect, options)` | Function | Shorthand for `transact(effect, { ...options, mode: 'versionchange' })`. | Same as `transact`, except `mode` is not accepted. | The `Transactable` instance |
45+
| `readonly(effect, options)` | Function | Shorthand for `transact(...)` with `mode: 'readonly'`. | Same as `transact`, except `mode` is not accepted. | The `Transactable` instance |
46+
| `readwrite(effect, options)` | Function | Shorthand for `transact(...)` with `mode: 'readwrite'`. | Same as `transact`, except `mode` is not accepted. | The `Transactable` instance |
47+
| `versionchange(effect, options)` | Function | Shorthand for `transact(...)` with `mode: 'versionchange'`. | Same as `transact`, except `mode` is not accepted. | The `Transactable` instance |
4848
| `close()` | Function | Closes the IndexedDB database connection. | None | The `Transactable` instance |
4949
| `delete()` | Function | Deletes the IndexedDB database. | None | The `Transactable` instance |
50-
| `stop()` | Function | Stops all active event listeners. | None | The `Transactable` instance |
50+
| `stop()` | Function | Closes the database and stops any active event listeners. | None | The `Transactable` instance |
5151
:::
5252

5353

0 commit comments

Comments
 (0)