@blockcast/mmt-transport 0.2.2 → 0.3.0

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.
Files changed (167) hide show
  1. package/README.md +44 -58
  2. package/dist/abr-controller.d.ts +34 -4
  3. package/dist/abr-controller.d.ts.map +1 -1
  4. package/dist/abr-controller.js +66 -17
  5. package/dist/abr-controller.js.map +1 -1
  6. package/dist/amt-gateway.d.ts.map +1 -1
  7. package/dist/amt-gateway.js +3 -4
  8. package/dist/amt-gateway.js.map +1 -1
  9. package/dist/driad-discovery.d.ts.map +1 -1
  10. package/dist/driad-discovery.js +3 -6
  11. package/dist/driad-discovery.js.map +1 -1
  12. package/dist/endpoint-config.d.ts +68 -0
  13. package/dist/endpoint-config.d.ts.map +1 -1
  14. package/dist/endpoint-config.js +118 -3
  15. package/dist/endpoint-config.js.map +1 -1
  16. package/dist/index.d.ts +10 -8
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8 -11
  19. package/dist/index.js.map +1 -1
  20. package/dist/mmt-transport.no-modules.js +881 -60
  21. package/dist/mmt-transport.no-modules.js.map +4 -4
  22. package/dist/moqt-priority.d.ts +39 -0
  23. package/dist/moqt-priority.d.ts.map +1 -0
  24. package/dist/moqt-priority.js +41 -0
  25. package/dist/moqt-priority.js.map +1 -0
  26. package/dist/multicast-catalog-policy.d.ts +96 -0
  27. package/dist/multicast-catalog-policy.d.ts.map +1 -0
  28. package/dist/multicast-catalog-policy.js +660 -0
  29. package/dist/multicast-catalog-policy.js.map +1 -0
  30. package/dist/multicast-orchestrator.d.ts +24 -0
  31. package/dist/multicast-orchestrator.d.ts.map +1 -1
  32. package/dist/multicast-orchestrator.js +102 -9
  33. package/dist/multicast-orchestrator.js.map +1 -1
  34. package/dist/multicast-signaling-reconciler.d.ts +37 -0
  35. package/dist/multicast-signaling-reconciler.d.ts.map +1 -0
  36. package/dist/multicast-signaling-reconciler.js +161 -0
  37. package/dist/multicast-signaling-reconciler.js.map +1 -0
  38. package/dist/multicast-subscription-pool.d.ts.map +1 -1
  39. package/dist/multicast-subscription-pool.js +37 -10
  40. package/dist/multicast-subscription-pool.js.map +1 -1
  41. package/dist/multicast-tier.d.ts +19 -0
  42. package/dist/multicast-tier.d.ts.map +1 -1
  43. package/dist/multicast-tier.js +21 -4
  44. package/dist/multicast-tier.js.map +1 -1
  45. package/dist/no-modules.d.ts +40 -3
  46. package/dist/no-modules.d.ts.map +1 -1
  47. package/dist/no-modules.js +30 -3
  48. package/dist/no-modules.js.map +1 -1
  49. package/dist/packet-batch.d.ts +13 -0
  50. package/dist/packet-batch.d.ts.map +1 -0
  51. package/dist/packet-batch.js +14 -0
  52. package/dist/packet-batch.js.map +1 -0
  53. package/dist/stall-detector.d.ts +10 -0
  54. package/dist/stall-detector.d.ts.map +1 -1
  55. package/dist/stall-detector.js +21 -0
  56. package/dist/stall-detector.js.map +1 -1
  57. package/dist/takeover.d.ts +3 -0
  58. package/dist/takeover.d.ts.map +1 -1
  59. package/dist/takeover.js +35 -2
  60. package/dist/takeover.js.map +1 -1
  61. package/dist/transport-manager.d.ts +21 -17
  62. package/dist/transport-manager.d.ts.map +1 -1
  63. package/dist/transport-manager.js +63 -29
  64. package/dist/transport-manager.js.map +1 -1
  65. package/dist/types.d.ts +98 -123
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js +12 -50
  68. package/dist/types.js.map +1 -1
  69. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  70. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  71. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  72. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  73. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  74. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  75. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  76. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  77. package/node_modules/@blockcast/mmt-base/README.md +134 -0
  78. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  79. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
  80. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
  81. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
  82. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
  83. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
  84. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  85. package/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
  86. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  87. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
  88. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  89. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
  90. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  91. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  92. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  93. package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  94. package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  95. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  96. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  97. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  98. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  99. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
  100. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
  101. package/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
  102. package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
  103. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
  104. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
  105. package/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
  106. package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
  107. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  108. package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
  109. package/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
  110. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
  111. package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  112. package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  113. package/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
  114. package/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
  115. package/package.json +13 -6
  116. package/src/abr-controller.ts +81 -17
  117. package/src/amt-gateway.ts +3 -5
  118. package/src/driad-discovery.ts +3 -6
  119. package/src/endpoint-config.ts +157 -3
  120. package/src/index.ts +54 -33
  121. package/src/moqt-priority.ts +51 -0
  122. package/src/multicast-catalog-policy.ts +753 -0
  123. package/src/multicast-orchestrator.ts +114 -10
  124. package/src/multicast-signaling-reconciler.ts +156 -0
  125. package/src/multicast-subscription-pool.ts +46 -11
  126. package/src/multicast-tier.ts +30 -3
  127. package/src/no-modules.ts +54 -2
  128. package/src/packet-batch.ts +17 -0
  129. package/src/stall-detector.ts +37 -0
  130. package/src/takeover.ts +41 -3
  131. package/src/transport-manager.ts +64 -34
  132. package/src/types.ts +103 -160
  133. package/dist/endpoint-config.test.d.ts +0 -2
  134. package/dist/endpoint-config.test.d.ts.map +0 -1
  135. package/dist/endpoint-config.test.js +0 -39
  136. package/dist/endpoint-config.test.js.map +0 -1
  137. package/dist/fec-client.d.ts +0 -442
  138. package/dist/fec-client.d.ts.map +0 -1
  139. package/dist/fec-client.js +0 -789
  140. package/dist/fec-client.js.map +0 -1
  141. package/dist/fec-client.test.d.ts +0 -8
  142. package/dist/fec-client.test.d.ts.map +0 -1
  143. package/dist/fec-client.test.js +0 -112
  144. package/dist/fec-client.test.js.map +0 -1
  145. package/dist/multicast-subscription-pool.test.d.ts +0 -2
  146. package/dist/multicast-subscription-pool.test.d.ts.map +0 -1
  147. package/dist/multicast-subscription-pool.test.js +0 -108
  148. package/dist/multicast-subscription-pool.test.js.map +0 -1
  149. package/dist/multicast-tier.test.d.ts +0 -2
  150. package/dist/multicast-tier.test.d.ts.map +0 -1
  151. package/dist/multicast-tier.test.js +0 -75
  152. package/dist/multicast-tier.test.js.map +0 -1
  153. package/dist/stall-detector.test.d.ts +0 -2
  154. package/dist/stall-detector.test.d.ts.map +0 -1
  155. package/dist/stall-detector.test.js +0 -33
  156. package/dist/stall-detector.test.js.map +0 -1
  157. package/dist/transport-manager.test.d.ts +0 -2
  158. package/dist/transport-manager.test.d.ts.map +0 -1
  159. package/dist/transport-manager.test.js +0 -155
  160. package/dist/transport-manager.test.js.map +0 -1
  161. package/src/endpoint-config.test.ts +0 -54
  162. package/src/fec-client.test.ts +0 -140
  163. package/src/fec-client.ts +0 -1103
  164. package/src/multicast-subscription-pool.test.ts +0 -115
  165. package/src/multicast-tier.test.ts +0 -80
  166. package/src/stall-detector.test.ts +0 -34
  167. package/src/transport-manager.test.ts +0 -192
@@ -1,115 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { MulticastSubscriptionPool } from "./multicast-subscription-pool.js";
3
- import type { TransportConfig, WindowMulticastSubscriptionEventMap } from "./types.js";
4
-
5
- const config: TransportConfig = {
6
- group: "232.1.1.1",
7
- source: "192.0.2.1",
8
- port: 5000,
9
- streamType: "video",
10
- };
11
-
12
- describe("MulticastSubscriptionPool", () => {
13
- it("shares an endpoint and unsubscribes only after the last owner releases", async () => {
14
- const listeners = new Map<keyof WindowMulticastSubscriptionEventMap, (event: CustomEvent<never>) => void>();
15
- const unsubscribe = vi.fn();
16
- const subscription = {
17
- addEventListener: vi.fn((type, listener) => listeners.set(type, listener)),
18
- removeEventListener: vi.fn(),
19
- unsubscribe,
20
- };
21
- const subscribe = vi.fn().mockResolvedValue(subscription);
22
- const gateway = { subscribe };
23
- const pool = new MulticastSubscriptionPool();
24
- const firstPacket = vi.fn();
25
- const secondPacket = vi.fn();
26
-
27
- const first = await pool.acquire(gateway, config, { onBytes: firstPacket });
28
- const second = await pool.acquire(gateway, { ...config, group: " 232.1.1.1 " }, { onBytes: secondPacket });
29
- expect(subscribe).toHaveBeenCalledOnce();
30
-
31
- const payload = new Uint8Array([1, 2]).buffer;
32
- listeners.get("bytes")?.({ detail: payload } as CustomEvent<never>);
33
- expect(firstPacket).toHaveBeenCalledWith(payload);
34
- expect(secondPacket).toHaveBeenCalledWith(payload);
35
-
36
- await first.release();
37
- expect(unsubscribe).not.toHaveBeenCalled();
38
- await second.release();
39
- expect(unsubscribe).toHaveBeenCalledOnce();
40
- expect(pool.size).toBe(0);
41
- });
42
-
43
- it("counts an awaiting acquire before the previous owner releases", async () => {
44
- const unsubscribe = vi.fn();
45
- const subscription = {
46
- addEventListener: vi.fn(), removeEventListener: vi.fn(), unsubscribe,
47
- };
48
- const subscribe = vi.fn().mockResolvedValue(subscription);
49
- const gateway = { subscribe };
50
- const pool = new MulticastSubscriptionPool();
51
- const first = await pool.acquire(gateway, config, { onBytes: vi.fn() });
52
- const secondPending = pool.acquire(gateway, config, { onBytes: vi.fn() });
53
- await first.release();
54
- const second = await secondPending;
55
- expect(unsubscribe).not.toHaveBeenCalled();
56
- expect(pool.size).toBe(1);
57
- await second.release();
58
- expect(unsubscribe).toHaveBeenCalledOnce();
59
- });
60
-
61
- it("does not conflate transport preferences or gateway instances", async () => {
62
- const makeSubscription = () => ({
63
- addEventListener: vi.fn(), removeEventListener: vi.fn(), unsubscribe: vi.fn(),
64
- });
65
- const firstGateway = { subscribe: vi.fn().mockResolvedValue(makeSubscription()) };
66
- const secondGateway = { subscribe: vi.fn().mockResolvedValue(makeSubscription()) };
67
- const pool = new MulticastSubscriptionPool();
68
- const native = await pool.acquire(firstGateway, { ...config, mode: "native" }, { onBytes: vi.fn() });
69
- const tunnel = await pool.acquire(firstGateway, { ...config, mode: "tunnel", amtRelay: "relay.example" }, { onBytes: vi.fn() });
70
- const replacement = await pool.acquire(secondGateway, { ...config, mode: "native" }, { onBytes: vi.fn() });
71
- expect(firstGateway.subscribe).toHaveBeenCalledTimes(2);
72
- expect(secondGateway.subscribe).toHaveBeenCalledOnce();
73
- await Promise.all([native.release(), tunnel.release(), replacement.release()]);
74
- });
75
-
76
- it("evicts a closed entry so the next owner reconnects", async () => {
77
- const stateListeners: Array<(event: CustomEvent<"closed">) => void> = [];
78
- const subscribe = vi.fn().mockImplementation(async () => ({
79
- addEventListener: vi.fn((type, listener) => {
80
- if (type === "state") stateListeners.push(listener);
81
- }),
82
- removeEventListener: vi.fn(),
83
- unsubscribe: vi.fn(),
84
- }));
85
- const gateway = { subscribe };
86
- const pool = new MulticastSubscriptionPool();
87
- const first = await pool.acquire(gateway, config, { onBytes: vi.fn() });
88
- stateListeners[0]({ detail: "closed" } as CustomEvent<"closed">);
89
- expect(pool.size).toBe(0);
90
- const second = await pool.acquire(gateway, config, { onBytes: vi.fn() });
91
- expect(subscribe).toHaveBeenCalledTimes(2);
92
- await first.release();
93
- await second.release();
94
- });
95
-
96
- it("isolates consumer callback failures", async () => {
97
- let bytesListener: ((event: CustomEvent<ArrayBuffer>) => void) | undefined;
98
- const subscribe = vi.fn().mockResolvedValue({
99
- addEventListener: vi.fn((type, listener) => {
100
- if (type === "bytes") bytesListener = listener;
101
- }),
102
- removeEventListener: vi.fn(), unsubscribe: vi.fn(),
103
- });
104
- const gateway = { subscribe };
105
- const pool = new MulticastSubscriptionPool();
106
- const secondConsumer = vi.fn();
107
- const first = await pool.acquire(gateway, config, { onBytes: () => { throw new Error("boom"); } });
108
- const second = await pool.acquire(gateway, config, { onBytes: secondConsumer });
109
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
110
- bytesListener?.({ detail: new ArrayBuffer(1) } as CustomEvent<ArrayBuffer>);
111
- expect(secondConsumer).toHaveBeenCalledOnce();
112
- consoleError.mockRestore();
113
- await Promise.all([first.release(), second.release()]);
114
- });
115
- });
@@ -1,80 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { MulticastOrchestrator } from "./multicast-orchestrator.js";
3
- import { endpointKey, selectMulticastTier, type MulticastTierEndpoint } from "./multicast-tier.js";
4
-
5
- const low: MulticastTierEndpoint = {
6
- groupAddress: "232.1.1.1", port: 5000, tracks: [{ name: "video/720p" }], bandwidth: 2_000_000,
7
- };
8
- const high: MulticastTierEndpoint = {
9
- groupAddress: "232.1.1.2", port: 5000, tracks: [{ name: "video/4k" }], bandwidth: 15_000_000,
10
- };
11
-
12
- describe("multicast tier selection", () => {
13
- it("starts conservatively and applies headroom", () => {
14
- expect(selectMulticastTier([high, low], undefined)).toBe(low);
15
- expect(selectMulticastTier([low, high], 16_000_000)).toBe(low);
16
- expect(selectMulticastTier([low, high], 20_000_000)).toBe(high);
17
- });
18
-
19
- it("switches make-before-break only on a valid target RAP", () => {
20
- let now = 0;
21
- const orchestrator = new MulticastOrchestrator({
22
- endpoints: [low, high], now: () => now, dwellMs: 1000, joinDeadlineMs: 5000,
23
- });
24
- expect(orchestrator.start()).toBe(low);
25
- expect(orchestrator.onGoodput({ bitrate: 20_000_000 })).toBeUndefined();
26
- now = 1000;
27
- expect(orchestrator.onGoodput({ bitrate: 20_000_000 })?.kind).toBe("join-target");
28
- expect(orchestrator.onTierPacket({
29
- endpointKey: endpointKey(high), keyframe: false, timestamp: 10, hasValidCodecConfig: true,
30
- })).toBeUndefined();
31
- expect(orchestrator.onTierPacket({
32
- endpointKey: endpointKey(high), keyframe: true, timestamp: 11, hasValidCodecConfig: true,
33
- })?.kind).toBe("commit-swap");
34
- });
35
-
36
- it("preserves a pinned initial tier", () => {
37
- let now = 0;
38
- const orchestrator = new MulticastOrchestrator({
39
- endpoints: [low, high], initialEndpoint: high, adaptive: false,
40
- now: () => now, dwellMs: 1000, joinDeadlineMs: 5000,
41
- });
42
- expect(orchestrator.start()).toBe(high);
43
- expect(orchestrator.onGoodput({ bitrate: 1_000_000 })).toBeUndefined();
44
- now = 10_000;
45
- expect(orchestrator.onGoodput({ bitrate: 1_000_000 })).toBeUndefined();
46
- expect(orchestrator.current).toBe(high);
47
- });
48
-
49
- it("abandons a silent target when polled after the join deadline", () => {
50
- let now = 0;
51
- const orchestrator = new MulticastOrchestrator({
52
- endpoints: [low, high], now: () => now, dwellMs: 1000, joinDeadlineMs: 5000,
53
- });
54
- orchestrator.start();
55
- orchestrator.onGoodput({ bitrate: 20_000_000 });
56
- now = 1000;
57
- orchestrator.onGoodput({ bitrate: 20_000_000 });
58
- now = 6000;
59
- expect(orchestrator.poll()?.kind).toBe("abandon-join");
60
- expect(orchestrator.state).toBe("stable");
61
- });
62
-
63
- it("rejects a target RAP older than the active tier high-water mark", () => {
64
- let now = 0;
65
- const orchestrator = new MulticastOrchestrator({
66
- endpoints: [low, high], now: () => now, dwellMs: 1000, joinDeadlineMs: 5000,
67
- });
68
- orchestrator.start();
69
- orchestrator.onTierPacket({
70
- endpointKey: endpointKey(low), keyframe: false, timestamp: 10_000, hasValidCodecConfig: true,
71
- });
72
- orchestrator.onGoodput({ bitrate: 20_000_000 });
73
- now = 1000;
74
- orchestrator.onGoodput({ bitrate: 20_000_000 });
75
- expect(orchestrator.onTierPacket({
76
- endpointKey: endpointKey(high), keyframe: true, timestamp: 9_000, hasValidCodecConfig: true,
77
- })).toBeUndefined();
78
- expect(orchestrator.state).toBe("joining");
79
- });
80
- });
@@ -1,34 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { TransportStallDetector } from "./stall-detector.js";
3
-
4
- describe("TransportStallDetector", () => {
5
- it("detects repair-only starvation and recovery", () => {
6
- let now = 0;
7
- const onStarvation = vi.fn();
8
- const onRecovery = vi.fn();
9
- const detector = new TransportStallDetector({ windowMs: 1000, now: () => now, onStarvation, onRecovery });
10
- detector.update({ sourceSymbols: 0, repairSymbols: 0, blocksComplete: 0, blocksRecovered: 0 });
11
- now = 1000;
12
- detector.update({ sourceSymbols: 0, repairSymbols: 10, blocksComplete: 0, blocksRecovered: 0 });
13
- expect(onStarvation).toHaveBeenCalledOnce();
14
- now = 1100;
15
- detector.update({ sourceSymbols: 1, repairSymbols: 10, blocksComplete: 1, blocksRecovered: 0 });
16
- expect(onRecovery).toHaveBeenCalledOnce();
17
- });
18
-
19
- it("detects starvation when all packet counters stop", () => {
20
- let now = 0;
21
- const onStarvation = vi.fn();
22
- const detector = new TransportStallDetector({
23
- windowMs: 1000,
24
- now: () => now,
25
- onStarvation,
26
- onRecovery: vi.fn(),
27
- });
28
- const snapshot = { sourceSymbols: 10, repairSymbols: 2, blocksComplete: 1, blocksRecovered: 0 };
29
- detector.update(snapshot);
30
- now = 1000;
31
- detector.update(snapshot);
32
- expect(onStarvation).toHaveBeenCalledOnce();
33
- });
34
- });
@@ -1,192 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- import { TransportManager } from "./transport-manager.js";
4
- import type {
5
- TransportConfig,
6
- WindowMulticastSubscriptionEventMap,
7
- } from "./types.js";
8
-
9
- const CONFIG: TransportConfig = {
10
- group: "232.1.1.50",
11
- source: "192.168.1.181",
12
- port: 8000,
13
- streamType: "video",
14
- driadEnabled: false,
15
- };
16
-
17
- describe("TransportManager window.multicast integration", () => {
18
- let hadWindow = false;
19
- let previousWindow: unknown;
20
-
21
- beforeEach(() => {
22
- hadWindow = Object.hasOwn(globalThis, "window");
23
- previousWindow = (globalThis as unknown as { window?: unknown })
24
- .window;
25
- vi.spyOn(console, "log").mockImplementation(() => {});
26
- vi.spyOn(console, "warn").mockImplementation(() => {});
27
- vi.spyOn(console, "error").mockImplementation(() => {});
28
- });
29
-
30
- afterEach(() => {
31
- if (hadWindow) {
32
- (globalThis as unknown as { window?: unknown }).window =
33
- previousWindow;
34
- } else {
35
- delete (globalThis as unknown as { window?: unknown }).window;
36
- }
37
- vi.restoreAllMocks();
38
- });
39
-
40
- it("does not treat legacy window.Multicast as a usable transport", async () => {
41
- const subscribeSSM = vi.fn();
42
- (globalThis as unknown as { window?: unknown }).window = {
43
- Multicast: { subscribeSSM },
44
- };
45
-
46
- const manager = new TransportManager();
47
- const connected = await manager.connect(CONFIG);
48
-
49
- expect(connected).toBe(false);
50
- expect(subscribeSSM).not.toHaveBeenCalled();
51
- });
52
-
53
- it("subscribes through lowercase window.multicast and forwards bytes", async () => {
54
- const listeners = new Map<
55
- keyof WindowMulticastSubscriptionEventMap,
56
- (event: CustomEvent<unknown>) => void
57
- >();
58
- const subscription = {
59
- id: "sub-1",
60
- addEventListener: vi.fn((type, listener) => {
61
- listeners.set(
62
- type as keyof WindowMulticastSubscriptionEventMap,
63
- listener as (event: CustomEvent<unknown>) => void,
64
- );
65
- }),
66
- removeEventListener: vi.fn(),
67
- unsubscribe: vi.fn().mockResolvedValue(undefined),
68
- };
69
- const subscribe = vi.fn().mockResolvedValue(subscription);
70
- (globalThis as unknown as { window?: unknown }).window = {
71
- multicast: {
72
- capabilities: { consumeModes: ["bytes"] },
73
- subscribe,
74
- },
75
- };
76
-
77
- const manager = new TransportManager();
78
- const onPacket = vi.fn();
79
- manager.setPacketCallback(onPacket);
80
-
81
- const connected = await manager.connect(CONFIG);
82
- expect(connected).toBe(true);
83
- expect(subscribe).toHaveBeenCalledWith({
84
- group: CONFIG.group,
85
- source: CONFIG.source,
86
- port: CONFIG.port,
87
- consume: "bytes",
88
- });
89
-
90
- const payload = new Uint8Array([1, 2, 3]).buffer;
91
- listeners.get("bytes")?.({ detail: payload } as CustomEvent<unknown>);
92
-
93
- expect(onPacket).toHaveBeenCalledWith(payload);
94
- expect(manager.getState().packetsReceived).toBe(1);
95
- expect(manager.getState().bytesReceived).toBe(3);
96
- manager.disconnect();
97
- });
98
-
99
- it("releases the previous lease before reconnecting", async () => {
100
- const unsubscribe = vi.fn();
101
- const subscription = {
102
- addEventListener: vi.fn(), removeEventListener: vi.fn(), unsubscribe,
103
- };
104
- const subscribe = vi.fn().mockResolvedValue(subscription);
105
- (globalThis as unknown as { window?: unknown }).window = {
106
- multicast: { capabilities: { consumeModes: ["bytes"] }, subscribe },
107
- };
108
- const manager = new TransportManager();
109
- expect(await manager.connect(CONFIG)).toBe(true);
110
- expect(await manager.connect(CONFIG)).toBe(true);
111
- expect(unsubscribe).toHaveBeenCalledOnce();
112
- manager.disconnect();
113
- expect(unsubscribe).toHaveBeenCalledTimes(2);
114
- });
115
-
116
- it("releases a stale lease when connect calls overlap", async () => {
117
- let resolveSubscription!: (subscription: {
118
- addEventListener: ReturnType<typeof vi.fn>;
119
- removeEventListener: ReturnType<typeof vi.fn>;
120
- unsubscribe: ReturnType<typeof vi.fn>;
121
- }) => void;
122
- const unsubscribe = vi.fn();
123
- const subscribe = vi.fn().mockReturnValue(new Promise((resolve) => {
124
- resolveSubscription = resolve;
125
- }));
126
- (globalThis as unknown as { window?: unknown }).window = {
127
- multicast: { capabilities: { consumeModes: ["bytes"] }, subscribe },
128
- };
129
- const manager = new TransportManager();
130
- const first = manager.connect(CONFIG);
131
- await vi.waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
132
- const second = manager.connect(CONFIG);
133
- resolveSubscription({ addEventListener: vi.fn(), removeEventListener: vi.fn(), unsubscribe });
134
-
135
- expect(await Promise.all([first, second])).toEqual([false, true]);
136
- expect(subscribe).toHaveBeenCalledOnce();
137
- manager.disconnect();
138
- expect(unsubscribe).toHaveBeenCalledOnce();
139
- });
140
-
141
- it("does not publish a connection completed after disconnect", async () => {
142
- let resolveSubscription!: (subscription: {
143
- addEventListener: ReturnType<typeof vi.fn>;
144
- removeEventListener: ReturnType<typeof vi.fn>;
145
- unsubscribe: ReturnType<typeof vi.fn>;
146
- }) => void;
147
- const unsubscribe = vi.fn();
148
- const subscribe = vi.fn().mockReturnValue(new Promise((resolve) => {
149
- resolveSubscription = resolve;
150
- }));
151
- (globalThis as unknown as { window?: unknown }).window = {
152
- multicast: { capabilities: { consumeModes: ["bytes"] }, subscribe },
153
- };
154
- const manager = new TransportManager();
155
- const connecting = manager.connect(CONFIG);
156
- await vi.waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
157
- manager.disconnect();
158
- resolveSubscription({ addEventListener: vi.fn(), removeEventListener: vi.fn(), unsubscribe });
159
-
160
- expect(await connecting).toBe(false);
161
- expect(manager.getState().connected).toBe(false);
162
- expect(unsubscribe).toHaveBeenCalledOnce();
163
- });
164
-
165
- it("rejects invalid endpoints before AMT fallback", async () => {
166
- (globalThis as unknown as { window?: unknown }).window = {};
167
- const manager = new TransportManager();
168
- const onError = vi.fn();
169
- manager.setErrorCallback(onError);
170
- expect(await manager.connect({ ...CONFIG, group: "127.0.0.1", amtRelay: "relay.example" })).toBe(false);
171
- expect(onError).toHaveBeenCalledOnce();
172
- });
173
-
174
- it("reports disconnected after an invalid reconnect", async () => {
175
- const unsubscribe = vi.fn();
176
- const subscribe = vi.fn().mockResolvedValue({
177
- addEventListener: vi.fn(), removeEventListener: vi.fn(), unsubscribe,
178
- });
179
- (globalThis as unknown as { window?: unknown }).window = {
180
- multicast: { capabilities: { consumeModes: ["bytes"] }, subscribe },
181
- };
182
- const manager = new TransportManager();
183
- expect(await manager.connect(CONFIG)).toBe(true);
184
-
185
- expect(await manager.connect({ ...CONFIG, group: "127.0.0.1" })).toBe(false);
186
- expect(unsubscribe).toHaveBeenCalledOnce();
187
- expect(manager.getState()).toMatchObject({
188
- mode: "disconnected",
189
- connected: false,
190
- });
191
- });
192
- });