Skip to content

Commit b075a25

Browse files
committed
Add experimental support of AbortSignal (#55)
1 parent fc7be7b commit b075a25

26 files changed

+591
-374
lines changed

README.md

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
- [`encoding = db.keyEncoding([encoding])`](#encoding--dbkeyencodingencoding)
4040
- [`encoding = db.valueEncoding([encoding])`](#encoding--dbvalueencodingencoding)
4141
- [`key = db.prefixKey(key, keyFormat[, local])`](#key--dbprefixkeykey-keyformat-local)
42-
- [`db.defer(fn)`](#dbdeferfn)
43-
- [`db.deferAsync(fn)`](#dbdeferasyncfn)
42+
- [`db.defer(fn[, options])`](#dbdeferfn-options)
43+
- [`db.deferAsync(fn[, options])`](#dbdeferasyncfn-options)
4444
- [`chainedBatch`](#chainedbatch)
4545
- [`chainedBatch.put(key, value[, options])`](#chainedbatchputkey-value-options)
4646
- [`chainedBatch.del(key[, options])`](#chainedbatchdelkey-options)
@@ -59,6 +59,7 @@
5959
- [`iterator.db`](#iteratordb)
6060
- [`iterator.count`](#iteratorcount)
6161
- [`iterator.limit`](#iteratorlimit)
62+
- [Aborting Iterators](#aborting-iterators)
6263
- [`keyIterator`](#keyiterator)
6364
- [`valueIterator`](#valueiterator)
6465
- [`sublevel`](#sublevel)
@@ -103,6 +104,7 @@
103104
- [`LEVEL_ITERATOR_NOT_OPEN`](#level_iterator_not_open)
104105
- [`LEVEL_ITERATOR_BUSY`](#level_iterator_busy)
105106
- [`LEVEL_BATCH_NOT_OPEN`](#level_batch_not_open)
107+
- [`LEVEL_ABORTED`](#level_aborted)
106108
- [`LEVEL_ENCODING_NOT_FOUND`](#level_encoding_not_found)
107109
- [`LEVEL_ENCODING_NOT_SUPPORTED`](#level_encoding_not_supported)
108110
- [`LEVEL_DECODE_ERROR`](#level_decode_error)
@@ -381,6 +383,7 @@ The `gte` and `lte` range options take precedence over `gt` and `lt` respectivel
381383
- `values` (boolean, default: `true`): whether to return the value of each entry. If set to `false`, the iterator will yield values that are `undefined`. Prefer to use `db.values()` instead.
382384
- `keyEncoding`: custom key encoding for this iterator, used to encode range options, to encode `seek()` targets and to decode keys.
383385
- `valueEncoding`: custom value encoding for this iterator, used to decode values.
386+
- `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to [abort read operations on the iterator](#aborting-iterators).
384387

385388
Lastly, an implementation is free to add its own options.
386389

@@ -529,9 +532,9 @@ console.log(nested.prefixKey('a', 'utf8')) // '!example!!nested!a'
529532
console.log(nested.prefixKey('a', 'utf8', true)) // '!nested!a'
530533
```
531534

532-
### `db.defer(fn)`
535+
### `db.defer(fn[, options])`
533536

534-
Call the function `fn` at a later time when [`db.status`](#dbstatus) changes to `'open'` or `'closed'`. Used by `abstract-level` itself to implement "deferred open" which is a feature that makes it possible to call methods like `db.put()` before the database has finished opening. The `defer()` method is exposed for implementations and plugins to achieve the same on their custom methods:
537+
Call the function `fn` at a later time when [`db.status`](#dbstatus) changes to `'open'` or `'closed'`. Known as a _deferred operation_. Used by `abstract-level` itself to implement "deferred open" which is a feature that makes it possible to call methods like `db.put()` before the database has finished opening. The `defer()` method is exposed for implementations and plugins to achieve the same on their custom methods:
535538

536539
```js
537540
db.foo = function (key) {
@@ -543,9 +546,13 @@ db.foo = function (key) {
543546
}
544547
```
545548

549+
The optional `options` object may contain:
550+
551+
- `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to abort the deferred operation. When aborted (now or later) the `fn` function will not be called.
552+
546553
When deferring a custom operation, do it early: after normalizing optional arguments but before encoding (to avoid double encoding and to emit original input if the operation has events) and before any _fast paths_ (to avoid calling back before the database has finished opening). For example, `db.batch([])` has an internal fast path where it skips work if the array of operations is empty. Resources that can be closed on their own (like iterators) should however first check such state before deferring, in order to reject operations after close (including when the database was reopened).
547554

548-
### `db.deferAsync(fn)`
555+
### `db.deferAsync(fn[, options])`
549556

550557
Similar to `db.defer(fn)` but for asynchronous work. Returns a promise, which waits for [`db.status`](#dbstatus) to change to `'open'` or `'closed'` and then calls `fn` which itself must return a promise. This allows for recursion:
551558

@@ -559,6 +566,10 @@ db.foo = async function (key) {
559566
}
560567
```
561568

569+
The optional `options` object may contain:
570+
571+
- `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to abort the deferred operation. When aborted (now or later) the `fn` function will not be called, and the promise returned by `deferAsync()` will be rejected with a [`LEVEL_ABORTED`](#errors) error.
572+
562573
### `chainedBatch`
563574

564575
#### `chainedBatch.put(key, value[, options])`
@@ -721,6 +732,44 @@ const hasMore = iterator.count < iterator.limit
721732
const remaining = iterator.limit - iterator.count
722733
```
723734

735+
#### Aborting Iterators
736+
737+
Iterators take an experimental `signal` option that, once signaled, aborts an in-progress read operation (if any) and rejects subsequent reads. The relevant promise will be rejected with a [`LEVEL_ABORTED`](#errors) error. Aborting does not close the iterator, because closing is asynchronous and may result in an error that needs a place to go. This means signals should be used together with a pattern that automatically closes the iterator:
738+
739+
```js
740+
const abortController = new AbortController()
741+
const signal = abortController.signal
742+
743+
// Will result in 'aborted' log
744+
abortController.abort()
745+
746+
try {
747+
for await (const entry of db.iterator({ signal })) {
748+
console.log(entry)
749+
}
750+
} catch (err) {
751+
if (err.code === 'LEVEL_ABORTED') {
752+
console.log('aborted')
753+
}
754+
}
755+
```
756+
757+
Otherwise, close the iterator explicitly:
758+
759+
```js
760+
const iterator = db.iterator({ signal })
761+
762+
try {
763+
const entries = await iterator.nextv(10)
764+
} catch (err) {
765+
if (err.code === 'LEVEL_ABORTED') {
766+
console.log('aborted')
767+
}
768+
} finally {
769+
await iterator.close()
770+
}
771+
```
772+
724773
### `keyIterator`
725774

726775
A key iterator has the same interface as `iterator` except that its methods yield keys instead of entries. Usage is otherwise the same.
@@ -1161,6 +1210,10 @@ When `iterator.next()` or `seek()` was called while a previous `next()` call was
11611210

11621211
When an operation was made on a chained batch while it was closing or closed, which may also be the result of the database being closed or that `write()` was called on the chained batch.
11631212

1213+
#### `LEVEL_ABORTED`
1214+
1215+
When an operation was aborted by the user.
1216+
11641217
#### `LEVEL_ENCODING_NOT_FOUND`
11651218

11661219
When a `keyEncoding` or `valueEncoding` option specified a named encoding that does not exist.
@@ -1564,6 +1617,14 @@ class ExampleSublevel extends AbstractSublevel {
15641617

15651618
The first argument to this constructor must be an instance of the relevant `AbstractLevel` implementation. The constructor will set `iterator.db` which is used (among other things) to access encodings and ensures that `db` will not be garbage collected in case there are no other references to it. The `options` argument must be the original `options` object that was passed to `db._iterator()` and it is therefore not (publicly) possible to create an iterator via constructors alone.
15661619

1620+
The `signal` option, if any and once signaled, should abort an in-progress `_next()`, `_nextv()` or `_all()` call and reject the promise returned by that call with a [`LEVEL_ABORTED`](#errors) error. Doing so is optional until a future semver-major release. Responsibilities are divided as follows:
1621+
1622+
1. Before a database has finished opening, `abstract-level` handles the signal
1623+
2. While a call is in progress, the implementation handles the signal
1624+
3. Once the signal is aborted, `abstract-level` rejects further calls.
1625+
1626+
A method like `_next()` therefore doesn't have to check the signal _before_ it start its asynchronous work, only _during_ that work. Whether to respect the signal and on which (potentially long-running) methods, is up to the implementation.
1627+
15671628
#### `iterator._next()`
15681629

15691630
Advance to the next entry and yield that entry. Must return a promise. If an error occurs, reject the promise. If the natural end of the iterator has been reached, resolve the promise with `undefined`. Otherwise resolve the promise with an array containing a `key` and `value`. If a `limit` was set and the iterator already yielded that many entries (via any of the methods) then `_next()` will not be called.

UPGRADING.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ As for why that last example works yet the same is not supported on a chained ba
202202

203203
#### 2.1. Promises all the way
204204

205-
All private methods that previously took a callback now use a promise. For example, the function signature `_get(key, options, callback)` has changed to `async _get(key, options)`. Same as in the public API, the new function signatures are predictable and the only method that requires special attention is `iterator._next()`. Which in addition now also takes an `options` argument. For details, please see the updated [README](./README.md#private-api-for-implementors).
205+
All private methods that previously took a callback now use a promise. For example, the function signature `_get(key, options, callback)` has changed to `async _get(key, options)`. Same as in the public API, the new function signatures are predictable and the only method that requires special attention is `iterator._next()`. For details, please see the updated [README](./README.md#private-api-for-implementors).
206206

207207
#### 2.2. Ticks
208208

@@ -240,9 +240,7 @@ class ExampleLevel extends AbstractLevel {
240240

241241
#### 2.3. A new way to abort iterator work
242242

243-
_This section is incomplete._
244-
245-
Closing an iterator now aborts work, if supported by implementation. The undocumented `abortOnClose` option of iterators (added as a workaround for `many-level`) has been removed in favor of AbortSignal.
243+
Iterators now take an experimental `signal` option that is an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). You can use the `signal` to abort an in-progress `_next()`, `_nextv()` or `_all()` call. Doing so is optional until a future semver-major release.
246244

247245
#### 2.4. Snapshots must be synchronous
248246

abstract-chained-batch.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const kStatus = Symbol('status')
1010
const kPublicOperations = Symbol('publicOperations')
1111
const kLegacyOperations = Symbol('legacyOperations')
1212
const kPrivateOperations = Symbol('privateOperations')
13-
const kCallClose = Symbol('callClose')
1413
const kClosePromise = Symbol('closePromise')
1514
const kLength = Symbol('length')
1615
const kPrewriteRun = Symbol('prewriteRun')
@@ -273,13 +272,8 @@ class AbstractChainedBatch {
273272
} else {
274273
this[kStatus] = 'writing'
275274

276-
// Prepare promise in case write() is called in the mean time
277-
let close
278-
this[kClosePromise] = new Promise((resolve, reject) => {
279-
close = () => {
280-
this[kCallClose]().then(resolve, reject)
281-
}
282-
})
275+
// Prepare promise in case close() is called in the mean time
276+
const close = prepareClose(this)
283277

284278
try {
285279
// Process operations added by prewrite hook functions
@@ -344,20 +338,33 @@ class AbstractChainedBatch {
344338
// First caller of close() or write() is responsible for error
345339
return this[kClosePromise].catch(noop)
346340
} else {
347-
this[kClosePromise] = this[kCallClose]()
341+
// Wrap promise to avoid race issues on recursive calls
342+
prepareClose(this)()
348343
return this[kClosePromise]
349344
}
350345
}
351346

352-
async [kCallClose] () {
353-
this[kStatus] = 'closing'
354-
await this._close()
355-
this.db.detachResource(this)
356-
}
357-
358347
async _close () {}
359348
}
360349

350+
const prepareClose = function (batch) {
351+
let close
352+
353+
batch[kClosePromise] = new Promise((resolve, reject) => {
354+
close = () => {
355+
privateClose(batch).then(resolve, reject)
356+
}
357+
})
358+
359+
return close
360+
}
361+
362+
const privateClose = async function (batch) {
363+
batch[kStatus] = 'closing'
364+
await batch._close()
365+
batch.db.detachResource(batch)
366+
}
367+
361368
class PrewriteData {
362369
constructor (privateOperations, publicOperations) {
363370
this[kPrivateOperations] = privateOperations
@@ -381,7 +388,7 @@ class PrewriteData {
381388
}
382389
}
383390

384-
function assertStatus (batch) {
391+
const assertStatus = function (batch) {
385392
if (batch[kStatus] !== 'open') {
386393
throw new ModuleError('Batch is not open: cannot change operations after write() or close()', {
387394
code: 'LEVEL_BATCH_NOT_OPEN'

0 commit comments

Comments
 (0)