@objectstack/service-cluster 5.1.2 → 5.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/service-cluster@5.1.2 build /home/runner/work/framework/framework/packages/services/service-cluster
2
+ > @objectstack/service-cluster@5.1.5 build /home/runner/work/framework/framework/packages/services/service-cluster
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.ts, src/testing.ts
@@ -14,14 +14,14 @@
14
14
  ESM dist/testing.js 8.60 KB
15
15
  ESM dist/index.js.map 33.85 KB
16
16
  ESM dist/testing.js.map 18.62 KB
17
- ESM ⚡️ Build success in 969ms
17
+ ESM ⚡️ Build success in 920ms
18
18
  CJS dist/index.cjs 13.98 KB
19
- CJS dist/testing.cjs 11.59 KB
19
+ CJS dist/testing.cjs 11.60 KB
20
20
  CJS dist/index.cjs.map 37.23 KB
21
21
  CJS dist/testing.cjs.map 14.18 KB
22
- CJS ⚡️ Build success in 985ms
22
+ CJS ⚡️ Build success in 932ms
23
23
  DTS Build start
24
- DTS ⚡️ Build success in 15661ms
24
+ DTS ⚡️ Build success in 18267ms
25
25
  DTS dist/index.d.ts 8.05 KB
26
26
  DTS dist/testing.d.ts 794.00 B
27
27
  DTS dist/index.d.cts 8.05 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @objectstack/service-cluster
2
2
 
3
+ ## 5.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [b4c74a9]
8
+ - @objectstack/spec@6.2.0
9
+ - @objectstack/core@6.2.0
10
+
11
+ ## 5.1.4
12
+
13
+ ### Patch Changes
14
+
15
+ - @objectstack/spec@6.1.1
16
+ - @objectstack/core@6.1.1
17
+
18
+ ## 5.1.3
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [93c0589]
23
+ - @objectstack/spec@6.1.0
24
+ - @objectstack/core@6.1.0
25
+
3
26
  ## 5.1.2
4
27
 
5
28
  ### Patch Changes
package/dist/testing.cjs CHANGED
@@ -97,9 +97,9 @@ function runLockContract(name, make) {
97
97
  });
98
98
  _vitest.it.call(void 0, "TTL auto-releases a stuck holder", async () => {
99
99
  const l = await make();
100
- const h1 = await l.acquire("k", { ttlMs: 30 });
100
+ const h1 = await l.acquire("k", { ttlMs: 50 });
101
101
  _vitest.expect.call(void 0, h1).not.toBeNull();
102
- await new Promise((r) => setTimeout(r, 60));
102
+ await new Promise((r) => setTimeout(r, 150));
103
103
  _vitest.expect.call(void 0, h1.isHeld()).toBe(false);
104
104
  const h2 = await l.acquire("k");
105
105
  _vitest.expect.call(void 0, h2).not.toBeNull();
@@ -118,18 +118,18 @@ function runLockContract(name, make) {
118
118
  });
119
119
  _vitest.it.call(void 0, "renew extends the lease", async () => {
120
120
  const l = await make();
121
- const h = await l.acquire("k", { ttlMs: 40 });
122
- await new Promise((r) => setTimeout(r, 20));
123
- await h.renew(100);
124
- await new Promise((r) => setTimeout(r, 30));
121
+ const h = await l.acquire("k", { ttlMs: 200 });
122
+ await new Promise((r) => setTimeout(r, 50));
123
+ await h.renew(400);
124
+ await new Promise((r) => setTimeout(r, 250));
125
125
  _vitest.expect.call(void 0, h.isHeld()).toBe(true);
126
126
  await h.release();
127
127
  await l.close();
128
128
  });
129
129
  _vitest.it.call(void 0, "renew on a lost lock throws", async () => {
130
130
  const l = await make();
131
- const h = await l.acquire("k", { ttlMs: 20 });
132
- await new Promise((r) => setTimeout(r, 50));
131
+ const h = await l.acquire("k", { ttlMs: 50 });
132
+ await new Promise((r) => setTimeout(r, 150));
133
133
  await _vitest.expect.call(void 0, h.renew()).rejects.toThrow();
134
134
  await l.close();
135
135
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/framework/framework/packages/services/service-cluster/dist/testing.cjs","../src/testing.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/services/service-cluster/dist/testing.cjs","sourcesContent":[null,"// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Generic contract tests for cluster primitives.\n *\n * These are written once and run against any driver. The memory driver\n * suite calls them directly; future postgres/redis driver packages will\n * `import { runPubSubContract } from '@objectstack/service-cluster/testing'`\n * to get the same coverage for free.\n */\n\nimport { describe, expect, it, vi } from 'vitest';\nimport type {\n IPubSub,\n ILock,\n IKV,\n ICounter,\n} from '@objectstack/spec/contracts';\n\nexport interface ContractFactories {\n makePubSub: () => Promise<IPubSub>;\n makeLock: () => Promise<ILock>;\n makeKV: () => Promise<IKV>;\n makeCounter: () => Promise<ICounter>;\n}\n\nexport function runPubSubContract(name: string, make: () => Promise<IPubSub>) {\n describe(`IPubSub contract — ${name}`, () => {\n it('delivers published messages to subscribers', async () => {\n const bus = await make();\n const received: unknown[] = [];\n bus.subscribe<{ n: number }>('ch', (msg) => {\n received.push(msg.payload);\n });\n await bus.publish('ch', { n: 1 });\n await bus.publish('ch', { n: 2 });\n expect(received).toEqual([{ n: 1 }, { n: 2 }]);\n await bus.close();\n });\n\n it('does not deliver to other channels', async () => {\n const bus = await make();\n const h = vi.fn();\n bus.subscribe('a', h);\n await bus.publish('b', { x: 1 });\n expect(h).not.toHaveBeenCalled();\n await bus.close();\n });\n\n it('supports multiple subscribers per channel', async () => {\n const bus = await make();\n const a = vi.fn();\n const b = vi.fn();\n bus.subscribe('ch', a);\n bus.subscribe('ch', b);\n await bus.publish('ch', 'hi');\n expect(a).toHaveBeenCalledTimes(1);\n expect(b).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('unsubscribe stops delivery and is idempotent', async () => {\n const bus = await make();\n const h = vi.fn();\n const off = bus.subscribe('ch', h);\n await bus.publish('ch', 1);\n off();\n off(); // idempotent\n await bus.publish('ch', 2);\n expect(h).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('isolates handler errors from siblings', async () => {\n const errors: unknown[] = [];\n const bus = await make();\n // Memory driver allows override via constructor; for the\n // generic contract we just verify no throw leaks to publish().\n bus.subscribe('ch', () => {\n throw new Error('boom');\n });\n const ok = vi.fn();\n bus.subscribe('ch', ok);\n await expect(bus.publish('ch', 1)).resolves.toBeUndefined();\n expect(ok).toHaveBeenCalledTimes(1);\n await bus.close();\n void errors;\n });\n\n it('close rejects further publishes', async () => {\n const bus = await make();\n await bus.close();\n await expect(bus.publish('ch', 1)).rejects.toThrow(/closed/);\n });\n });\n}\n\nexport function runLockContract(name: string, make: () => Promise<ILock>) {\n describe(`ILock contract — ${name}`, () => {\n it('acquires and releases a free lock', async () => {\n const l = await make();\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n expect(h!.isHeld()).toBe(false);\n await l.close();\n });\n\n it('fails fast when waitMs=0 and lock is held', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n expect(h1).not.toBeNull();\n const h2 = await l.acquire('k');\n expect(h2).toBeNull();\n await h1!.release();\n await l.close();\n });\n\n it('hands off lock to a waiter on release', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const waiterP = l.acquire('k', { waitMs: 1000 });\n await new Promise((r) => setTimeout(r, 10));\n await h1!.release();\n const h2 = await waiterP;\n expect(h2).not.toBeNull();\n expect(h2!.isHeld()).toBe(true);\n await h2!.release();\n await l.close();\n });\n\n it('TTL auto-releases a stuck holder', async () => {\n const l = await make();\n const h1 = await l.acquire('k', { ttlMs: 30 });\n expect(h1).not.toBeNull();\n await new Promise((r) => setTimeout(r, 60));\n expect(h1!.isHeld()).toBe(false);\n const h2 = await l.acquire('k');\n expect(h2).not.toBeNull();\n await h2!.release();\n await l.close();\n });\n\n it('fencing tokens are monotonically increasing per key', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const t1 = h1!.fencingToken;\n await h1!.release();\n const h2 = await l.acquire('k');\n expect(h2!.fencingToken).toBeGreaterThan(t1);\n await h2!.release();\n await l.close();\n });\n\n it('renew extends the lease', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 40 });\n await new Promise((r) => setTimeout(r, 20));\n await h!.renew(100);\n await new Promise((r) => setTimeout(r, 30));\n // total 50ms in, original TTL would have expired at 40ms.\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n await l.close();\n });\n\n it('renew on a lost lock throws', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 20 });\n await new Promise((r) => setTimeout(r, 50));\n await expect(h!.renew()).rejects.toThrow();\n await l.close();\n });\n\n it('withLock returns fn result and auto-releases', async () => {\n const l = await make();\n const result = await l.withLock('k', async () => 42);\n expect(result).toBe(42);\n // Lock should be free immediately.\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n await h!.release();\n await l.close();\n });\n\n it('withLock returns null without calling fn on timeout', async () => {\n const l = await make();\n const h = await l.acquire('k');\n const fn = vi.fn();\n const result = await l.withLock('k', fn, { waitMs: 0 });\n expect(result).toBeNull();\n expect(fn).not.toHaveBeenCalled();\n await h!.release();\n await l.close();\n });\n\n it('release is idempotent', async () => {\n const l = await make();\n const h = await l.acquire('k');\n await h!.release();\n await expect(h!.release()).resolves.toBeUndefined();\n await l.close();\n });\n });\n}\n\nexport function runKVContract(name: string, make: () => Promise<IKV>) {\n describe(`IKV contract — ${name}`, () => {\n it('set then get round-trips and increments version', async () => {\n const kv = await make();\n const a = await kv.set('k', { v: 1 });\n expect(a.version).toBe(1n);\n const got = await kv.get<{ v: number }>('k');\n expect(got?.value).toEqual({ v: 1 });\n const b = await kv.set('k', { v: 2 });\n expect(b.version).toBe(2n);\n await kv.close();\n });\n\n it('get on missing key returns undefined', async () => {\n const kv = await make();\n expect(await kv.get('absent')).toBeUndefined();\n await kv.close();\n });\n\n it('delete removes the key', async () => {\n const kv = await make();\n await kv.set('k', 1);\n expect(await kv.delete('k')).toBe(true);\n expect(await kv.get('k')).toBeUndefined();\n expect(await kv.delete('k')).toBe(false);\n await kv.close();\n });\n\n it('ifVersion=0n requires absent key', async () => {\n const kv = await make();\n await kv.set('k', 'first', { ifVersion: 0n });\n await expect(kv.set('k', 'dup', { ifVersion: 0n })).rejects.toThrow(\n /version mismatch/i,\n );\n await kv.close();\n });\n\n it('cas succeeds on match, fails on mismatch', async () => {\n const kv = await make();\n const e1 = await kv.set('k', 1);\n const e2 = await kv.cas('k', e1.version, 2);\n expect(e2?.value).toBe(2);\n const failed = await kv.cas('k', e1.version, 3);\n expect(failed).toBeUndefined();\n await kv.close();\n });\n\n it('TTL expires entries', async () => {\n const kv = await make();\n await kv.set('k', 'v', { ttl: 0.03 }); // 30ms\n expect((await kv.get('k'))?.value).toBe('v');\n await new Promise((r) => setTimeout(r, 60));\n expect(await kv.get('k')).toBeUndefined();\n await kv.close();\n });\n });\n}\n\nexport function runCounterContract(name: string, make: () => Promise<ICounter>) {\n describe(`ICounter contract — ${name}`, () => {\n it('starts at 0, incr returns new value', async () => {\n const c = await make();\n expect(await c.peek('k')).toBe(0n);\n expect(await c.incr('k')).toBe(1n);\n expect(await c.incr('k')).toBe(2n);\n expect(await c.peek('k')).toBe(2n);\n await c.close();\n });\n\n it('incr by custom delta', async () => {\n const c = await make();\n expect(await c.incr('k', { by: 5 })).toBe(5n);\n expect(await c.incr('k', { by: -2 })).toBe(3n);\n await c.close();\n });\n\n it('reset', async () => {\n const c = await make();\n await c.incr('k', { by: 10 });\n await c.reset('k', 100n);\n expect(await c.peek('k')).toBe(100n);\n await c.reset('k');\n expect(await c.peek('k')).toBe(0n);\n await c.close();\n });\n\n it('isolates keys', async () => {\n const c = await make();\n await c.incr('a');\n await c.incr('b', { by: 7 });\n expect(await c.peek('a')).toBe(1n);\n expect(await c.peek('b')).toBe(7n);\n await c.close();\n });\n });\n}\n\nexport function runFullContract(name: string, f: ContractFactories) {\n runPubSubContract(name, f.makePubSub);\n runLockContract(name, f.makeLock);\n runKVContract(name, f.makeKV);\n runCounterContract(name, f.makeCounter);\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/framework/framework/packages/services/service-cluster/dist/testing.cjs","../src/testing.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/services/service-cluster/dist/testing.cjs","sourcesContent":[null,"// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Generic contract tests for cluster primitives.\n *\n * These are written once and run against any driver. The memory driver\n * suite calls them directly; future postgres/redis driver packages will\n * `import { runPubSubContract } from '@objectstack/service-cluster/testing'`\n * to get the same coverage for free.\n */\n\nimport { describe, expect, it, vi } from 'vitest';\nimport type {\n IPubSub,\n ILock,\n IKV,\n ICounter,\n} from '@objectstack/spec/contracts';\n\nexport interface ContractFactories {\n makePubSub: () => Promise<IPubSub>;\n makeLock: () => Promise<ILock>;\n makeKV: () => Promise<IKV>;\n makeCounter: () => Promise<ICounter>;\n}\n\nexport function runPubSubContract(name: string, make: () => Promise<IPubSub>) {\n describe(`IPubSub contract — ${name}`, () => {\n it('delivers published messages to subscribers', async () => {\n const bus = await make();\n const received: unknown[] = [];\n bus.subscribe<{ n: number }>('ch', (msg) => {\n received.push(msg.payload);\n });\n await bus.publish('ch', { n: 1 });\n await bus.publish('ch', { n: 2 });\n expect(received).toEqual([{ n: 1 }, { n: 2 }]);\n await bus.close();\n });\n\n it('does not deliver to other channels', async () => {\n const bus = await make();\n const h = vi.fn();\n bus.subscribe('a', h);\n await bus.publish('b', { x: 1 });\n expect(h).not.toHaveBeenCalled();\n await bus.close();\n });\n\n it('supports multiple subscribers per channel', async () => {\n const bus = await make();\n const a = vi.fn();\n const b = vi.fn();\n bus.subscribe('ch', a);\n bus.subscribe('ch', b);\n await bus.publish('ch', 'hi');\n expect(a).toHaveBeenCalledTimes(1);\n expect(b).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('unsubscribe stops delivery and is idempotent', async () => {\n const bus = await make();\n const h = vi.fn();\n const off = bus.subscribe('ch', h);\n await bus.publish('ch', 1);\n off();\n off(); // idempotent\n await bus.publish('ch', 2);\n expect(h).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('isolates handler errors from siblings', async () => {\n const errors: unknown[] = [];\n const bus = await make();\n // Memory driver allows override via constructor; for the\n // generic contract we just verify no throw leaks to publish().\n bus.subscribe('ch', () => {\n throw new Error('boom');\n });\n const ok = vi.fn();\n bus.subscribe('ch', ok);\n await expect(bus.publish('ch', 1)).resolves.toBeUndefined();\n expect(ok).toHaveBeenCalledTimes(1);\n await bus.close();\n void errors;\n });\n\n it('close rejects further publishes', async () => {\n const bus = await make();\n await bus.close();\n await expect(bus.publish('ch', 1)).rejects.toThrow(/closed/);\n });\n });\n}\n\nexport function runLockContract(name: string, make: () => Promise<ILock>) {\n describe(`ILock contract — ${name}`, () => {\n it('acquires and releases a free lock', async () => {\n const l = await make();\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n expect(h!.isHeld()).toBe(false);\n await l.close();\n });\n\n it('fails fast when waitMs=0 and lock is held', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n expect(h1).not.toBeNull();\n const h2 = await l.acquire('k');\n expect(h2).toBeNull();\n await h1!.release();\n await l.close();\n });\n\n it('hands off lock to a waiter on release', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const waiterP = l.acquire('k', { waitMs: 1000 });\n await new Promise((r) => setTimeout(r, 10));\n await h1!.release();\n const h2 = await waiterP;\n expect(h2).not.toBeNull();\n expect(h2!.isHeld()).toBe(true);\n await h2!.release();\n await l.close();\n });\n\n it('TTL auto-releases a stuck holder', async () => {\n const l = await make();\n const h1 = await l.acquire('k', { ttlMs: 50 });\n expect(h1).not.toBeNull();\n await new Promise((r) => setTimeout(r, 150));\n expect(h1!.isHeld()).toBe(false);\n const h2 = await l.acquire('k');\n expect(h2).not.toBeNull();\n await h2!.release();\n await l.close();\n });\n\n it('fencing tokens are monotonically increasing per key', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const t1 = h1!.fencingToken;\n await h1!.release();\n const h2 = await l.acquire('k');\n expect(h2!.fencingToken).toBeGreaterThan(t1);\n await h2!.release();\n await l.close();\n });\n\n it('renew extends the lease', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 200 });\n await new Promise((r) => setTimeout(r, 50));\n await h!.renew(400);\n await new Promise((r) => setTimeout(r, 250));\n // total 300ms in, original TTL would have expired at 200ms.\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n await l.close();\n });\n\n it('renew on a lost lock throws', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 50 });\n await new Promise((r) => setTimeout(r, 150));\n await expect(h!.renew()).rejects.toThrow();\n await l.close();\n });\n\n it('withLock returns fn result and auto-releases', async () => {\n const l = await make();\n const result = await l.withLock('k', async () => 42);\n expect(result).toBe(42);\n // Lock should be free immediately.\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n await h!.release();\n await l.close();\n });\n\n it('withLock returns null without calling fn on timeout', async () => {\n const l = await make();\n const h = await l.acquire('k');\n const fn = vi.fn();\n const result = await l.withLock('k', fn, { waitMs: 0 });\n expect(result).toBeNull();\n expect(fn).not.toHaveBeenCalled();\n await h!.release();\n await l.close();\n });\n\n it('release is idempotent', async () => {\n const l = await make();\n const h = await l.acquire('k');\n await h!.release();\n await expect(h!.release()).resolves.toBeUndefined();\n await l.close();\n });\n });\n}\n\nexport function runKVContract(name: string, make: () => Promise<IKV>) {\n describe(`IKV contract — ${name}`, () => {\n it('set then get round-trips and increments version', async () => {\n const kv = await make();\n const a = await kv.set('k', { v: 1 });\n expect(a.version).toBe(1n);\n const got = await kv.get<{ v: number }>('k');\n expect(got?.value).toEqual({ v: 1 });\n const b = await kv.set('k', { v: 2 });\n expect(b.version).toBe(2n);\n await kv.close();\n });\n\n it('get on missing key returns undefined', async () => {\n const kv = await make();\n expect(await kv.get('absent')).toBeUndefined();\n await kv.close();\n });\n\n it('delete removes the key', async () => {\n const kv = await make();\n await kv.set('k', 1);\n expect(await kv.delete('k')).toBe(true);\n expect(await kv.get('k')).toBeUndefined();\n expect(await kv.delete('k')).toBe(false);\n await kv.close();\n });\n\n it('ifVersion=0n requires absent key', async () => {\n const kv = await make();\n await kv.set('k', 'first', { ifVersion: 0n });\n await expect(kv.set('k', 'dup', { ifVersion: 0n })).rejects.toThrow(\n /version mismatch/i,\n );\n await kv.close();\n });\n\n it('cas succeeds on match, fails on mismatch', async () => {\n const kv = await make();\n const e1 = await kv.set('k', 1);\n const e2 = await kv.cas('k', e1.version, 2);\n expect(e2?.value).toBe(2);\n const failed = await kv.cas('k', e1.version, 3);\n expect(failed).toBeUndefined();\n await kv.close();\n });\n\n it('TTL expires entries', async () => {\n const kv = await make();\n await kv.set('k', 'v', { ttl: 0.03 }); // 30ms\n expect((await kv.get('k'))?.value).toBe('v');\n await new Promise((r) => setTimeout(r, 60));\n expect(await kv.get('k')).toBeUndefined();\n await kv.close();\n });\n });\n}\n\nexport function runCounterContract(name: string, make: () => Promise<ICounter>) {\n describe(`ICounter contract — ${name}`, () => {\n it('starts at 0, incr returns new value', async () => {\n const c = await make();\n expect(await c.peek('k')).toBe(0n);\n expect(await c.incr('k')).toBe(1n);\n expect(await c.incr('k')).toBe(2n);\n expect(await c.peek('k')).toBe(2n);\n await c.close();\n });\n\n it('incr by custom delta', async () => {\n const c = await make();\n expect(await c.incr('k', { by: 5 })).toBe(5n);\n expect(await c.incr('k', { by: -2 })).toBe(3n);\n await c.close();\n });\n\n it('reset', async () => {\n const c = await make();\n await c.incr('k', { by: 10 });\n await c.reset('k', 100n);\n expect(await c.peek('k')).toBe(100n);\n await c.reset('k');\n expect(await c.peek('k')).toBe(0n);\n await c.close();\n });\n\n it('isolates keys', async () => {\n const c = await make();\n await c.incr('a');\n await c.incr('b', { by: 7 });\n expect(await c.peek('a')).toBe(1n);\n expect(await c.peek('b')).toBe(7n);\n await c.close();\n });\n });\n}\n\nexport function runFullContract(name: string, f: ContractFactories) {\n runPubSubContract(name, f.makePubSub);\n runLockContract(name, f.makeLock);\n runKVContract(name, f.makeKV);\n runCounterContract(name, f.makeCounter);\n}\n"]}
package/dist/testing.js CHANGED
@@ -97,9 +97,9 @@ function runLockContract(name, make) {
97
97
  });
98
98
  it("TTL auto-releases a stuck holder", async () => {
99
99
  const l = await make();
100
- const h1 = await l.acquire("k", { ttlMs: 30 });
100
+ const h1 = await l.acquire("k", { ttlMs: 50 });
101
101
  expect(h1).not.toBeNull();
102
- await new Promise((r) => setTimeout(r, 60));
102
+ await new Promise((r) => setTimeout(r, 150));
103
103
  expect(h1.isHeld()).toBe(false);
104
104
  const h2 = await l.acquire("k");
105
105
  expect(h2).not.toBeNull();
@@ -118,18 +118,18 @@ function runLockContract(name, make) {
118
118
  });
119
119
  it("renew extends the lease", async () => {
120
120
  const l = await make();
121
- const h = await l.acquire("k", { ttlMs: 40 });
122
- await new Promise((r) => setTimeout(r, 20));
123
- await h.renew(100);
124
- await new Promise((r) => setTimeout(r, 30));
121
+ const h = await l.acquire("k", { ttlMs: 200 });
122
+ await new Promise((r) => setTimeout(r, 50));
123
+ await h.renew(400);
124
+ await new Promise((r) => setTimeout(r, 250));
125
125
  expect(h.isHeld()).toBe(true);
126
126
  await h.release();
127
127
  await l.close();
128
128
  });
129
129
  it("renew on a lost lock throws", async () => {
130
130
  const l = await make();
131
- const h = await l.acquire("k", { ttlMs: 20 });
132
- await new Promise((r) => setTimeout(r, 50));
131
+ const h = await l.acquire("k", { ttlMs: 50 });
132
+ await new Promise((r) => setTimeout(r, 150));
133
133
  await expect(h.renew()).rejects.toThrow();
134
134
  await l.close();
135
135
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Generic contract tests for cluster primitives.\n *\n * These are written once and run against any driver. The memory driver\n * suite calls them directly; future postgres/redis driver packages will\n * `import { runPubSubContract } from '@objectstack/service-cluster/testing'`\n * to get the same coverage for free.\n */\n\nimport { describe, expect, it, vi } from 'vitest';\nimport type {\n IPubSub,\n ILock,\n IKV,\n ICounter,\n} from '@objectstack/spec/contracts';\n\nexport interface ContractFactories {\n makePubSub: () => Promise<IPubSub>;\n makeLock: () => Promise<ILock>;\n makeKV: () => Promise<IKV>;\n makeCounter: () => Promise<ICounter>;\n}\n\nexport function runPubSubContract(name: string, make: () => Promise<IPubSub>) {\n describe(`IPubSub contract — ${name}`, () => {\n it('delivers published messages to subscribers', async () => {\n const bus = await make();\n const received: unknown[] = [];\n bus.subscribe<{ n: number }>('ch', (msg) => {\n received.push(msg.payload);\n });\n await bus.publish('ch', { n: 1 });\n await bus.publish('ch', { n: 2 });\n expect(received).toEqual([{ n: 1 }, { n: 2 }]);\n await bus.close();\n });\n\n it('does not deliver to other channels', async () => {\n const bus = await make();\n const h = vi.fn();\n bus.subscribe('a', h);\n await bus.publish('b', { x: 1 });\n expect(h).not.toHaveBeenCalled();\n await bus.close();\n });\n\n it('supports multiple subscribers per channel', async () => {\n const bus = await make();\n const a = vi.fn();\n const b = vi.fn();\n bus.subscribe('ch', a);\n bus.subscribe('ch', b);\n await bus.publish('ch', 'hi');\n expect(a).toHaveBeenCalledTimes(1);\n expect(b).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('unsubscribe stops delivery and is idempotent', async () => {\n const bus = await make();\n const h = vi.fn();\n const off = bus.subscribe('ch', h);\n await bus.publish('ch', 1);\n off();\n off(); // idempotent\n await bus.publish('ch', 2);\n expect(h).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('isolates handler errors from siblings', async () => {\n const errors: unknown[] = [];\n const bus = await make();\n // Memory driver allows override via constructor; for the\n // generic contract we just verify no throw leaks to publish().\n bus.subscribe('ch', () => {\n throw new Error('boom');\n });\n const ok = vi.fn();\n bus.subscribe('ch', ok);\n await expect(bus.publish('ch', 1)).resolves.toBeUndefined();\n expect(ok).toHaveBeenCalledTimes(1);\n await bus.close();\n void errors;\n });\n\n it('close rejects further publishes', async () => {\n const bus = await make();\n await bus.close();\n await expect(bus.publish('ch', 1)).rejects.toThrow(/closed/);\n });\n });\n}\n\nexport function runLockContract(name: string, make: () => Promise<ILock>) {\n describe(`ILock contract — ${name}`, () => {\n it('acquires and releases a free lock', async () => {\n const l = await make();\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n expect(h!.isHeld()).toBe(false);\n await l.close();\n });\n\n it('fails fast when waitMs=0 and lock is held', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n expect(h1).not.toBeNull();\n const h2 = await l.acquire('k');\n expect(h2).toBeNull();\n await h1!.release();\n await l.close();\n });\n\n it('hands off lock to a waiter on release', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const waiterP = l.acquire('k', { waitMs: 1000 });\n await new Promise((r) => setTimeout(r, 10));\n await h1!.release();\n const h2 = await waiterP;\n expect(h2).not.toBeNull();\n expect(h2!.isHeld()).toBe(true);\n await h2!.release();\n await l.close();\n });\n\n it('TTL auto-releases a stuck holder', async () => {\n const l = await make();\n const h1 = await l.acquire('k', { ttlMs: 30 });\n expect(h1).not.toBeNull();\n await new Promise((r) => setTimeout(r, 60));\n expect(h1!.isHeld()).toBe(false);\n const h2 = await l.acquire('k');\n expect(h2).not.toBeNull();\n await h2!.release();\n await l.close();\n });\n\n it('fencing tokens are monotonically increasing per key', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const t1 = h1!.fencingToken;\n await h1!.release();\n const h2 = await l.acquire('k');\n expect(h2!.fencingToken).toBeGreaterThan(t1);\n await h2!.release();\n await l.close();\n });\n\n it('renew extends the lease', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 40 });\n await new Promise((r) => setTimeout(r, 20));\n await h!.renew(100);\n await new Promise((r) => setTimeout(r, 30));\n // total 50ms in, original TTL would have expired at 40ms.\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n await l.close();\n });\n\n it('renew on a lost lock throws', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 20 });\n await new Promise((r) => setTimeout(r, 50));\n await expect(h!.renew()).rejects.toThrow();\n await l.close();\n });\n\n it('withLock returns fn result and auto-releases', async () => {\n const l = await make();\n const result = await l.withLock('k', async () => 42);\n expect(result).toBe(42);\n // Lock should be free immediately.\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n await h!.release();\n await l.close();\n });\n\n it('withLock returns null without calling fn on timeout', async () => {\n const l = await make();\n const h = await l.acquire('k');\n const fn = vi.fn();\n const result = await l.withLock('k', fn, { waitMs: 0 });\n expect(result).toBeNull();\n expect(fn).not.toHaveBeenCalled();\n await h!.release();\n await l.close();\n });\n\n it('release is idempotent', async () => {\n const l = await make();\n const h = await l.acquire('k');\n await h!.release();\n await expect(h!.release()).resolves.toBeUndefined();\n await l.close();\n });\n });\n}\n\nexport function runKVContract(name: string, make: () => Promise<IKV>) {\n describe(`IKV contract — ${name}`, () => {\n it('set then get round-trips and increments version', async () => {\n const kv = await make();\n const a = await kv.set('k', { v: 1 });\n expect(a.version).toBe(1n);\n const got = await kv.get<{ v: number }>('k');\n expect(got?.value).toEqual({ v: 1 });\n const b = await kv.set('k', { v: 2 });\n expect(b.version).toBe(2n);\n await kv.close();\n });\n\n it('get on missing key returns undefined', async () => {\n const kv = await make();\n expect(await kv.get('absent')).toBeUndefined();\n await kv.close();\n });\n\n it('delete removes the key', async () => {\n const kv = await make();\n await kv.set('k', 1);\n expect(await kv.delete('k')).toBe(true);\n expect(await kv.get('k')).toBeUndefined();\n expect(await kv.delete('k')).toBe(false);\n await kv.close();\n });\n\n it('ifVersion=0n requires absent key', async () => {\n const kv = await make();\n await kv.set('k', 'first', { ifVersion: 0n });\n await expect(kv.set('k', 'dup', { ifVersion: 0n })).rejects.toThrow(\n /version mismatch/i,\n );\n await kv.close();\n });\n\n it('cas succeeds on match, fails on mismatch', async () => {\n const kv = await make();\n const e1 = await kv.set('k', 1);\n const e2 = await kv.cas('k', e1.version, 2);\n expect(e2?.value).toBe(2);\n const failed = await kv.cas('k', e1.version, 3);\n expect(failed).toBeUndefined();\n await kv.close();\n });\n\n it('TTL expires entries', async () => {\n const kv = await make();\n await kv.set('k', 'v', { ttl: 0.03 }); // 30ms\n expect((await kv.get('k'))?.value).toBe('v');\n await new Promise((r) => setTimeout(r, 60));\n expect(await kv.get('k')).toBeUndefined();\n await kv.close();\n });\n });\n}\n\nexport function runCounterContract(name: string, make: () => Promise<ICounter>) {\n describe(`ICounter contract — ${name}`, () => {\n it('starts at 0, incr returns new value', async () => {\n const c = await make();\n expect(await c.peek('k')).toBe(0n);\n expect(await c.incr('k')).toBe(1n);\n expect(await c.incr('k')).toBe(2n);\n expect(await c.peek('k')).toBe(2n);\n await c.close();\n });\n\n it('incr by custom delta', async () => {\n const c = await make();\n expect(await c.incr('k', { by: 5 })).toBe(5n);\n expect(await c.incr('k', { by: -2 })).toBe(3n);\n await c.close();\n });\n\n it('reset', async () => {\n const c = await make();\n await c.incr('k', { by: 10 });\n await c.reset('k', 100n);\n expect(await c.peek('k')).toBe(100n);\n await c.reset('k');\n expect(await c.peek('k')).toBe(0n);\n await c.close();\n });\n\n it('isolates keys', async () => {\n const c = await make();\n await c.incr('a');\n await c.incr('b', { by: 7 });\n expect(await c.peek('a')).toBe(1n);\n expect(await c.peek('b')).toBe(7n);\n await c.close();\n });\n });\n}\n\nexport function runFullContract(name: string, f: ContractFactories) {\n runPubSubContract(name, f.makePubSub);\n runLockContract(name, f.makeLock);\n runKVContract(name, f.makeKV);\n runCounterContract(name, f.makeCounter);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Generic contract tests for cluster primitives.\n *\n * These are written once and run against any driver. The memory driver\n * suite calls them directly; future postgres/redis driver packages will\n * `import { runPubSubContract } from '@objectstack/service-cluster/testing'`\n * to get the same coverage for free.\n */\n\nimport { describe, expect, it, vi } from 'vitest';\nimport type {\n IPubSub,\n ILock,\n IKV,\n ICounter,\n} from '@objectstack/spec/contracts';\n\nexport interface ContractFactories {\n makePubSub: () => Promise<IPubSub>;\n makeLock: () => Promise<ILock>;\n makeKV: () => Promise<IKV>;\n makeCounter: () => Promise<ICounter>;\n}\n\nexport function runPubSubContract(name: string, make: () => Promise<IPubSub>) {\n describe(`IPubSub contract — ${name}`, () => {\n it('delivers published messages to subscribers', async () => {\n const bus = await make();\n const received: unknown[] = [];\n bus.subscribe<{ n: number }>('ch', (msg) => {\n received.push(msg.payload);\n });\n await bus.publish('ch', { n: 1 });\n await bus.publish('ch', { n: 2 });\n expect(received).toEqual([{ n: 1 }, { n: 2 }]);\n await bus.close();\n });\n\n it('does not deliver to other channels', async () => {\n const bus = await make();\n const h = vi.fn();\n bus.subscribe('a', h);\n await bus.publish('b', { x: 1 });\n expect(h).not.toHaveBeenCalled();\n await bus.close();\n });\n\n it('supports multiple subscribers per channel', async () => {\n const bus = await make();\n const a = vi.fn();\n const b = vi.fn();\n bus.subscribe('ch', a);\n bus.subscribe('ch', b);\n await bus.publish('ch', 'hi');\n expect(a).toHaveBeenCalledTimes(1);\n expect(b).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('unsubscribe stops delivery and is idempotent', async () => {\n const bus = await make();\n const h = vi.fn();\n const off = bus.subscribe('ch', h);\n await bus.publish('ch', 1);\n off();\n off(); // idempotent\n await bus.publish('ch', 2);\n expect(h).toHaveBeenCalledTimes(1);\n await bus.close();\n });\n\n it('isolates handler errors from siblings', async () => {\n const errors: unknown[] = [];\n const bus = await make();\n // Memory driver allows override via constructor; for the\n // generic contract we just verify no throw leaks to publish().\n bus.subscribe('ch', () => {\n throw new Error('boom');\n });\n const ok = vi.fn();\n bus.subscribe('ch', ok);\n await expect(bus.publish('ch', 1)).resolves.toBeUndefined();\n expect(ok).toHaveBeenCalledTimes(1);\n await bus.close();\n void errors;\n });\n\n it('close rejects further publishes', async () => {\n const bus = await make();\n await bus.close();\n await expect(bus.publish('ch', 1)).rejects.toThrow(/closed/);\n });\n });\n}\n\nexport function runLockContract(name: string, make: () => Promise<ILock>) {\n describe(`ILock contract — ${name}`, () => {\n it('acquires and releases a free lock', async () => {\n const l = await make();\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n expect(h!.isHeld()).toBe(false);\n await l.close();\n });\n\n it('fails fast when waitMs=0 and lock is held', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n expect(h1).not.toBeNull();\n const h2 = await l.acquire('k');\n expect(h2).toBeNull();\n await h1!.release();\n await l.close();\n });\n\n it('hands off lock to a waiter on release', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const waiterP = l.acquire('k', { waitMs: 1000 });\n await new Promise((r) => setTimeout(r, 10));\n await h1!.release();\n const h2 = await waiterP;\n expect(h2).not.toBeNull();\n expect(h2!.isHeld()).toBe(true);\n await h2!.release();\n await l.close();\n });\n\n it('TTL auto-releases a stuck holder', async () => {\n const l = await make();\n const h1 = await l.acquire('k', { ttlMs: 50 });\n expect(h1).not.toBeNull();\n await new Promise((r) => setTimeout(r, 150));\n expect(h1!.isHeld()).toBe(false);\n const h2 = await l.acquire('k');\n expect(h2).not.toBeNull();\n await h2!.release();\n await l.close();\n });\n\n it('fencing tokens are monotonically increasing per key', async () => {\n const l = await make();\n const h1 = await l.acquire('k');\n const t1 = h1!.fencingToken;\n await h1!.release();\n const h2 = await l.acquire('k');\n expect(h2!.fencingToken).toBeGreaterThan(t1);\n await h2!.release();\n await l.close();\n });\n\n it('renew extends the lease', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 200 });\n await new Promise((r) => setTimeout(r, 50));\n await h!.renew(400);\n await new Promise((r) => setTimeout(r, 250));\n // total 300ms in, original TTL would have expired at 200ms.\n expect(h!.isHeld()).toBe(true);\n await h!.release();\n await l.close();\n });\n\n it('renew on a lost lock throws', async () => {\n const l = await make();\n const h = await l.acquire('k', { ttlMs: 50 });\n await new Promise((r) => setTimeout(r, 150));\n await expect(h!.renew()).rejects.toThrow();\n await l.close();\n });\n\n it('withLock returns fn result and auto-releases', async () => {\n const l = await make();\n const result = await l.withLock('k', async () => 42);\n expect(result).toBe(42);\n // Lock should be free immediately.\n const h = await l.acquire('k');\n expect(h).not.toBeNull();\n await h!.release();\n await l.close();\n });\n\n it('withLock returns null without calling fn on timeout', async () => {\n const l = await make();\n const h = await l.acquire('k');\n const fn = vi.fn();\n const result = await l.withLock('k', fn, { waitMs: 0 });\n expect(result).toBeNull();\n expect(fn).not.toHaveBeenCalled();\n await h!.release();\n await l.close();\n });\n\n it('release is idempotent', async () => {\n const l = await make();\n const h = await l.acquire('k');\n await h!.release();\n await expect(h!.release()).resolves.toBeUndefined();\n await l.close();\n });\n });\n}\n\nexport function runKVContract(name: string, make: () => Promise<IKV>) {\n describe(`IKV contract — ${name}`, () => {\n it('set then get round-trips and increments version', async () => {\n const kv = await make();\n const a = await kv.set('k', { v: 1 });\n expect(a.version).toBe(1n);\n const got = await kv.get<{ v: number }>('k');\n expect(got?.value).toEqual({ v: 1 });\n const b = await kv.set('k', { v: 2 });\n expect(b.version).toBe(2n);\n await kv.close();\n });\n\n it('get on missing key returns undefined', async () => {\n const kv = await make();\n expect(await kv.get('absent')).toBeUndefined();\n await kv.close();\n });\n\n it('delete removes the key', async () => {\n const kv = await make();\n await kv.set('k', 1);\n expect(await kv.delete('k')).toBe(true);\n expect(await kv.get('k')).toBeUndefined();\n expect(await kv.delete('k')).toBe(false);\n await kv.close();\n });\n\n it('ifVersion=0n requires absent key', async () => {\n const kv = await make();\n await kv.set('k', 'first', { ifVersion: 0n });\n await expect(kv.set('k', 'dup', { ifVersion: 0n })).rejects.toThrow(\n /version mismatch/i,\n );\n await kv.close();\n });\n\n it('cas succeeds on match, fails on mismatch', async () => {\n const kv = await make();\n const e1 = await kv.set('k', 1);\n const e2 = await kv.cas('k', e1.version, 2);\n expect(e2?.value).toBe(2);\n const failed = await kv.cas('k', e1.version, 3);\n expect(failed).toBeUndefined();\n await kv.close();\n });\n\n it('TTL expires entries', async () => {\n const kv = await make();\n await kv.set('k', 'v', { ttl: 0.03 }); // 30ms\n expect((await kv.get('k'))?.value).toBe('v');\n await new Promise((r) => setTimeout(r, 60));\n expect(await kv.get('k')).toBeUndefined();\n await kv.close();\n });\n });\n}\n\nexport function runCounterContract(name: string, make: () => Promise<ICounter>) {\n describe(`ICounter contract — ${name}`, () => {\n it('starts at 0, incr returns new value', async () => {\n const c = await make();\n expect(await c.peek('k')).toBe(0n);\n expect(await c.incr('k')).toBe(1n);\n expect(await c.incr('k')).toBe(2n);\n expect(await c.peek('k')).toBe(2n);\n await c.close();\n });\n\n it('incr by custom delta', async () => {\n const c = await make();\n expect(await c.incr('k', { by: 5 })).toBe(5n);\n expect(await c.incr('k', { by: -2 })).toBe(3n);\n await c.close();\n });\n\n it('reset', async () => {\n const c = await make();\n await c.incr('k', { by: 10 });\n await c.reset('k', 100n);\n expect(await c.peek('k')).toBe(100n);\n await c.reset('k');\n expect(await c.peek('k')).toBe(0n);\n await c.close();\n });\n\n it('isolates keys', async () => {\n const c = await make();\n await c.incr('a');\n await c.incr('b', { by: 7 });\n expect(await c.peek('a')).toBe(1n);\n expect(await c.peek('b')).toBe(7n);\n await c.close();\n });\n });\n}\n\nexport function runFullContract(name: string, f: ContractFactories) {\n runPubSubContract(name, f.makePubSub);\n runLockContract(name, f.makeLock);\n runKVContract(name, f.makeKV);\n runCounterContract(name, f.makeCounter);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-cluster",
3
- "version": "5.1.2",
3
+ "version": "5.1.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Cluster Service for ObjectStack — pluggable PubSub/Lock/KV/Counter primitives. Memory driver included; postgres/redis drivers ship separately.",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@objectstack/core": "6.0.0",
23
- "@objectstack/spec": "6.0.0"
22
+ "@objectstack/core": "6.2.0",
23
+ "@objectstack/spec": "6.2.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.9.1",
package/src/testing.ts CHANGED
@@ -132,9 +132,9 @@ export function runLockContract(name: string, make: () => Promise<ILock>) {
132
132
 
133
133
  it('TTL auto-releases a stuck holder', async () => {
134
134
  const l = await make();
135
- const h1 = await l.acquire('k', { ttlMs: 30 });
135
+ const h1 = await l.acquire('k', { ttlMs: 50 });
136
136
  expect(h1).not.toBeNull();
137
- await new Promise((r) => setTimeout(r, 60));
137
+ await new Promise((r) => setTimeout(r, 150));
138
138
  expect(h1!.isHeld()).toBe(false);
139
139
  const h2 = await l.acquire('k');
140
140
  expect(h2).not.toBeNull();
@@ -155,11 +155,11 @@ export function runLockContract(name: string, make: () => Promise<ILock>) {
155
155
 
156
156
  it('renew extends the lease', async () => {
157
157
  const l = await make();
158
- const h = await l.acquire('k', { ttlMs: 40 });
159
- await new Promise((r) => setTimeout(r, 20));
160
- await h!.renew(100);
161
- await new Promise((r) => setTimeout(r, 30));
162
- // total 50ms in, original TTL would have expired at 40ms.
158
+ const h = await l.acquire('k', { ttlMs: 200 });
159
+ await new Promise((r) => setTimeout(r, 50));
160
+ await h!.renew(400);
161
+ await new Promise((r) => setTimeout(r, 250));
162
+ // total 300ms in, original TTL would have expired at 200ms.
163
163
  expect(h!.isHeld()).toBe(true);
164
164
  await h!.release();
165
165
  await l.close();
@@ -167,8 +167,8 @@ export function runLockContract(name: string, make: () => Promise<ILock>) {
167
167
 
168
168
  it('renew on a lost lock throws', async () => {
169
169
  const l = await make();
170
- const h = await l.acquire('k', { ttlMs: 20 });
171
- await new Promise((r) => setTimeout(r, 50));
170
+ const h = await l.acquire('k', { ttlMs: 50 });
171
+ await new Promise((r) => setTimeout(r, 150));
172
172
  await expect(h!.renew()).rejects.toThrow();
173
173
  await l.close();
174
174
  });