From 5a6e1be62639fdf599bea0b6ef15e5395aae8924 Mon Sep 17 00:00:00 2001 From: stelcheck Date: Wed, 29 Nov 2017 21:07:51 +0900 Subject: [PATCH] locking/unlocking Locking and unlocking of topic instances. --- src/classes/ValidatedTopic.ts | 88 +++++++++++ test/topic/index.ts | 1 + test/topic/lock-unlock.ts | 265 ++++++++++++++++++++++++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 test/topic/lock-unlock.ts diff --git a/src/classes/ValidatedTopic.ts b/src/classes/ValidatedTopic.ts index 997edb8..3a031e1 100644 --- a/src/classes/ValidatedTopic.ts +++ b/src/classes/ValidatedTopic.ts @@ -8,6 +8,23 @@ import { ValidationError } from '../errors' const isObject = require('isobject') +/** + * Wrap a promise around state.distribute + * + * @param state MAGE state + */ +async function promisifyStateDistribute(state: mage.core.IState) { + return new Promise((resolve, reject) => { + state.distribute((error?: Error) => { + if (error) { + return reject(error) + } + + resolve() + }) + }) +} + /** * The IStaticThis interface is required * for us to be able to create static factory functions @@ -504,6 +521,68 @@ export default class ValidatedTopic { return this.getState().archivist.del(this.getTopic(), this.getIndex()) } + /** + * Check if the current topic is locked. + */ + public async isLocked(state: mage.core.IState = new mage.core.State()) { + return new Promise((resolve, reject) => { + state.archivist.get(this.getTopic(), this.getLockIndex(), { + optional: true + }, (error, isLocked) => { + if (error) { + return reject(error) + } + + resolve(!!isLocked) + }) + }) + } + + /** + * Lock this topic instance + * + * By default, locked topics will be unlocked + * once `state.distribute` is called on the state associated + * with this topic. If you do not want this behaviour, + * set `autoUnlock` to true, and make sure + * to call `unlock` manually when you are done. + * + * @param autoUnlock Unlock this topic once we complete the transaction + */ + public async lock(autoUnlock: boolean = true) { + const state = new mage.core.State() + const isLocked = await this.isLocked(state) + + if (isLocked) { + throw new Error('Topic is locked') + } + + const lockIndex = this.getLockIndex() + + // Auto-unlock + if (autoUnlock) { + this.getState().archivist.del(this.getTopic(), lockIndex) + } + + state.archivist.set(this.getTopic(), lockIndex, 'locked') + + return promisifyStateDistribute(state) + } + + /** + * Unlock this topic instance + * + * You will only need to call this manually when + * you have previously called `lock` with `autoUnlock` + * set to false. + */ + public async unlock() { + const state = new mage.core.State() + state.archivist.del(this.getTopic(), this.getLockIndex()) + + return promisifyStateDistribute(state) + } + /** * Validate the current instance */ @@ -527,4 +606,13 @@ export default class ValidatedTopic { index: this.getIndex() }, errors) } + + /** + * Generate a lock index for this current topic + */ + private getLockIndex() { + return Object.assign(this.getIndex(), { + mageValidatorLock: 'locked' + }) + } } diff --git a/test/topic/index.ts b/test/topic/index.ts index 4f5aef6..f21fcc6 100644 --- a/test/topic/index.ts +++ b/test/topic/index.ts @@ -58,4 +58,5 @@ describe('Validated Topics', function () { require('./add-set-touch') require('./del') require('./type-decorator') + require('./lock-unlock') }) diff --git a/test/topic/lock-unlock.ts b/test/topic/lock-unlock.ts new file mode 100644 index 0000000..e11f298 --- /dev/null +++ b/test/topic/lock-unlock.ts @@ -0,0 +1,265 @@ +import { TestTopic } from './' +import * as assert from 'assert' +import * as mage from 'mage' + +let state: mage.core.IState + +describe('lock, unlock and isLocked', function () { + let RealState: mage.core.IState + + beforeEach(() => { + state = new mage.core.State() + RealState = mage.core.State + }) + + afterEach(() => { + mage.core.State = RealState + }) + + it('lock will throw if state.archivist.get.fails', async () => { + const id = '12132341233' + const message = 'fails' + + mage.core.State = class { + public archivist = { + get(_topic: string, _index: any, _cfg: any, cb: any) { + cb(new Error(message)) + } + } + } + + const topic = await TestTopic.create(state, { id }) + try { + await topic.isLocked() + } catch (error) { + return assert.equal(error.message, message) + } + + + throw new Error('did not fail') + }) + + it('isLocked checks if a key is locked', async () => { + const id = '12132341233' + + mage.core.State = class { + public archivist = { + get(topic: string, index: any, cfg: any, cb: any) { + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + assert.equal(index.mageValidatorLock, 'locked') + assert.equal(cfg.optional, true) + + cb(null, 'locked') + } + } + } + + const topic = await TestTopic.create(state, { id }) + const isLocked = await topic.isLocked() + + assert.equal(isLocked, true) + }) + + it('isLocked can take a state as a parameter', async () => { + const id = '12132341233' + + mage.core.State = class { + public archivist = { + get(topic: string, index: any, cfg: any, cb: any) { + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + assert.equal(index.mageValidatorLock, 'locked') + assert.equal(cfg.optional, true) + + cb(null, 'locked') + } + } + } + + const topic = await TestTopic.create(state, { id }) + const isLocked = await topic.isLocked(new mage.core.State()) + + assert.equal(isLocked, true) + }) + + it('lock will throw if it fails to get the lock', async () => { + const id = '12132341233' + const message = 'fails' + + mage.core.State = class { + public archivist = { + get(_topic: string, _index: any, _cfg: any, cb: any) { + cb(new Error(message)) + } + } + } + + const topic = await TestTopic.create(state, { id }) + try { + await topic.lock() + } catch (error) { + return assert.equal(error.message, message) + } + + + throw new Error('did not fail') + }) + + it('lock will throw if the topic is locked', async () => { + const id = '12132341233' + + mage.core.State = class { + public archivist = { + get(topic: string, index: any, cfg: any, cb: any) { + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + assert.equal(index.mageValidatorLock, 'locked') + assert.equal(cfg.optional, true) + + return cb(null, 'locked') + } + } + } + + const topic = await TestTopic.create(state, { id }) + try { + await topic.lock() + } catch (error) { + return assert.equal(error.message, ('Topic is locked')) + } + + + throw new Error('did not fail') + }) + + it('lock will lock, and set autoUnlock operation on the topic state', async () => { + const id = '1' + let wasDistributeCalled = false + + mage.core.State = class { + public archivist = { + get(_topic: string, _index: any, _cfg: any, cb: any) { + return cb() + }, + set(topic: string, index: any, data: any) { + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + assert.equal(data, 'locked') + } + } + + /* tslint:disable:prefer-function-over-method */ + public distribute(cb: (error?: Error) => void) { + wasDistributeCalled = true + cb() + } + } + + const topic = await TestTopic.create(state, { id }) + await topic.lock() + + assert(wasDistributeCalled) + + const loaded = ( state.archivist).loaded.TestTopicoids1omageValidatorLockslocked + assert(loaded) + assert.equal(loaded.operation, 'del') + assert.equal(loaded.topic, 'TestTopic') + assert.deepEqual(loaded.index, { + id, + mageValidatorLock: 'locked' + }) + }) + + it('lock will not autoUnlock if autoUnlock is set to false', async () => { + const id = '1' + let wasDistributeCalled = false + + mage.core.State = class { + public archivist = { + get(_topic: string, _index: any, _cfg: any, cb: any) { + return cb() + }, + set(topic: string, index: any, data: any) { + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + assert.equal(data, 'locked') + } + } + + /* tslint:disable:prefer-function-over-method */ + public distribute(cb: (error?: Error) => void) { + wasDistributeCalled = true + cb() + } + } + + const topic = await TestTopic.create(state, { id }) + await topic.lock(false) + + assert(wasDistributeCalled) + + const loaded = ( state.archivist).loaded.TestTopicoids1omageValidatorLockslocked + assert.equal(loaded, undefined) + }) + + it('lock will fail if it cannot distribute the lock', async () => { + const id = '1' + const message = 'oh maimai' + + mage.core.State = class { + public archivist = { + get(_topic: string, _index: any, _cfg: any, cb: any) { + return cb() + }, + set(topic: string, index: any, data: any) { + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + assert.equal(data, 'locked') + } + } + + /* tslint:disable:prefer-function-over-method */ + public distribute(cb: (error?: Error) => void) { + cb(new Error(message)) + } + } + + const topic = await TestTopic.create(state, { id }) + try { + await topic.lock() + } catch (error) { + return assert.equal(error.message, message) + } + + throw new Error('did not fail') + }) + + it('unlock deletes the lock immediately', async () => { + const id = '1' + let wasDelCalled = false + let wasDistributeCalled = false + + mage.core.State = class { + public archivist = { + del(topic: string, index: any) { + wasDelCalled = true + assert.equal(topic, 'TestTopic') + assert.equal(index.id, id) + } + } + + /* tslint:disable:prefer-function-over-method */ + public distribute(cb: (error?: Error) => void) { + wasDistributeCalled = true + cb() + } + } + + const topic = await TestTopic.create(state, { id }) + await topic.unlock() + + assert(wasDelCalled) + assert(wasDistributeCalled) + }) +})