subscription-registry.test.ts64 lines · main
| 1 | import { describe, expect, it } from 'bun:test'; |
| 2 | |
| 3 | import { SubscriptionRegistry } from './subscription-registry.js'; |
| 4 | |
| 5 | describe('SubscriptionRegistry — Phase 1 §1.1 ref-counting invariants', () => { |
| 6 | it('add → add same → remove → remove same → empty', () => { |
| 7 | const r = new SubscriptionRegistry(); |
| 8 | |
| 9 | // First attach to an empty channel returns true so the caller knows |
| 10 | // to issue LISTEN exactly once per channel. |
| 11 | expect(r.attach('s1', 'c1')).toBe(true); |
| 12 | expect(r.hasChannel('c1')).toBe(true); |
| 13 | expect(r.channelCount).toBe(1); |
| 14 | |
| 15 | // Idempotent attach — same sub, same channel — returns false because |
| 16 | // the channel is no longer empty. Set semantics mean no duplicate entry. |
| 17 | expect(r.attach('s1', 'c1')).toBe(false); |
| 18 | expect(r.subsForChannel('c1')).toEqual(['s1']); |
| 19 | expect(r.channelCount).toBe(1); |
| 20 | |
| 21 | // Last detach drains the set → returns true → caller issues UNLISTEN. |
| 22 | expect(r.detach('s1', 'c1')).toBe(true); |
| 23 | expect(r.hasChannel('c1')).toBe(false); |
| 24 | expect(r.channelCount).toBe(0); |
| 25 | |
| 26 | // Detach the same sub again from the same (now-gone) channel: no-op, |
| 27 | // returns false. Caller must not double-UNLISTEN. |
| 28 | expect(r.detach('s1', 'c1')).toBe(false); |
| 29 | expect(r.channelCount).toBe(0); |
| 30 | expect(r.subsForChannel('c1')).toEqual([]); |
| 31 | }); |
| 32 | |
| 33 | it('multiple subs on a channel: only first attach and last detach return true', () => { |
| 34 | const r = new SubscriptionRegistry(); |
| 35 | |
| 36 | expect(r.attach('s1', 'c1')).toBe(true); // first → LISTEN |
| 37 | expect(r.attach('s2', 'c1')).toBe(false); // ref + 1 |
| 38 | expect(r.attach('s3', 'c1')).toBe(false); // ref + 1 |
| 39 | |
| 40 | expect(r.subsForChannel('c1').slice().sort()).toEqual(['s1', 's2', 's3']); |
| 41 | |
| 42 | expect(r.detach('s2', 'c1')).toBe(false); // ref - 1, still has subs |
| 43 | expect(r.detach('s1', 'c1')).toBe(false); // ref - 1, still has subs |
| 44 | expect(r.detach('s3', 'c1')).toBe(true); // last → UNLISTEN |
| 45 | expect(r.hasChannel('c1')).toBe(false); |
| 46 | }); |
| 47 | |
| 48 | it('detach of unknown sub is a no-op false', () => { |
| 49 | const r = new SubscriptionRegistry(); |
| 50 | r.attach('s1', 'c1'); |
| 51 | expect(r.detach('s-bogus', 'c1')).toBe(false); |
| 52 | expect(r.subsForChannel('c1')).toEqual(['s1']); |
| 53 | }); |
| 54 | |
| 55 | it('subsForChannel returns a snapshot — mutation during iteration is safe', () => { |
| 56 | const r = new SubscriptionRegistry(); |
| 57 | r.attach('s1', 'c1'); |
| 58 | r.attach('s2', 'c1'); |
| 59 | const snap = r.subsForChannel('c1'); |
| 60 | r.detach('s1', 'c1'); |
| 61 | expect(snap.slice().sort()).toEqual(['s1', 's2']); |
| 62 | expect(r.subsForChannel('c1')).toEqual(['s2']); |
| 63 | }); |
| 64 | }); |