Rename to pzp and upgrade modules

This commit is contained in:
Jacob Karlsson 2024-04-30 17:17:26 +02:00
parent 9075f983d8
commit b0157ed984
11 changed files with 213 additions and 146 deletions

View File

@ -1,25 +0,0 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test

13
.woodpecker.yaml Normal file
View File

@ -0,0 +1,13 @@
matrix:
NODE_VERSION:
- 18
- 20
steps:
test:
when:
event: [push]
image: node:${NODE_VERSION}
commands:
- npm install
- npm test

View File

@ -1,9 +1,7 @@
**Work in progress** # pzp-gc
## Installation ## Installation
We're not on npm yet. In your package.json, include this as
```js ```js
"ppppp-gc": "github:staltz/ppppp-gc" npm install pzp-gc
``` ```

View File

@ -2,9 +2,20 @@
const multicb = require('multicb') const multicb = require('multicb')
const makeDebug = require('debug') const makeDebug = require('debug')
// @ts-ignore
const p = (fn) => (...args) => {
return new Promise((res, rej) => {
// @ts-ignore
fn(...args, (err, val) => {
if (err) return rej(err)
return res(val)
})
})
}
/** /**
* @typedef {ReturnType<import('ppppp-db').init>} PPPPPDB * @typedef {ReturnType<import('pzp-db').init>} pzpDB
* @typedef {ReturnType<import('ppppp-goals').init>} PPPPPGoal * @typedef {ReturnType<import('pzp-goals').init>} pzpGoal
* @typedef {{ * @typedef {{
* gc: { * gc: {
* maxLogBytes: number * maxLogBytes: number
@ -23,12 +34,12 @@ const makeDebug = require('debug')
*/ */
/** /**
* @param {{ db: PPPPPDB, goals: PPPPPGoal }} peer * @param {{ db: pzpDB, goals: pzpGoal }} peer
* @param {Config} config * @param {Config} config
*/ */
function initGC(peer, config) { function initGC(peer, config) {
// State // State
const debug = makeDebug('ppppp:gc') const debug = makeDebug('pzp:gc')
let stopMonitoringLogSize = /** @type {CallableFunction | null} */ (null) let stopMonitoringLogSize = /** @type {CallableFunction | null} */ (null)
let hasCleanupScheduled = false let hasCleanupScheduled = false
@ -47,77 +58,79 @@ function initGC(peer, config) {
/** /**
* Deletes messages that don't correspond to any goal. * Deletes messages that don't correspond to any goal.
* @private * @private
* @param {CB<void>} cb * @return {Promise<void>}
*/ */
function cleanup(cb) { async function cleanup() {
debug('Cleanup started') return new Promise(async (res, rej) => {
const startTime = Date.now() debug('Cleanup started')
const done = multicb({ pluck: 1 }) const startTime = Date.now()
const done = multicb({ pluck: 1 })
/** /**
* @param {string} errExplanation * @param {string} errExplanation
*/ */
function makeRecCB(errExplanation) { function makeRecCB(errExplanation) {
const cb = done() const cb = done()
return (/**@type {Error=}*/ err) => { return (/**@type {Error=}*/ err) => {
if (err) debug('%s: %s', errExplanation, flattenCauseChain(err)) if (err) debug('%s: %s', errExplanation, flattenCauseChain(err))
cb() cb()
}
} }
}
let waiting = false let waiting = false
for (const rec of peer.db.records()) { for await (const rec of peer.db.records()) {
if (!rec.msg) continue if (!rec.msg) continue
const { id: msgID, msg } = rec const { id: msgID, msg } = rec
const [purpose, details] = peer.goals.getMsgPurpose(msgID, msg) const [purpose, details] = await p(peer.goals.getMsgPurpose)(msgID, msg)
switch (purpose) { switch (purpose) {
case 'goal': { case 'goal': {
continue // don't cleanup continue // don't cleanup
} }
case 'none': { case 'none': {
const recCB = makeRecCB('Failed to delete msg when cleaning up') const recCB = makeRecCB('Failed to delete msg when cleaning up')
debug('Deleting msg %s with purpose=none', msgID) debug('Deleting msg %s with purpose=none', msgID)
peer.db.del(msgID, recCB)
waiting = true
continue
}
case 'ghost': {
const { tangleID, span } = details
const recCB = makeRecCB('Failed to delete ghost msg when cleaning up')
// TODO: Could one msg be a ghostable in MANY tangles? Or just one?
debug('Deleting and ghosting msg %s with purpose=ghost', msgID)
peer.db.ghosts.add({ tangleID, msgID, span }, (err) => {
if (err) return recCB(err)
peer.db.del(msgID, recCB) peer.db.del(msgID, recCB)
}) waiting = true
waiting = true continue
continue }
} case 'ghost': {
case 'trail': { const { tangleID, span } = details
if (!msg.data) continue // it's already erased const recCB = makeRecCB('Failed to delete ghost msg when cleaning up')
const recCB = makeRecCB('Failed to erase trail msg when cleaning up') // TODO: Could one msg be a ghostable in MANY tangles? Or just one?
debug('Erasing msg %s with purpose=trail', msgID) debug('Deleting and ghosting msg %s with purpose=ghost', msgID)
peer.db.erase(msgID, recCB) peer.db.ghosts.add({ tangleID, msgID, span }, (err) => {
waiting = true if (err) return recCB(err)
continue peer.db.del(msgID, recCB)
} })
default: { waiting = true
cb(new Error('Unreachable')) continue
return }
case 'trail': {
if (!msg.data) continue // it's already erased
const recCB = makeRecCB('Failed to erase trail msg when cleaning up')
debug('Erasing msg %s with purpose=trail', msgID)
peer.db.erase(msgID, recCB)
waiting = true
continue
}
default: {
rej(new Error('Unreachable'))
return
}
} }
} }
}
if (waiting) done(whenEnded) if (waiting) done(whenEnded)
else whenEnded() else whenEnded()
/** @param {Error=} err */ /** @param {Error=} err */
function whenEnded(err) { function whenEnded(err) {
const duration = Date.now() - startTime const duration = Date.now() - startTime
if (err) debug('Cleanup ended with an error %s', err.message ?? err) if (err) debug('Cleanup ended with an error %s', err.message ?? err)
else debug('Cleanup completed in %sms', duration) else debug('Cleanup completed in %sms', duration)
cb() res()
} }
})
} }
/** /**
@ -203,7 +216,7 @@ function initGC(peer, config) {
if (needsCompaction) reportCompactionNeed(percentDeleted, stats) if (needsCompaction) reportCompactionNeed(percentDeleted, stats)
hasCleanupScheduled = true hasCleanupScheduled = true
if (needsCleanup) { if (needsCleanup) {
cleanup(() => { cleanup().finally(() => {
compact(() => { compact(() => {
hasCleanupScheduled = false hasCleanupScheduled = false
}) })
@ -253,7 +266,7 @@ function initGC(peer, config) {
*/ */
function forceImmediately(cb) { function forceImmediately(cb) {
debug('Force clean and compact immediately') debug('Force clean and compact immediately')
cleanup(() => { cleanup().finally(() => {
compact(cb) compact(cb)
}) })
} }

View File

@ -1,13 +1,13 @@
{ {
"name": "ppppp-gc", "name": "pzp-gc",
"version": "1.0.0", "version": "0.0.1",
"description": "PPPPP garbage collector", "description": "PZP garbage collector",
"author": "Andre Staltz <contact@staltz.com>", "author": "Andre Staltz <contact@staltz.com>",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/staltz/ppppp-gc", "homepage": "https://codeberg.org/pzp/pzp-gc",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:staltz/ppppp-gc.git" "url": "git@codeberg.org:pzp/pzp-gc.git"
}, },
"main": "index.js", "main": "index.js",
"files": [ "files": [
@ -31,12 +31,12 @@
"@types/debug": "4.1.9", "@types/debug": "4.1.9",
"bs58": "^5.0.0", "bs58": "^5.0.0",
"c8": "7", "c8": "7",
"ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929", "pzp-caps": "^1.0.0",
"ppppp-db": "github:staltz/ppppp-db#667b33779d98aff12a9b0cd2d7c80469a95cd04e", "pzp-db": "^1.0.1",
"ppppp-dict": "github:staltz/ppppp-dict#6f0ff4e3383a8c18b766949f6db9b51460ecb640", "pzp-dict": "^1.0.0",
"ppppp-goals": "github:staltz/ppppp-goals#f862c2de624649906a4375711f3813db3b94a2ca", "pzp-goals": "^1.0.0",
"ppppp-keypair": "github:staltz/ppppp-keypair#61ef4420578f450dc2cc7b1efc1c5a691a871c74", "pzp-keypair": "^1.0.0",
"ppppp-set": "github:staltz/ppppp-set#8983ba29f03db95a76b4bd9a55aa4392b350fdbb", "pzp-set": "^1.0.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"rimraf": "^4.4.0", "rimraf": "^4.4.0",

View File

@ -51,7 +51,7 @@ test('Dict ghosts', async (t) => {
let msgID3 let msgID3
let msgID4 let msgID4
let msgID5 let msgID5
for (const rec of alice.db.records()) { for await (const rec of alice.db.records()) {
if (rec.msg.metadata.dataSize === 0) mootID = rec.id if (rec.msg.metadata.dataSize === 0) mootID = rec.id
if (rec.msg.data?.update?.name === 'alice') msgID1 = rec.id if (rec.msg.data?.update?.name === 'alice') msgID1 = rec.id
if (rec.msg.data?.update?.age === 24) msgID2 = rec.id if (rec.msg.data?.update?.age === 24) msgID2 = rec.id
@ -60,18 +60,22 @@ test('Dict ghosts', async (t) => {
if (rec.msg.data?.update?.name === 'ALICE') msgID5 = rec.id if (rec.msg.data?.update?.name === 'ALICE') msgID5 = rec.id
} }
const msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
// Assert situation before GC // Assert situation before GC
assert.deepEqual( assert.deepEqual(
getFields([...alice.db.msgs()]), getFields(msgs),
['alice', 24, 'Alice', 25, 'ALICE'], ['alice', 24, 'Alice', 25, 'ALICE'],
'has all dict msgs' 'has all dict msgs'
) )
assert.ok(isErased(alice.db.get(mootID)), 'moot by def erased') assert.ok(isErased(await p(alice.db.get)(mootID)), 'moot by def erased')
assert.ok(isPresent(alice.db.get(msgID1)), 'msg1 exists') assert.ok(isPresent(await p(alice.db.get)(msgID1)), 'msg1 exists')
assert.ok(isPresent(alice.db.get(msgID2)), 'msg2 exists') assert.ok(isPresent(await p(alice.db.get)(msgID2)), 'msg2 exists')
assert.ok(isPresent(alice.db.get(msgID3)), 'msg3 exists') assert.ok(isPresent(await p(alice.db.get)(msgID3)), 'msg3 exists')
assert.ok(isPresent(alice.db.get(msgID4)), 'msg4 exists') assert.ok(isPresent(await p(alice.db.get)(msgID4)), 'msg4 exists')
assert.ok(isPresent(alice.db.get(msgID5)), 'msg5 exists') assert.ok(isPresent(await p(alice.db.get)(msgID5)), 'msg5 exists')
assert.deepEqual( assert.deepEqual(
await p(alice.db.log.stats)(), await p(alice.db.log.stats)(),
@ -84,9 +88,13 @@ test('Dict ghosts', async (t) => {
alice.goals.set(dictID, 'dict') alice.goals.set(dictID, 'dict')
await p(alice.gc.forceImmediately)() await p(alice.gc.forceImmediately)()
const msgs2 = []
for await (const msg of alice.db.msgs()) {
msgs2.push(msg)
}
// Assert situation after GC // Assert situation after GC
assert.deepEqual( assert.deepEqual(
getFields([...alice.db.msgs()]), getFields(msgs2),
[25, 'ALICE'], [25, 'ALICE'],
'alice has only field root msgs' 'alice has only field root msgs'
) )
@ -97,12 +105,12 @@ test('Dict ghosts', async (t) => {
'log stats after' 'log stats after'
) )
assert.ok(isErased(alice.db.get(mootID)), 'moot by def erased') assert.ok(isErased(await p(alice.db.get)(mootID)), 'moot by def erased')
assert.ok(isDeleted(alice.db.get(msgID1)), 'msg1 deleted') assert.ok(isDeleted(await p(alice.db.get)(msgID1)), 'msg1 deleted')
assert.ok(isDeleted(alice.db.get(msgID2)), 'msg2 deleted') // ghost! assert.ok(isDeleted(await p(alice.db.get)(msgID2)), 'msg2 deleted') // ghost!
assert.ok(isErased(alice.db.get(msgID3)), 'msg3 erased') assert.ok(isErased(await p(alice.db.get)(msgID3)), 'msg3 erased')
assert.ok(isPresent(alice.db.get(msgID4)), 'msg4 exists') assert.ok(isPresent(await p(alice.db.get)(msgID4)), 'msg4 exists')
assert.ok(isPresent(alice.db.get(msgID5)), 'msg5 exists') assert.ok(isPresent(await p(alice.db.get)(msgID5)), 'msg5 exists')
assert.deepEqual(alice.db.ghosts.get(dictID), [msgID2]) assert.deepEqual(alice.db.ghosts.get(dictID), [msgID2])

View File

@ -26,8 +26,12 @@ test('Feed decay', async (t) => {
}) })
} }
let msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A0', 'A1', 'A2', 'A3', 'A4'], ['A0', 'A1', 'A2', 'A3', 'A4'],
'alice has the whole feed' 'alice has the whole feed'
) )
@ -39,8 +43,12 @@ test('Feed decay', async (t) => {
await p(alice.gc.forceImmediately)() await p(alice.gc.forceImmediately)()
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A2', 'A3', 'A4'], ['A2', 'A3', 'A4'],
'alice has only latest 3 msgs in the feed' 'alice has only latest 3 msgs in the feed'
) )

View File

@ -28,8 +28,12 @@ test('Feed holes', async (t) => {
posts.push(rec.id) posts.push(rec.id)
} }
let msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9'], ['A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9'],
'alice has the whole feed' 'alice has the whole feed'
) )
@ -40,8 +44,12 @@ test('Feed holes', async (t) => {
await p(alice.db.erase)(posts[6]) // vital as trail from A7 await p(alice.db.erase)(posts[6]) // vital as trail from A7
assert('alice deleted the middle part of the feed') assert('alice deleted the middle part of the feed')
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A0', 'A1', 'A2', /* */ 'A7', 'A8', 'A9'], ['A0', 'A1', 'A2', /* */ 'A7', 'A8', 'A9'],
'alice has the beginning and the end of the feed' 'alice has the beginning and the end of the feed'
) )
@ -63,8 +71,12 @@ test('Feed holes', async (t) => {
await p(alice.gc.forceImmediately)() await p(alice.gc.forceImmediately)()
assert.deepEqual(calledErase, [posts[2]], 'erased A2') assert.deepEqual(calledErase, [posts[2]], 'erased A2')
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
[/* */ 'A7', 'A8', 'A9'], [/* */ 'A7', 'A8', 'A9'],
'alice has only the end of the feed' 'alice has only the end of the feed'
) )

View File

@ -1,7 +1,7 @@
const test = require('node:test') const test = require('node:test')
const assert = require('node:assert') const assert = require('node:assert')
const p = require('node:util').promisify const p = require('node:util').promisify
const Keypair = require('ppppp-keypair') const Keypair = require('pzp-keypair')
const { createPeer } = require('./util') const { createPeer } = require('./util')
const bobKeypair = Keypair.generate('ed25519', 'bob') const bobKeypair = Keypair.generate('ed25519', 'bob')
@ -53,8 +53,12 @@ test('Orphan weave msgs', async (t) => {
tangles: [threadRoot.id], tangles: [threadRoot.id],
}) })
let msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['B0', 'A1', 'C1'], ['B0', 'A1', 'C1'],
'alice has the full thread' 'alice has the full thread'
) )
@ -62,8 +66,12 @@ test('Orphan weave msgs', async (t) => {
await p(alice.db.del)(threadRoot.id) await p(alice.db.del)(threadRoot.id)
assert('alice deleted the root') assert('alice deleted the root')
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A1', 'C1'], ['A1', 'C1'],
'alice has only thread replies' 'alice has only thread replies'
) )
@ -76,8 +84,12 @@ test('Orphan weave msgs', async (t) => {
await p(alice.gc.forceImmediately)() await p(alice.gc.forceImmediately)()
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A1'], ['A1'],
'alice does not have the thread, except her own reply' 'alice does not have the thread, except her own reply'
) )

View File

@ -25,8 +25,12 @@ test('Cleanup is scheduled automatically', async (t) => {
}) })
} }
let msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A0', 'A1', 'A2', 'A3', 'A4'], ['A0', 'A1', 'A2', 'A3', 'A4'],
'alice has the whole feed' 'alice has the whole feed'
) )
@ -39,8 +43,12 @@ test('Cleanup is scheduled automatically', async (t) => {
alice.gc.start(4 * 1024) // 4kB, approximately 8 messages alice.gc.start(4 * 1024) // 4kB, approximately 8 messages
await p(setTimeout)(3000) await p(setTimeout)(3000)
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A2', 'A3', 'A4'], ['A2', 'A3', 'A4'],
'alice has only latest 3 msgs in the feed' 'alice has only latest 3 msgs in the feed'
) )
@ -68,8 +76,12 @@ test('Compaction is scheduled automatically', async (t) => {
msgIDs.push(rec.id) msgIDs.push(rec.id)
} }
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A', 'B', 'C', 'D', 'E'], ['A', 'B', 'C', 'D', 'E'],
'alice has 5 messages' 'alice has 5 messages'
) )
@ -79,8 +91,12 @@ test('Compaction is scheduled automatically', async (t) => {
await p(alice.db.del)(msgIDs[3]) await p(alice.db.del)(msgIDs[3])
await p(alice.db.del)(msgIDs[4]) await p(alice.db.del)(msgIDs[4])
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['C'], ['C'],
'alice has 1 message before compaction' 'alice has 1 message before compaction'
) )
@ -95,8 +111,12 @@ test('Compaction is scheduled automatically', async (t) => {
alice.gc.start(6 * 1024) // 6kB, approximately 12 messages alice.gc.start(6 * 1024) // 6kB, approximately 12 messages
await p(setTimeout)(3000) await p(setTimeout)(3000)
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['C'], ['C'],
'alice has 1 message after compaction' 'alice has 1 message after compaction'
) )
@ -129,8 +149,12 @@ test('start() will automatically stop()', async (t) => {
}) })
} }
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A0', 'A1', 'A2', 'A3', 'A4'], ['A0', 'A1', 'A2', 'A3', 'A4'],
'alice has the whole feed' 'alice has the whole feed'
) )
@ -138,8 +162,12 @@ test('start() will automatically stop()', async (t) => {
alice.gc.start(4 * 1024) // 4kB, approximately 8 messages alice.gc.start(4 * 1024) // 4kB, approximately 8 messages
await p(setTimeout)(3000) await p(setTimeout)(3000)
msgs = []
for await (const msg of alice.db.msgs()) {
msgs.push(msg)
}
assert.deepEqual( assert.deepEqual(
getTexts([...alice.db.msgs()]), getTexts(msgs),
['A2', 'A3', 'A4'], ['A2', 'A3', 'A4'],
'alice has only latest 3 msgs in the feed' 'alice has only latest 3 msgs in the feed'
) )

View File

@ -1,15 +1,15 @@
const OS = require('node:os') const OS = require('node:os')
const Path = require('node:path') const Path = require('node:path')
const rimraf = require('rimraf') const rimraf = require('rimraf')
const caps = require('ppppp-caps') const caps = require('pzp-caps')
const Keypair = require('ppppp-keypair') const Keypair = require('pzp-keypair')
function createPeer(config) { function createPeer(config) {
if (config.name) { if (config.name) {
const name = config.name const name = config.name
const tmp = OS.tmpdir() const tmp = OS.tmpdir()
config.global ??= {} config.global ??= {}
config.global.path ??= Path.join(tmp, `ppppp-gc-${name}-${Date.now()}`) config.global.path ??= Path.join(tmp, `pzp-gc-${name}-${Date.now()}`)
config.global.keypair ??= Keypair.generate('ed25519', name) config.global.keypair ??= Keypair.generate('ed25519', name)
delete config.name delete config.name
} }
@ -27,10 +27,10 @@ function createPeer(config) {
return require('secret-stack/bare')() return require('secret-stack/bare')()
.use(require('secret-stack/plugins/net')) .use(require('secret-stack/plugins/net'))
.use(require('secret-handshake-ext/secret-stack')) .use(require('secret-handshake-ext/secret-stack'))
.use(require('ppppp-db')) .use(require('pzp-db'))
.use(require('ppppp-dict')) .use(require('pzp-dict'))
.use(require('ppppp-set')) .use(require('pzp-set'))
.use(require('ppppp-goals')) .use(require('pzp-goals'))
.use(require('ssb-box')) .use(require('ssb-box'))
.use(require('../lib')) .use(require('../lib'))
.call(null, { .call(null, {