New lifecycle management for GunSmith

This commit is contained in:
Daniel Lugo 2021-09-17 14:50:12 -04:00
parent a41862c7b0
commit 1dc71d31c2
2 changed files with 125 additions and 79 deletions

View file

@ -99,7 +99,7 @@ const handleMsg = msg => {
logger.error( logger.error(
`Could not find request for put message from gun subprocess. Data will be logged below.` `Could not find request for put message from gun subprocess. Data will be logged below.`
) )
logger.info({ console.log({
msg, msg,
pendingPut: pendingPut || 'No pending put found', pendingPut: pendingPut || 'No pending put found',
allPendingPuts: pendingPuts allPendingPuts: pendingPuts
@ -240,7 +240,12 @@ const isReady = () =>
let procID = 0 let procID = 0
let killed = false
const forge = async () => { const forge = async () => {
if (killed) {
throw new Error('Tried to forge after killing GunSmith')
}
logger.info(`Forging Gun # ${++procID}`) logger.info(`Forging Gun # ${++procID}`)
if (isForging) { if (isForging) {
throw new Error('Double forge?') throw new Error('Double forge?')
@ -453,11 +458,12 @@ function createReplica(path, afterMap = false) {
return this return this
}, },
once(cb, opts = { wait: 500 }) { once(cb, opts = { wait: 500 }) {
// We could use this.on() but then we couldn't call .off()
const tmp = createReplica(path, afterMap)
if (afterMap) { if (afterMap) {
throw new Error('Cannot call once() after map() on a GunSmith node') throw new Error('Cannot call once() after map() on a GunSmith node')
} }
// We could use this.on() but then we couldn't call .off()
const tmp = createReplica(path, afterMap)
/** @type {GunT.ListenerData} */ /** @type {GunT.ListenerData} */
let lastVal = null let lastVal = null
@ -713,28 +719,28 @@ function createUserReplica() {
return completeReplica return completeReplica
} }
/**
* @typedef {Smith.GunSmithNode & { kill(): void }} RootNode
*/
/** /**
* @param {import('gun/types/options').IGunConstructorOptions} opts * @param {import('gun/types/options').IGunConstructorOptions} opts
* @returns {RootNode} * @returns {Smith.GunSmithNode}
*/ */
const Gun = opts => { const Gun = opts => {
lastOpts = opts lastOpts = opts
forge() forge()
// We should ideally wait for a response but we'd break the constructor's return createReplica('$root')
// signature
return {
...createReplica('$root'),
kill() {
currentGun.send('bye')
currentGun.disconnect()
currentGun.kill()
}
}
} }
module.exports = Gun module.exports = Gun
module.exports.kill = () => {
if (currentGun) {
currentGun.send('bye')
currentGun.off('message', handleMsg)
currentGun.disconnect()
currentGun.kill()
// @ts-ignore
currentGun = null
killed = true
logger.info('Killed gunsmith.')
}
}

View file

@ -8,25 +8,54 @@ const Gun = require('./GunSmith')
const words = require('random-words') const words = require('random-words')
const fs = require('fs') const fs = require('fs')
const logger = require('../../config/log')
const { removeBuiltInGunProps } = require('./misc') const { removeBuiltInGunProps } = require('./misc')
if (!fs.existsSync('./test-radata')) { if (!fs.existsSync('./test-radata')) {
fs.mkdirSync('./test-radata') fs.mkdirSync('./test-radata')
} }
const createInstance = () => // start with true, have first test doesn't check, else all other test start to
Gun({ // run right away
axe: false, let isBusy = true
multicast: false,
file: './test-radata/' + words({ exactly: 2 }).join('-') const instance = Gun({
axe: false,
multicast: false,
file: './test-radata/' + words({ exactly: 2 }).join('-')
})
const release = () => {
isBusy = false
}
/**
* @returns {Promise<void>}
*/
const whenReady = () =>
new Promise(res => {
setTimeout(() => {
if (isBusy) {
whenReady().then(res)
} else {
isBusy = true
res()
}
}, 1000)
}) })
describe('gun smith', () => { describe('gun smith', () => {
it('puts a true and reads it with once()', done => { afterAll(() => {
expect.hasAssertions() Gun.kill()
})
it('puts a true and reads it with once()', async function(done) {
// await whenReady()
logger.info('puts a true and reads it with once()')
const a = words() const a = words()
const b = words() const b = words()
const instance = createInstance()
instance instance
.get(a) .get(a)
.get(b) .get(b)
@ -37,39 +66,53 @@ describe('gun smith', () => {
.get(b) .get(b)
.once( .once(
val => { val => {
expect(val).toStrictEqual(true) expect(val).toBe(true)
instance.kill()
done() done()
release()
}, },
{ wait: 5000 } { wait: 1000 }
) )
}) })
it('puts a false and reads it with once()', done => { it('puts a false and reads it with once()', async function(done) {
expect.hasAssertions() await whenReady()
logger.info('puts a false and reads it with once()')
const a = words() const a = words()
const b = words() const b = words()
const instance = createInstance()
instance await new Promise((res, rej) => {
.get(a) instance
.get(b) .get(a)
.put(false) .get(b)
.put(false, ack => {
if (ack.err) {
rej(new Error(ack.err))
} else {
// @ts-ignore
res()
}
})
})
instance instance
.get(a) .get(a)
.get(b) .get(b)
.once(val => { .once(
expect(val).toBe(false) val => {
instance.kill() expect(val).toBe(false)
done() release()
}) done()
},
{ wait: 1000 }
)
}) })
it('puts numbers and reads them with once()', done => { it('puts numbers and reads them with once()', async done => {
expect.hasAssertions() expect.hasAssertions()
await whenReady()
const a = words() const a = words()
const b = words() const b = words()
const instance = createInstance()
instance instance
.get(a) .get(a)
.get(b) .get(b)
@ -80,17 +123,17 @@ describe('gun smith', () => {
.get(b) .get(b)
.once(val => { .once(val => {
expect(val).toBe(5) expect(val).toBe(5)
instance.kill() release()
done() done()
}) })
}) })
it('puts strings and reads them with once()', done => { it('puts strings and reads them with once()', async done => {
expect.hasAssertions() expect.hasAssertions()
await whenReady()
const a = words() const a = words()
const b = words() const b = words()
const sentence = words({ exactly: 50 }).join(' ') const sentence = words({ exactly: 50 }).join(' ')
const instance = createInstance()
instance instance
.get(a) .get(a)
@ -102,13 +145,14 @@ describe('gun smith', () => {
.get(b) .get(b)
.once(val => { .once(val => {
expect(val).toBe(sentence) expect(val).toBe(sentence)
instance.kill() release()
done() done()
}) })
}) })
it('merges puts', done => { it('merges puts', async done => {
expect.hasAssertions() expect.hasAssertions()
await whenReady()
const a = { const a = {
a: 1 a: 1
} }
@ -116,7 +160,6 @@ describe('gun smith', () => {
b: 1 b: 1
} }
const c = { ...a, ...b } const c = { ...a, ...b }
const instance = createInstance()
const node = instance.get('foo').get('bar') const node = instance.get('foo').get('bar')
@ -129,21 +172,21 @@ describe('gun smith', () => {
return return
} }
expect(removeBuiltInGunProps(data)).toEqual(c) expect(removeBuiltInGunProps(data)).toEqual(c)
instance.kill() release()
done() done()
}) })
}) })
it('writes primitive items into sets', done => { it('writes primitive items into sets', async done => {
expect.hasAssertions() expect.hasAssertions()
const instance = createInstance() await whenReady()
const node = instance.get(words()).get(words()) const node = instance.get(words()).get(words())
const item = node.set('hello') const item = node.set('hello')
node.once(data => { node.once(data => {
if (typeof data !== 'object' || data === null) { if (typeof data !== 'object' || data === null) {
instance.kill() release()
done(new Error('Data not an object')) done(new Error('Data not an object'))
return return
} }
@ -151,14 +194,14 @@ describe('gun smith', () => {
expect(removeBuiltInGunProps(data)).toEqual({ expect(removeBuiltInGunProps(data)).toEqual({
[item._.get]: 'hello' [item._.get]: 'hello'
}) })
instance.kill() release()
done() done()
}) })
}) })
it('writes object items into sets', done => { it('writes object items into sets', async done => {
expect.hasAssertions() expect.hasAssertions()
const instance = createInstance() await whenReady()
const node = instance.get(words()).get(words()) const node = instance.get(words()).get(words())
const obj = { const obj = {
@ -175,14 +218,14 @@ describe('gun smith', () => {
} }
expect(removeBuiltInGunProps(data)).toEqual(obj) expect(removeBuiltInGunProps(data)).toEqual(obj)
instance.kill() release()
done() done()
}) })
}) })
it('maps over a primitive set', done => { it('maps over a primitive set', async done => {
expect.assertions(100) expect.assertions(100)
const instance = createInstance() await whenReady()
const node = instance.get(words()).get(words()) const node = instance.get(words()).get(words())
@ -197,16 +240,15 @@ describe('gun smith', () => {
expect(ids).toContain(id) expect(ids).toContain(id)
checked++ checked++
if (checked === 50) { if (checked === 50) {
instance.kill() release()
done() done()
} }
}) })
}) })
it('maps over an object set', done => { it('maps over an object set', async done => {
expect.assertions(100) expect.assertions(100)
const instance = createInstance() await whenReady()
const node = instance.get(words()).get(words()) const node = instance.get(words()).get(words())
const items = words({ exactly: 50 }).map(w => ({ const items = words({ exactly: 50 }).map(w => ({
@ -222,16 +264,15 @@ describe('gun smith', () => {
expect(ids).toContain(id) expect(ids).toContain(id)
checked++ checked++
if (checked === 50) { if (checked === 50) {
instance.kill() release()
done() done()
} }
}) })
}) })
it('offs `on()`s', done => { it('offs `on()`s', async done => {
expect.assertions(1) expect.assertions(1)
const instance = createInstance() await whenReady()
const node = instance.get(words()).get(words()) const node = instance.get(words()).get(words())
const fn = jest.fn() const fn = jest.fn()
@ -245,16 +286,15 @@ describe('gun smith', () => {
done(new Error(ack.err)) done(new Error(ack.err))
} else { } else {
expect(fn).not.toHaveBeenCalled() expect(fn).not.toHaveBeenCalled()
instance.kill()
done() done()
release()
} }
}) })
}) })
it('offs `map().on()`s', done => { it('offs `map().on()`s', async done => {
expect.assertions(1) expect.assertions(1)
const instance = createInstance() await whenReady()
const node = instance.get(words()).get(words()) const node = instance.get(words()).get(words())
const fn = jest.fn() const fn = jest.fn()
@ -270,17 +310,17 @@ describe('gun smith', () => {
done(new Error(ack.err)) done(new Error(ack.err))
} else { } else {
expect(fn).not.toHaveBeenCalled() expect(fn).not.toHaveBeenCalled()
instance.kill()
done() done()
release()
} }
}) })
}) })
// eslint-disable-next-line jest/no-commented-out-tests // eslint-disable-next-line jest/no-commented-out-tests
// it('on()s and handles object>primitive>object transitions', done => { // it('on()s and handles object>primitive>object transitions', async done => {
// expect.assertions(3) // expect.assertions(3)
// const instance = createInstance() // await whenReady()
//
// const a = { // const a = {
// one: 1 // one: 1
// } // }
@ -311,7 +351,7 @@ describe('gun smith', () => {
it('provides an user node with create(), auth() and leave()', async done => { it('provides an user node with create(), auth() and leave()', async done => {
expect.assertions(6) expect.assertions(6)
const instance = createInstance() await whenReady()
const user = instance.user() const user = instance.user()
const alias = words() const alias = words()
@ -333,13 +373,13 @@ describe('gun smith', () => {
expect(authAck.sea.pub).toEqual(pub) expect(authAck.sea.pub).toEqual(pub)
expect(user.is?.pub).toEqual(pub) expect(user.is?.pub).toEqual(pub)
user.leave() user.leave()
instance.kill()
done() done()
release()
}) })
it('provides thenables for values', async done => { it('provides thenables for values', async done => {
expect.assertions(1) expect.assertions(1)
const instance = createInstance() await whenReady()
const a = words() const a = words()
const b = words() const b = words()
@ -362,7 +402,7 @@ describe('gun smith', () => {
.get(b) .get(b)
.then() .then()
expect(fetch).toEqual(value) expect(fetch).toEqual(value)
instance.kill()
done() done()
release()
}) })
}) })