Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

locking/unlocking #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/classes/ValidatedTopic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about TypeScript, but in ES (afaik) if you return a Promise, you should not mark your function async.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't matter: if you return a promise on an async call, then that promise will be the promise to be returned.

tslint is currently configured to request that functions returning promises or otherwise using await require the async keyword. I believe this is for readability purposes (e.g. when checking the function signature), but also to make sure that whatever you return is a promise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if that is true for ES as well?

Copy link
Member Author

@stelcheck stelcheck Dec 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

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
*/
Expand All @@ -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'
})
}
}
1 change: 1 addition & 0 deletions test/topic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ describe('Validated Topics', function () {
require('./add-set-touch')
require('./del')
require('./type-decorator')
require('./lock-unlock')
})
265 changes: 265 additions & 0 deletions test/topic/lock-unlock.ts
Original file line number Diff line number Diff line change
@@ -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 = <any> mage.core.State
})

afterEach(() => {
mage.core.State = <any> RealState
})

it('lock will throw if state.archivist.get.fails', async () => {
const id = '12132341233'
const message = 'fails'

mage.core.State = <any> 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 = <any> 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 = <any> 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 = <any> 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 = <any> 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 = <any> 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 = (<any> 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 = <any> 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 = (<any> 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 = <any> 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 = <any> 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)
})
})