subscription-registry.test.ts64 lines · main
1import { describe, expect, it } from 'bun:test';
2
3import { SubscriptionRegistry } from './subscription-registry.js';
4
5describe('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});