From 4d0b3ef8195e76d8c906f42b2065ad136409cb76 Mon Sep 17 00:00:00 2001 From: Nicolas Otten Date: Thu, 6 Aug 2026 13:34:36 +0100 Subject: [PATCH 1/4] IBLCATALOG-616: quick draft for a new refresh method --- lib/ceych.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/ceych.js b/lib/ceych.js index 53977da..72252e6 100644 --- a/lib/ceych.js +++ b/lib/ceych.js @@ -120,6 +120,24 @@ class Ceych { return this.cache.drop(cacheKey); } + /** + * Manually refresh the cache entry for the given function and args combination. The function passed should be the unwrapped, initial function. + * @param {*} func + * @param {*} opts + * @param {...any} args + * @returns + */ + async refresh(func, opts, ...args) { + const cacheKey = createCacheKey(opts.func, args, opts.suffix); + const newValue = await func(...args); + + if (this.stats) { + this.stats.increment('ceych.refresh'); + } + + return this.cache.set(cacheKey, newValue, opts.ttl ?? this.defaultTTL * 1000); + } + /** * Disables the use of the cache. This can be useful if you want to toggle usage of the cache for operational purposes - e.g. for operational purposes, or unit tests. */ From eccffcf34cf181fd54b2261434d1d5c33de812b9 Mon Sep 17 00:00:00 2001 From: BenSymons Date: Mon, 17 Aug 2026 14:06:03 +0100 Subject: [PATCH 2/4] feat: add set function to manually update cache entries Co-authored-by: Nicolas Otten --- README.md | 12 +++ lib/ceych.js | 33 +++--- package.json | 5 +- test/lib/ceych.js | 264 ++++++++++++++++++++++------------------------ 4 files changed, 163 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index e295cf6..c91bced 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,18 @@ Returns a wrapped function that implements caching. Invalidates the current cache entry for the given function and args combination. The function passed should be the unwrapped, initial function. +#### `ceych.set() + +Use this to manually sets the cache entry for the given function and args combination. You can use this to overrwrite an existing cache entry to a newer one. + +The new cache key will have a TTL set randomly between this.defaultTtl/2 and this.defaultTtl. This is to ensure that when manually setting a lot of cache keys at the same time, they don't end up all expiring at the same time and causing lots of caches misses. + +##### Parameters + +* `funcOrOpts` - Either a function or a set of options of the format `{ func: yourFunction, suffix: 'yourSuffix' }` if you wish to include a suffix. +* `...args` - The args that you passed to the wrapped function call which initially stored the cache entry. +* `updatedValue` - The new value to store in the cache. + ##### Parameters * `funcOrOpts` - Either a function or a set of options of the format `{ func: yourFunction, suffix: 'yourSuffix' }` if you wish to include a suffix. diff --git a/lib/ceych.js b/lib/ceych.js index 72252e6..b0c60b5 100644 --- a/lib/ceych.js +++ b/lib/ceych.js @@ -32,7 +32,7 @@ function validateClientOpts(opts) { return opts; } -function validateInvalidateOpts(opts) { +function getOptions(opts) { if (!opts) { throw new Error('Incorrect invalidate opts received, you must pass a function or options object to invalidate.'); } @@ -51,7 +51,7 @@ function validateInvalidateOpts(opts) { if (opts.suffix && typeof opts.suffix !== 'string') { throw new Error('Incorrect invalidate opts received, opts.suffix must be a string.'); } - + if (!opts.suffix) opts.suffix = ''; return opts; @@ -110,32 +110,39 @@ class Ceych { * @param {...any} args The args that you passed to the wrapped function call which initially stored the cache entry. */ invalidate(funcOrOpts, ...args) { - const opts = validateInvalidateOpts(funcOrOpts); + const opts = getOptions(funcOrOpts); const cacheKey = createCacheKey(opts.func, args, opts.suffix); if (this.stats) { this.stats.increment('ceych.invalidate'); } + return this.cache.drop(cacheKey); } /** - * Manually refresh the cache entry for the given function and args combination. The function passed should be the unwrapped, initial function. - * @param {*} func - * @param {*} opts - * @param {...any} args - * @returns + * Manually sets the cache entry for the given function and args combination. + * This is an advanced escape hatch for cases where you know a cached value has changed + * and would prefer to manually store the new value instead of simply calling invalidate(). + * + * The new cache key will have a TTL set randomly between this.defaultTtl/2 and this.defaultTtl. + * This is to ensure that when manually setting a lot of cache keys at the same time, they don't end + * up all expiring at the same time and causing lots of caches misses. + * @param {function | {func: function, suffix: string}} funcOrOpts Either a function or options including `func` and optional `suffix`. + * @param {...any} functionArgs The args that identify the target cache key. + * @param {*} updatedValue The value to store in cache. */ - async refresh(func, opts, ...args) { - const cacheKey = createCacheKey(opts.func, args, opts.suffix); - const newValue = await func(...args); + set(funcOrOpts, functionArgs, updatedValue) { + const opts = getOptions(funcOrOpts); + const cacheKey = createCacheKey(opts.func, functionArgs, opts.suffix); + const randomTtl = Math.floor(Math.random() * this.defaultTTL / 2); if (this.stats) { - this.stats.increment('ceych.refresh'); + this.stats.increment('ceych.set'); } - return this.cache.set(cacheKey, newValue, opts.ttl ?? this.defaultTTL * 1000); + return this.cache.set(cacheKey, updatedValue, this.defaultTTL - randomTtl); } /** diff --git a/package.json b/package.json index d5a7d54..da69a05 100644 --- a/package.json +++ b/package.json @@ -41,5 +41,6 @@ "mocha": "^10.2.0", "nyc": "^17.1.0", "sinon": "^15.0.3" - } -} \ No newline at end of file + }, + "packageManager": "pnpm@11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485" +} diff --git a/test/lib/ceych.js b/test/lib/ceych.js index a72e311..82e6472 100644 --- a/test/lib/ceych.js +++ b/test/lib/ceych.js @@ -13,11 +13,15 @@ const sandbox = sinon.createSandbox(); describe('ceych', () => { let ceych; - const wrappable = sandbox.stub().returns(Promise.resolve(1)); - const cacheClient = new Catbox(new CatboxMemory.Engine()); - const cacheClientStub = sandbox.stub(cacheClient); + let cacheClientStub; + let cacheClient; + let wrappable; beforeEach(() => { + wrappable = sandbox.stub().returns(Promise.resolve(1)); + cacheClient = new Catbox(new CatboxMemory.Engine()); + cacheClientStub = sandbox.stub(cacheClient); + cacheClient.isReady.returns(true); ceych = new Ceych({ cacheClient: cacheClient }); @@ -87,30 +91,18 @@ describe('ceych', () => { }, Error, 'Can only wrap a function, received [1]'); }); - it('sets the TTL if the second argument is an integer', () => { - sandbox.stub(cacheClient, 'set').returns(Promise.resolve()); - sandbox.stub(cacheClient, 'isReady').returns(true); - + it('sets the TTL if the second argument is an integer', async () => { const func = ceych.wrap(wrappable, 5); - - return func() - .catch(assert.ifError) - .then(() => { - sinon.assert.calledWith(cacheClient.set, sinon.match.any, sinon.match.any, 5000); - }); + await func(); + sinon.assert.calledWith(cacheClient.set, sinon.match.any, sinon.match.any, 5000); }); - it('sets the suffix if the third argument is a string', () => { - sandbox.stub(cacheClient, 'set').returns(Promise.resolve()); + it('sets the suffix if the third argument is a string', async () => { const func = ceych.wrap(wrappable, 5, 'suffix'); - - return func() - .catch(assert.ifError) - .then(() => { - sinon.assert.calledWith(cacheClient.set, sinon.match({ - id: 'hashed' - })); - }); + await func(); + sinon.assert.calledWith(cacheClient.set, sinon.match({ + id: 'hashed' + })); }); it('throws if incorrect type supplied as ttl', async () => { @@ -149,7 +141,7 @@ describe('ceych', () => { const func = ceychWithStats.wrap(wrappable); try { await func(); - } catch(err) { + } catch (err) { sinon.assert.calledWith(statsClient.increment, 'ceych.misses'); } }); @@ -158,101 +150,58 @@ describe('ceych', () => { describe('.invalidate', () => { it('invalidates the cache entry', async () => { - const getStub = sandbox.stub().onFirstCall().returns(null) + cacheClient.get.onFirstCall().returns(null) .onSecondCall().returns({ item: 1 }) .onThirdCall().returns(null); - const dropStub = sandbox.stub().resolves(); - const cacheClient = { - get: getStub, - set: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(true), - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - drop: dropStub - }; - const ceych = new Ceych({ - cacheClient - }); - - const wrappable = sandbox.stub().returns(Promise.resolve(1)); const cacheKey = createCacheKey(wrappable, [], ''); const func = ceych.wrap(wrappable); await func(); await func(); sinon.assert.calledOnce(wrappable); - sinon.assert.calledTwice(getStub); - sinon.assert.alwaysCalledWith(getStub, cacheKey); + sinon.assert.calledTwice(cacheClient.get); + sinon.assert.alwaysCalledWith(cacheClient.get, cacheKey); ceych.invalidate(wrappable); sinon.assert.calledOnce(cacheClient.drop); await func(); - sinon.assert.calledThrice(getStub); - sinon.assert.alwaysCalledWith(getStub, cacheKey); + sinon.assert.calledThrice(cacheClient.get); + sinon.assert.alwaysCalledWith(cacheClient.get, cacheKey); sinon.assert.calledTwice(wrappable); }); it('supports a custom ttl and suffix', async () => { - const getStub = sandbox.stub() - .onFirstCall().returns(null) - .onSecondCall().returns({ item: 1 }) - .onThirdCall().returns(null); - const dropStub = sandbox.stub().resolves(); - - const cacheClient = { - get: getStub, - set: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(true), - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - drop: dropStub - }; - - const ceych = new Ceych({ - cacheClient - }); + cacheClient.get + .onFirstCall().returns(null) + .onSecondCall().returns({ item: 1 }) + .onThirdCall().returns(null); const suffix = 'saywat'; - const wrappable = sandbox.stub().returns(Promise.resolve(1)); const cacheKey = createCacheKey(wrappable, [], suffix); const func = ceych.wrap(wrappable, 20, suffix); await func(); await func(); sinon.assert.calledOnce(wrappable); - sinon.assert.calledTwice(getStub); - sinon.assert.alwaysCalledWith(getStub, cacheKey); + sinon.assert.calledTwice(cacheClient.get); + sinon.assert.alwaysCalledWith(cacheClient.get, cacheKey); await ceych.invalidate({ func: wrappable, suffix }); - sinon.assert.calledWith(dropStub, cacheKey); + sinon.assert.calledWith(cacheClient.drop, cacheKey); await func(); sinon.assert.calledTwice(wrappable); }); it('does not affect other cache keys of the same function', async () => { - const getStub = sandbox.stub().onFirstCall().returns(null) + cacheClient.get.onFirstCall().returns(null) .onSecondCall().returns(null) .onThirdCall().returns(null) .onCall(3).returns({ item: 1 }); - const dropStub = sandbox.stub().resolves(); - const cacheClient = { - get: getStub, - set: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(true), - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - drop: dropStub - }; - - const ceych = new Ceych({ - cacheClient - }); - const wrappable = sandbox.stub().returns(Promise.resolve(1)); const helloCacheKey = createCacheKey(wrappable, ['hello'], ''); const bonjourCacheKey = createCacheKey(wrappable, ['bonjour'], ''); const func = ceych.wrap(wrappable); @@ -260,12 +209,12 @@ describe('ceych', () => { await func('hello'); await func('bonjour'); sinon.assert.calledTwice(wrappable); - sinon.assert.calledWith(getStub, helloCacheKey); - sinon.assert.calledWith(getStub, bonjourCacheKey); + sinon.assert.calledWith(cacheClient.get, helloCacheKey); + sinon.assert.calledWith(cacheClient.get, bonjourCacheKey); await ceych.invalidate(wrappable, 'hello'); - sinon.assert.calledWith(dropStub, helloCacheKey); - sinon.assert.neverCalledWith(dropStub, bonjourCacheKey); + sinon.assert.calledWith(cacheClient.drop, helloCacheKey); + sinon.assert.neverCalledWith(cacheClient.drop, bonjourCacheKey); await func('hello'); await func('bonjour'); @@ -276,25 +225,11 @@ describe('ceych', () => { }); it('does not affect other cache keys of the same function, multi-argument', async () => { - const getStub = sandbox.stub().onFirstCall().returns(null) + cacheClient.get.onFirstCall().returns(null) .onSecondCall().returns(null) .onThirdCall().returns(null) .onCall(3).returns({ item: 1 }); - const dropStub = sandbox.stub().resolves(); - const cacheClient = { - get: getStub, - set: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(true), - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - drop: dropStub - }; - - const ceych = new Ceych({ - cacheClient - }); - const wrappable = sandbox.stub().returns(Promise.resolve(1)); const helloCacheKey = createCacheKey(wrappable, ['hello'], ''); const helloBonjourCacheKey = createCacheKey(wrappable, ['hello', 'bonjour'], ''); const func = ceych.wrap(wrappable); @@ -302,32 +237,25 @@ describe('ceych', () => { await func('hello'); await func('hello', 'bonjour'); sinon.assert.calledTwice(wrappable); - sinon.assert.calledWith(getStub, helloCacheKey); - sinon.assert.calledWith(getStub, helloBonjourCacheKey); + sinon.assert.calledWith(cacheClient.get, helloCacheKey); + sinon.assert.calledWith(cacheClient.get, helloBonjourCacheKey); await ceych.invalidate(wrappable, 'hello'); - sinon.assert.calledWith(dropStub, helloCacheKey); - sinon.assert.neverCalledWith(dropStub, helloBonjourCacheKey); + sinon.assert.calledWith(cacheClient.drop, helloCacheKey); + sinon.assert.neverCalledWith(cacheClient.drop, helloBonjourCacheKey); await func('hello'); await func('hello', 'bonjour'); const calls = wrappable.getCalls(); - assert.equal(2, calls.filter((c) => c.args[0] === 'hello' && c.args.length === 1).length); - assert.equal(1, calls.filter((c) => c.args.join(',') === 'hello,bonjour').length); + assert.equal(calls.filter((c) => c.args[0] === 'hello' && c.args.length === 1).length, 2); + assert.equal(calls.filter((c) => c.args.join(',') === 'hello,bonjour').length, 1); }); it('increments a metric for invalidation', async () => { - const cacheClient = { - get: sandbox.stub().onFirstCall().returns(null) - .onSecondCall().returns({ item: 1 }) - .onThirdCall().returns(null), - set: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(true), - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - drop: sandbox.stub().resolves() - }; + cacheClient.get.onFirstCall().returns(null) + .onSecondCall().returns({ item: 1 }) + .onThirdCall().returns(null); const statsClient = { increment: sandbox.stub(), @@ -339,7 +267,6 @@ describe('ceych', () => { statsClient }); - const wrappable = sandbox.stub().returns(Promise.resolve(1)); const func = ceych.wrap(wrappable); await func(); @@ -357,6 +284,90 @@ describe('ceych', () => { }); }); + describe('.set', () => { + it('updates the value of an existing key in the cache', async () => { + cacheClient.get.onFirstCall().returns(null); + cacheClient.get.onSecondCall().returns(100); + const cacheKey = createCacheKey(wrappable, [], ''); + const wrapped = ceych.wrap(wrappable); + + // First call: result is stored in cache + await wrapped(); + + // Manually set a new value in the cache + ceych.set(wrappable, [], 100); + + await wrapped(); + + sinon.assert.calledTwice(cacheClient.set); + const setArgs = cacheClient.set.getCall(1).args; + assert.strictEqual(setArgs[0].id, cacheKey.id); + assert.strictEqual(setArgs[1], 100); + + sinon.assert.calledOnce(wrappable); + }); + + it('sets the TTL to a random value between defaultTTL and defaultTTL / 2', async () => { + const cacheKey = createCacheKey(wrappable, [], ''); + const wrapped = ceych.wrap(wrappable); + sinon.stub(Math, 'random').returns(1); + + ceych.set(wrappable, [], 100); + + sinon.assert.calledOnce(cacheClient.set); + const setArgs = cacheClient.set.getCall(0).args; + assert.strictEqual(setArgs[0].id, cacheKey.id); + assert.strictEqual(setArgs[1], 100); + assert.strictEqual(setArgs[2], 15); + }); + + it('should support a suffix', async () => { + const cacheKey = createCacheKey(wrappable, [], 'suffix'); + const wrapped = ceych.wrap(wrappable); + + ceych.set({ func: wrappable, suffix: 'suffix' }, [], 10); + + const setArgs = cacheClient.set.getCall(0).args; + assert.strictEqual(setArgs[0].id, cacheKey.id); + assert.strictEqual(setArgs[1], 10); + }); + + it('does not affect other cache keys of the same function', async () => { + const frenchCacheKey = createCacheKey(wrappable, ['bonjour'], ''); + const englishCacheKey = createCacheKey(wrappable, ['hello'], ''); + const wrapped = ceych.wrap(wrappable); + + await wrapped('hello'); + await wrapped('bonjour'); + + sinon.assert.calledTwice(cacheClient.set); + + ceych.set(wrappable, ['hello'], 10); + + sinon.assert.calledThrice(cacheClient.set); + + const setArgs = cacheClient.set.getCall(2).args; + assert.strictEqual(setArgs[0].id, englishCacheKey.id); + }); + + it(`should increment a 'set' metric if a stats client has been passed in`, async () => { + const statsClient = { + increment: sandbox.stub(), + timing: sandbox.stub(), + }; + + const ceych = new Ceych({ + cacheClient, + statsClient + }); + + const func = ceych.wrap(wrappable); + + ceych.set(wrappable, [], 10); + sinon.assert.calledWithExactly(statsClient.increment, 'ceych.set'); + }); + }); + describe('.disableCache', () => { beforeEach(() => { sandbox.stub(hash, 'create').returns('hashed'); @@ -383,32 +394,13 @@ describe('ceych', () => { }); it('starts the cache client if it is stopped', async () => { - const cacheClient = { - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(false) - }; - - const ceych = new Ceych({ - cacheClient: cacheClient - }); - + cacheClient.isReady.returns(false); await ceych.enableCache(); sinon.assert.called(cacheClient.start); }); it('does nothing if the cache client was already started', async () => { - const cacheClient = { - start: sandbox.stub().resolves(), - stop: sandbox.stub().resolves(), - isReady: sandbox.stub().returns(true) - }; - - const ceych = new Ceych({ - cacheClient: cacheClient - }); cacheClient.start.resetHistory(); // start is called in the constructor, so reset its history - await ceych.enableCache(); sinon.assert.notCalled(cacheClient.start); }); From 99e44661cd768bf0092585288dfe6b5ea3d7a254 Mon Sep 17 00:00:00 2001 From: BenSymons Date: Mon, 17 Aug 2026 14:27:39 +0100 Subject: [PATCH 3/4] feat: copilot changes --- README.md | 4 ++-- lib/ceych.js | 12 ++++++------ test/lib/ceych.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c91bced..893c7f2 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Returns a wrapped function that implements caching. Invalidates the current cache entry for the given function and args combination. The function passed should be the unwrapped, initial function. -#### `ceych.set() +#### `ceych.set()` Use this to manually sets the cache entry for the given function and args combination. You can use this to overrwrite an existing cache entry to a newer one. @@ -86,7 +86,7 @@ The new cache key will have a TTL set randomly between this.defaultTtl/2 and thi ##### Parameters * `funcOrOpts` - Either a function or a set of options of the format `{ func: yourFunction, suffix: 'yourSuffix' }` if you wish to include a suffix. -* `...args` - The args that you passed to the wrapped function call which initially stored the cache entry. +* `args` - An array of args that you passed to the wrapped function call which initially stored the cache entry. * `updatedValue` - The new value to store in the cache. ##### Parameters diff --git a/lib/ceych.js b/lib/ceych.js index b0c60b5..6f6e644 100644 --- a/lib/ceych.js +++ b/lib/ceych.js @@ -34,7 +34,7 @@ function validateClientOpts(opts) { function getOptions(opts) { if (!opts) { - throw new Error('Incorrect invalidate opts received, you must pass a function or options object to invalidate.'); + throw new Error('Incorrect opts received, you must pass a function or options object to invalidate.'); } if (typeof opts === 'function') { @@ -45,11 +45,11 @@ function getOptions(opts) { } if (!opts.func || typeof opts.func !== 'function') { - throw new Error('Incorrect invalidate opts received, opts.func must be a function.'); + throw new Error('Incorrect opts received, opts.func must be a function.'); } if (opts.suffix && typeof opts.suffix !== 'string') { - throw new Error('Incorrect invalidate opts received, opts.suffix must be a string.'); + throw new Error('Incorrect opts received, opts.suffix must be a string.'); } if (!opts.suffix) opts.suffix = ''; @@ -126,11 +126,11 @@ class Ceych { * This is an advanced escape hatch for cases where you know a cached value has changed * and would prefer to manually store the new value instead of simply calling invalidate(). * - * The new cache key will have a TTL set randomly between this.defaultTtl/2 and this.defaultTtl. + * The new cache key will have a TTL set randomly between this.defaultTTL/2 and this.defaultTTL. * This is to ensure that when manually setting a lot of cache keys at the same time, they don't end * up all expiring at the same time and causing lots of caches misses. * @param {function | {func: function, suffix: string}} funcOrOpts Either a function or options including `func` and optional `suffix`. - * @param {...any} functionArgs The args that identify the target cache key. + * @param {args[]} functionArgs The args that identify the target cache key. * @param {*} updatedValue The value to store in cache. */ set(funcOrOpts, functionArgs, updatedValue) { @@ -142,7 +142,7 @@ class Ceych { this.stats.increment('ceych.set'); } - return this.cache.set(cacheKey, updatedValue, this.defaultTTL - randomTtl); + return this.cache.set(cacheKey, updatedValue, (this.defaultTTL - randomTtl) * 1000); } /** diff --git a/test/lib/ceych.js b/test/lib/ceych.js index 82e6472..d0e7b48 100644 --- a/test/lib/ceych.js +++ b/test/lib/ceych.js @@ -318,7 +318,7 @@ describe('ceych', () => { const setArgs = cacheClient.set.getCall(0).args; assert.strictEqual(setArgs[0].id, cacheKey.id); assert.strictEqual(setArgs[1], 100); - assert.strictEqual(setArgs[2], 15); + assert.strictEqual(setArgs[2], 15000); }); it('should support a suffix', async () => { From f0344c3f65ebe52ca3725a701fff4b6037ed1cd1 Mon Sep 17 00:00:00 2001 From: BenSymons Date: Mon, 17 Aug 2026 15:08:14 +0100 Subject: [PATCH 4/4] feat: return catbox value in get mock Co-authored-by: Nicolas Otten --- test/lib/ceych.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/ceych.js b/test/lib/ceych.js index d0e7b48..a9bf3ee 100644 --- a/test/lib/ceych.js +++ b/test/lib/ceych.js @@ -287,7 +287,7 @@ describe('ceych', () => { describe('.set', () => { it('updates the value of an existing key in the cache', async () => { cacheClient.get.onFirstCall().returns(null); - cacheClient.get.onSecondCall().returns(100); + cacheClient.get.onSecondCall().returns({ item: 100 }); const cacheKey = createCacheKey(wrappable, [], ''); const wrapped = ceych.wrap(wrappable);