webrtc-tree 1.0.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.
@@ -0,0 +1,153 @@
1
+ export interface RTCTreeNode {
2
+ children: string[];
3
+ parent: string | null;
4
+ layer: number;
5
+ }
6
+
7
+ export interface RoomConfig {
8
+ maxNodesPerLayer: number[]; // e.g. [1, 4, 8, 16, 64]
9
+ }
10
+
11
+ export class RTCTreeCoordinator {
12
+ // roomId -> peerId -> Node Data
13
+ private trees: Record<string, Record<string, RTCTreeNode>> = {};
14
+ private configs: Record<string, RoomConfig> = {};
15
+
16
+ /**
17
+ * 建立一個新的直播房間拓撲結構
18
+ * @param roomId 房間 ID
19
+ * @param streamerPeerId 直播主的 Peer ID
20
+ * @param config 拓撲設定,例如 maxNodesPerLayer: [1, 4, 8, 16, 64]
21
+ */
22
+ public createRoom(roomId: string, streamerPeerId: string, config: RoomConfig): void {
23
+ this.configs[roomId] = config;
24
+ this.trees[roomId] = {};
25
+ // Streamer is at layer 0
26
+ this.trees[roomId][streamerPeerId] = { children: [], parent: null, layer: 0 };
27
+ }
28
+
29
+ /**
30
+ * 新節點加入,透過 BFS 分配最合適的父節點
31
+ * @param roomId 房間 ID
32
+ * @param newPeerId 新節點的 Peer ID
33
+ * @returns 分配到的父節點 Peer ID,若無法分配則回傳 null
34
+ */
35
+ public joinNode(roomId: string, newPeerId: string): string | null {
36
+ const tree = this.trees[roomId];
37
+ const config = this.configs[roomId];
38
+ if (!tree || !config) return null;
39
+
40
+ // 尋找樹根 (layer 0 的節點,通常是 streamer)
41
+ let rootId: string | null = null;
42
+ for (const [id, node] of Object.entries(tree)) {
43
+ if (node.layer === 0) {
44
+ rootId = id;
45
+ break;
46
+ }
47
+ }
48
+
49
+ if (!rootId) return null;
50
+
51
+ // 如果該節點已經在樹中,先將其從原本的位置移除 (防止重複加入或狀態不一致)
52
+ if (tree[newPeerId]) {
53
+ this.removeNode(roomId, newPeerId);
54
+ }
55
+
56
+ // 計算目前每一層的總節點數
57
+ const layerCounts: Record<number, number> = {};
58
+ for (const node of Object.values(tree)) {
59
+ layerCounts[node.layer] = (layerCounts[node.layer] || 0) + 1;
60
+ }
61
+
62
+ // BFS 尋找未滿載的節點
63
+ const queue: string[] = [rootId];
64
+
65
+ while (queue.length > 0) {
66
+ const currentId = queue.shift()!;
67
+ const currentNode = tree[currentId];
68
+ const currentLayer = currentNode.layer;
69
+
70
+ // 下一層的索引
71
+ const nextLayer = currentLayer + 1;
72
+
73
+ // 如果已經達到最大層數設定,該節點不能再有子節點
74
+ if (nextLayer >= config.maxNodesPerLayer.length) {
75
+ // Continue searching in queue
76
+ queue.push(...currentNode.children);
77
+ continue;
78
+ }
79
+
80
+ // 檢查下一層是否已經達到整體上限
81
+ const nextLayerMax = config.maxNodesPerLayer[nextLayer];
82
+ const currentNextLayerCount = layerCounts[nextLayer] || 0;
83
+
84
+ if (currentNextLayerCount >= nextLayerMax) {
85
+ // 下一層已經滿了,把目前節點的子節點加入 queue 繼續尋找更下層
86
+ queue.push(...currentNode.children);
87
+ continue;
88
+ }
89
+
90
+ // 如果下一層還沒滿,那我們要決定「目前這個節點」還能不能接客
91
+ // 算法:該層平均每個節點可以接的子節點數
92
+ // 例如 layer 1 最大 4 人,layer 2 最大 8 人,代表 layer 1 每個節點最多接 8/4 = 2 人
93
+ const currentLayerMax = config.maxNodesPerLayer[currentLayer];
94
+ const maxChildrenPerNode = Math.floor(nextLayerMax / currentLayerMax) || 1;
95
+
96
+ if (currentNode.children.length < maxChildrenPerNode) {
97
+ // 找到可以接客的節點了!
98
+ currentNode.children.push(newPeerId);
99
+ tree[newPeerId] = {
100
+ children: [],
101
+ parent: currentId,
102
+ layer: nextLayer
103
+ };
104
+ return currentId;
105
+ }
106
+
107
+ // 目前節點滿了,把它的小孩加進 queue
108
+ queue.push(...currentNode.children);
109
+ }
110
+
111
+ return null; // 樹已滿或無法分配
112
+ }
113
+
114
+ /**
115
+ * 節點斷線,將其從樹中移除,並讓其子節點變成孤兒 (等待重新 join)
116
+ */
117
+ public removeNode(roomId: string, deadPeerId: string): void {
118
+ const tree = this.trees[roomId];
119
+ if (!tree) return;
120
+
121
+ const deadNode = tree[deadPeerId];
122
+ if (!deadNode) return;
123
+
124
+ // 將自己從父節點的 children 中移除
125
+ if (deadNode.parent && tree[deadNode.parent]) {
126
+ const parentNode = tree[deadNode.parent];
127
+ parentNode.children = parentNode.children.filter(id => id !== deadPeerId);
128
+ }
129
+
130
+ // 將子節點的 parent 設為 null (它們需要重新連線)
131
+ for (const childId of deadNode.children) {
132
+ if (tree[childId]) {
133
+ tree[childId].parent = null;
134
+ }
135
+ }
136
+
137
+ delete tree[deadPeerId];
138
+ }
139
+
140
+ /**
141
+ * 報告節點失效 (Self-Healing 觸發點)
142
+ */
143
+ public reportDeadNode(roomId: string, deadPeerId: string): void {
144
+ this.removeNode(roomId, deadPeerId);
145
+ }
146
+
147
+ /**
148
+ * 取得房間目前的樹狀結構 (For Debug/Monitor)
149
+ */
150
+ public getTree(roomId: string): Record<string, RTCTreeNode> | null {
151
+ return this.trees[roomId] || null;
152
+ }
153
+ }
@@ -0,0 +1 @@
1
+ export * from './RTCTreeCoordinator';
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "ignoreDeprecations": "5.0",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "rootDir": "src",
12
+ "outDir": "dist",
13
+ "declaration": true,
14
+ "types": ["jest", "node", "peerjs"]
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/server/index.ts', 'src/client/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: false,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });