diff --git a/index.js b/index.js index b734503..ecbee70 100644 --- a/index.js +++ b/index.js @@ -274,7 +274,8 @@ class HyperDB { definition, { versions = definition.versions, - snapshot = engine.snapshot(), + snapshotOptions = {}, + snapshot = engine.snapshot(snapshotOptions), updates = new Updates(1, []), rootInstance = null, writable = true, @@ -282,6 +283,7 @@ class HyperDB { } = {} ) { this.versions = versions + this.snapshotOptions = snapshotOptions this.context = context this.index = 0 // for the session this.engine = engine @@ -453,11 +455,16 @@ class HyperDB { this.engine = null } - _createSnapshot(rootInstance, writable, context) { - const snapshot = this.engineSnapshot.ref() + _createSnapshot(rootInstance, writable, context, options) { + const opts = engineOptions(options) + const snapshotOptions = opts ? { ...this.snapshotOptions, ...opts } : this.snapshotOptions + const snapshot = opts + ? this.engine.snapshotFrom(this.engineSnapshot, snapshotOptions) + : this.engineSnapshot.ref() return new HyperDB(this.engine, this.definition, { versions: this.versions, + snapshotOptions, snapshot, updates: this.updates.ref(), rootInstance, @@ -470,7 +477,7 @@ class HyperDB { maybeClosed(this) const context = (options && options.context) || this.context - return this._createSnapshot(this, false, context) + return this._createSnapshot(this, false, context, options) } // in future major, lets move transaction to be exclusive (aka sync) always @@ -494,7 +501,7 @@ class HyperDB { } const context = (options && options.context) || this.context - const tx = this._createSnapshot(this, true, context) + const tx = this._createSnapshot(this, true, context, options) tx.update() @@ -744,7 +751,7 @@ class HyperDB { this.updates.flush() this.engineSnapshot.unref() - this.engineSnapshot = this.engine.snapshot() + this.engineSnapshot = this.engine.snapshot(this.snapshotOptions) if (this.watchers !== null) { for (const fn of this.watchers) fn() @@ -783,6 +790,13 @@ function maybeClosed(db) { if (db.closing !== null) throw new Error('Hyperdb is closed') } +// strips hyperdb-level options, returns null if nothing is left for the engine +function engineOptions(options) { + if (!options) return null + const { context, ...opts } = options + return Object.keys(opts).length > 0 ? opts : null +} + function withinRange(range, key) { if (range.gte && b4a.compare(range.gte, key) > 0) return false if (range.gt && b4a.compare(range.gt, key) >= 0) return false diff --git a/lib/engine/bee.js b/lib/engine/bee.js index 385ba71..a583962 100644 --- a/lib/engine/bee.js +++ b/lib/engine/bee.js @@ -140,6 +140,10 @@ module.exports = class BeeEngine { return new BeeSnapshot(this.db.snapshot(), this._freeSnapBound) } + snapshotFrom(snap) { + return snap.ref() // snapshot options not supported + } + outdated(snap) { return ( snap === null || diff --git a/lib/engine/bee2.js b/lib/engine/bee2.js index e5ab238..c6620a2 100644 --- a/lib/engine/bee2.js +++ b/lib/engine/bee2.js @@ -157,9 +157,14 @@ module.exports = class Bee2Engine { return stream } - snapshot() { + snapshot(opts) { this.snaps.inc() - return new BeeSnapshot(this.db.snapshot(), this._freeSnapBound) + return new BeeSnapshot(this.db.snapshot(opts), this._freeSnapBound) + } + + snapshotFrom(snap, opts) { + this.snaps.inc() + return new BeeSnapshot(snap.snapshot.snapshot(opts), this._freeSnapBound) } checkout(opts) { diff --git a/lib/engine/rocks.js b/lib/engine/rocks.js index 16a1b34..9549fba 100644 --- a/lib/engine/rocks.js +++ b/lib/engine/rocks.js @@ -153,6 +153,10 @@ module.exports = class RocksEngine { return new RocksSnapshot(this.db.snapshot(), this.clock) } + snapshotFrom(snap) { + return snap.ref() // snapshot options not supported + } + outdated(snap) { return snap === null || snap.clock !== this.clock } diff --git a/test/checkout.js b/test/checkout.js index ae8a905..cd8ebe1 100644 --- a/test/checkout.js +++ b/test/checkout.js @@ -59,3 +59,83 @@ test.bee2('empty checkout', async function ({ create }, t) { await db.close() }) + +test.bee2('transaction discards dry-run writes', async function ({ create }, t) { + const db = await create() + + await db.insert('@db/members', { id: 'a', age: 40 }) + await db.flush() + + const snapshot = db.engineSnapshot + db.update() + t.is(db.engineSnapshot, snapshot, 'current update reuses the snapshot') + const ordinary = db.transaction() + t.is(ordinary.engineSnapshot, snapshot, 'ordinary transaction reuses the snapshot') + await ordinary.close() + + const tx = db.transaction({ timeout: 100 }) + t.is(tx.engineSnapshot.snapshot.config.timeout, 100) + + await db.insert('@db/members', { id: 'b', age: 41 }) + await db.flush() + + t.ok(await tx.get('@db/members', { id: 'a' })) + t.absent(await tx.get('@db/members', { id: 'b' })) + + await tx.insert('@db/members', { id: 'c', age: 42 }) + + t.ok(await tx.get('@db/members', { id: 'c' })) + + t.ok(await db.get('@db/members', { id: 'a' })) + t.ok(await db.get('@db/members', { id: 'b' })) + t.absent(await db.get('@db/members', { id: 'c' })) + + await tx.close() + await db.close() +}) + +test.bee2('transaction timeout', async function ({ create }, t) { + const db = await create() + await db.ready() + + // unavailable block will trigger timeout + db.db.move({ length: db.core.length + 1 }) + const tx = db.transaction({ timeout: 100 }) + + await t.exception(tx.get('@db/members', { id: 'a' }), /REQUEST_TIMEOUT/) + + await tx.close() + await db.close() +}) + +test.bee2('snapshot options keep the parent position', async function ({ create }, t) { + const db = await create() + + await db.insert('@db/members', { id: 'a', age: 40 }) + await db.flush() + + // pending write on root, then a commit past it leaves root outdated + await db.insert('@db/members', { id: 'p', age: 1 }) + const ahead = db.transaction() + await ahead.insert('@db/members', { id: 'b', age: 41 }) + await ahead.flush() + + const snap = db.snapshot({ timeout: 100, wait: false }) + t.is(snap.engineSnapshot.snapshot.config.timeout, 100) + t.is(snap.engineSnapshot.snapshot.config.wait, false) + t.absent(await snap.get('@db/members', { id: 'b' }), 'stays at root position') + + const child = snap.snapshot({ timeout: 200 }) + t.is(child.engineSnapshot.snapshot.config.timeout, 200) + t.is(child.engineSnapshot.snapshot.config.wait, false, 'inherits wait') + + const tx = db.transaction({ timeout: 100 }) + t.is(tx.updates.size, 0, 'drops stale pending writes') + t.ok(await tx.get('@db/members', { id: 'b' })) + t.is(tx.engineSnapshot.snapshot.config.timeout, 100, 'keeps options after update') + + await child.close() + await snap.close() + await tx.close() + await db.close() +}) diff --git a/test/snapshot.js b/test/snapshot.js index 7df947a..e032382 100644 --- a/test/snapshot.js +++ b/test/snapshot.js @@ -123,3 +123,46 @@ test('root close waits for snapshot closes', async function ({ create }, t) { t.ok(snap.closed) t.ok(snapOfSnap.closed) }) + +test('context-only options share the parent snapshot', async function ({ create }, t) { + const db = await create() + + await db.insert('@db/members', { id: 'a', age: 40 }) + await db.flush() + + const context = {} + + const snap = db.snapshot({ context }) + t.is(snap.context, context) + t.is(snap.engineSnapshot, db.engineSnapshot, 'snapshot reuses the engine snapshot') + t.absent('context' in snap.snapshotOptions, 'context is not an engine option') + + const tx = db.transaction({ context }) + t.is(tx.context, context) + t.is(tx.engineSnapshot, db.engineSnapshot, 'transaction reuses the engine snapshot') + t.absent('context' in tx.snapshotOptions, 'context is not an engine option') + + await snap.close() + await tx.close() + await db.close() +}) + +test('passing snapshot options works on every engine', async function ({ create }, t) { + const db = await create() + + await db.insert('@db/members', { id: 'a', age: 40 }) + await db.flush() + + const snap = db.snapshot({ timeout: 1000 }) + t.alike(await snap.get('@db/members', { id: 'a' }), { id: 'a', age: 40 }) + + const tx = db.transaction({ timeout: 1000 }) + t.alike(await tx.get('@db/members', { id: 'a' }), { id: 'a', age: 40 }) + await tx.insert('@db/members', { id: 'b', age: 41 }) + await tx.flush() + + t.alike(await db.get('@db/members', { id: 'b' }), { id: 'b', age: 41 }) + + await snap.close() + await db.close() +})