@lzpenguin/server 1.0.3 → 1.0.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.
- package/README.md +163 -2
- package/index.d.ts +72 -2
- package/index.js +3 -112
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,80 @@ server.init({
|
|
|
37
37
|
});
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## 新特性 🎉
|
|
41
|
+
|
|
42
|
+
### 同步优化(v2.0+)
|
|
43
|
+
|
|
44
|
+
我们对同步机制进行了全面优化,提供更可靠和高效的数据同步:
|
|
45
|
+
|
|
46
|
+
#### 1. **时间戳和序列号**
|
|
47
|
+
- ✅ 每条消息都包含服务器时间戳和序列号
|
|
48
|
+
- ✅ 自动检测消息丢失和延迟警告
|
|
49
|
+
- ✅ 确保数据按正确顺序处理
|
|
50
|
+
- ✅ 实时显示网络延迟和序列号状态
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
server.onData((data) => {
|
|
54
|
+
console.log('时间戳:', data.timestamp);
|
|
55
|
+
console.log('序列号:', data.sequence);
|
|
56
|
+
console.log('数据:', data.world);
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### 2. **操作确认机制**
|
|
61
|
+
- 支持回调确认每个更新操作
|
|
62
|
+
- 自动超时检测(5秒)
|
|
63
|
+
- 返回唯一操作ID用于追踪
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
server.update({
|
|
67
|
+
world: { score: 100 }
|
|
68
|
+
}, (error, result) => {
|
|
69
|
+
if (error) {
|
|
70
|
+
console.error('更新失败:', error);
|
|
71
|
+
} else {
|
|
72
|
+
console.log('更新已确认:', result);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### 3. **增量更新**
|
|
78
|
+
- 只发送变化的数据,节省带宽
|
|
79
|
+
- 支持数组追加操作
|
|
80
|
+
- 特别适合笔画等增量数据
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
server.updateDelta({
|
|
84
|
+
world: {
|
|
85
|
+
strokes: {
|
|
86
|
+
$append: [newStroke] // 只追加新笔画
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, (error, result) => {
|
|
90
|
+
console.log('增量更新完成');
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### 4. **自适应推送频率**
|
|
95
|
+
- 服务器根据活动情况动态调整推送频率
|
|
96
|
+
- 高活动:50ms 推送一次
|
|
97
|
+
- 中活动:100ms 推送一次
|
|
98
|
+
- 低活动:200ms 推送一次
|
|
99
|
+
- 无活动:500ms 推送一次
|
|
100
|
+
|
|
101
|
+
#### 5. **统计信息**
|
|
102
|
+
- 实时查看连接状态和同步统计
|
|
103
|
+
- 监控消息丢失和延迟
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const stats = server.getStats();
|
|
107
|
+
console.log('连接状态:', stats.connected);
|
|
108
|
+
console.log('本地序列号:', stats.localSequence);
|
|
109
|
+
console.log('最后接收序列号:', stats.lastReceivedSequence);
|
|
110
|
+
console.log('待确认操作数:', stats.pendingOperations);
|
|
111
|
+
console.log('更新次数:', stats.updateCount);
|
|
112
|
+
```
|
|
113
|
+
|
|
40
114
|
## API
|
|
41
115
|
|
|
42
116
|
### 构造函数
|
|
@@ -94,9 +168,20 @@ server.init({
|
|
|
94
168
|
部分更新数据(合并到现有数据,不替换)。
|
|
95
169
|
|
|
96
170
|
```javascript
|
|
97
|
-
//
|
|
171
|
+
// 基本更新(不带确认)
|
|
98
172
|
server.update({ world: { score: 200 } });
|
|
99
173
|
|
|
174
|
+
// 带确认回调的更新(推荐)
|
|
175
|
+
server.update({
|
|
176
|
+
world: { score: 200 }
|
|
177
|
+
}, (error, result) => {
|
|
178
|
+
if (error) {
|
|
179
|
+
console.error('更新失败:', error);
|
|
180
|
+
} else {
|
|
181
|
+
console.log('更新已确认');
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
100
185
|
// 更新玩家数据
|
|
101
186
|
server.update({
|
|
102
187
|
self: {
|
|
@@ -112,6 +197,12 @@ server.update({
|
|
|
112
197
|
});
|
|
113
198
|
```
|
|
114
199
|
|
|
200
|
+
**参数:**
|
|
201
|
+
- `updateData`: 要更新的数据对象
|
|
202
|
+
- `callback` (可选): 确认回调函数 `(error, result) => {}`
|
|
203
|
+
|
|
204
|
+
**返回值:** 操作ID(字符串),可用于追踪操作
|
|
205
|
+
|
|
115
206
|
**响应数据(通过 onData 接收):**
|
|
116
207
|
```json
|
|
117
208
|
{
|
|
@@ -144,9 +235,11 @@ unsubscribe();
|
|
|
144
235
|
**数据格式:**
|
|
145
236
|
```json
|
|
146
237
|
{
|
|
238
|
+
"timestamp": 1705123456789,
|
|
239
|
+
"sequence": 42,
|
|
147
240
|
"world": { "score": 200, "level": 1 },
|
|
148
241
|
"self": {
|
|
149
|
-
"public": { "name": "Player1", "position": { "x": 15,
|
|
242
|
+
"public": { "name": "Player1", "position": { "x": 15, y: 25 } },
|
|
150
243
|
"private": { "health": 90, "mana": 50 }
|
|
151
244
|
},
|
|
152
245
|
"players": [
|
|
@@ -156,6 +249,74 @@ unsubscribe();
|
|
|
156
249
|
}
|
|
157
250
|
```
|
|
158
251
|
|
|
252
|
+
**字段说明:**
|
|
253
|
+
- `timestamp`: 服务器时间戳(毫秒)
|
|
254
|
+
- `sequence`: 消息序列号(用于检测消息丢失)
|
|
255
|
+
- `world`: 世界数据
|
|
256
|
+
- `self`: 当前玩家数据
|
|
257
|
+
- `players`: 其他玩家数据
|
|
258
|
+
|
|
259
|
+
### 4. updateDelta() - 增量更新 ✨
|
|
260
|
+
|
|
261
|
+
只发送变化的数据,适合频繁更新的场景(如画板笔画)。
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
// 追加新笔画(不发送整个数组)
|
|
265
|
+
const newStroke = {
|
|
266
|
+
id: 'stroke_123',
|
|
267
|
+
points: [{ x: 0, y: 0 }, { x: 10, y: 10 }],
|
|
268
|
+
color: '#FF0000',
|
|
269
|
+
width: 2
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
server.updateDelta({
|
|
273
|
+
world: {
|
|
274
|
+
strokes: {
|
|
275
|
+
$append: [newStroke] // 使用 $append 标记表示追加
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}, (error, result) => {
|
|
279
|
+
if (!error) {
|
|
280
|
+
console.log('笔画已添加');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**参数:**
|
|
286
|
+
- `delta`: 增量数据对象(支持 `$append` 等特殊操作)
|
|
287
|
+
- `callback` (可选): 确认回调函数
|
|
288
|
+
|
|
289
|
+
**返回值:** 操作ID
|
|
290
|
+
|
|
291
|
+
### 5. getStats() - 获取统计信息 ✨
|
|
292
|
+
|
|
293
|
+
获取实时同步统计信息,用于监控和调试。
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
const stats = server.getStats();
|
|
297
|
+
console.log(stats);
|
|
298
|
+
// {
|
|
299
|
+
// connected: true,
|
|
300
|
+
// initialized: true,
|
|
301
|
+
// localSequence: 10,
|
|
302
|
+
// lastReceivedSequence: 8,
|
|
303
|
+
// lastReceivedTimestamp: 1705123456789,
|
|
304
|
+
// pendingOperations: 2,
|
|
305
|
+
// updateCount: 10,
|
|
306
|
+
// lastUpdateTime: 1705123456789
|
|
307
|
+
// }
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**返回值:**
|
|
311
|
+
- `connected`: 是否已连接
|
|
312
|
+
- `initialized`: 是否已初始化
|
|
313
|
+
- `localSequence`: 本地发送的序列号
|
|
314
|
+
- `lastReceivedSequence`: 最后接收的序列号
|
|
315
|
+
- `lastReceivedTimestamp`: 最后接收的时间戳
|
|
316
|
+
- `pendingOperations`: 待确认的操作数量
|
|
317
|
+
- `updateCount`: 总更新次数
|
|
318
|
+
- `lastUpdateTime`: 最后更新时间
|
|
319
|
+
|
|
159
320
|
**推送时机:** init 响应、定时推送(每 0.2 秒)、init/update 后立即推送
|
|
160
321
|
|
|
161
322
|
## 联机游戏示例
|
package/index.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export interface UpdateData {
|
|
|
54
54
|
* 服务器推送的数据
|
|
55
55
|
*/
|
|
56
56
|
export interface ServerData {
|
|
57
|
+
/** 服务器时间戳(毫秒) */
|
|
58
|
+
timestamp?: number;
|
|
59
|
+
/** 消息序列号 */
|
|
60
|
+
sequence?: number;
|
|
57
61
|
/** 世界数据 */
|
|
58
62
|
world: Record<string, any>;
|
|
59
63
|
/** 当前玩家数据 */
|
|
@@ -62,6 +66,43 @@ export interface ServerData {
|
|
|
62
66
|
players: Array<Record<string, any>>;
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 增量更新数据
|
|
71
|
+
*/
|
|
72
|
+
export interface DeltaData {
|
|
73
|
+
/** 世界数据(支持 $append 等特殊操作) */
|
|
74
|
+
world?: Record<string, any>;
|
|
75
|
+
/** 玩家数据 */
|
|
76
|
+
self?: PlayerSelfData;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 统计信息
|
|
81
|
+
*/
|
|
82
|
+
export interface Stats {
|
|
83
|
+
/** 是否已连接 */
|
|
84
|
+
connected: boolean;
|
|
85
|
+
/** 是否已初始化 */
|
|
86
|
+
initialized: boolean;
|
|
87
|
+
/** 本地发送的序列号 */
|
|
88
|
+
localSequence: number;
|
|
89
|
+
/** 最后接收的序列号 */
|
|
90
|
+
lastReceivedSequence: number;
|
|
91
|
+
/** 最后接收的时间戳 */
|
|
92
|
+
lastReceivedTimestamp: number;
|
|
93
|
+
/** 待确认的操作数量 */
|
|
94
|
+
pendingOperations: number;
|
|
95
|
+
/** 总更新次数 */
|
|
96
|
+
updateCount: number;
|
|
97
|
+
/** 最后更新时间 */
|
|
98
|
+
lastUpdateTime: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 确认回调函数
|
|
103
|
+
*/
|
|
104
|
+
export type ConfirmCallback = (error: Error | null, result: { confirmed: boolean } | null) => void;
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* RiffleServer - 游戏服务器 WebSocket 客户端
|
|
67
108
|
*/
|
|
@@ -82,14 +123,43 @@ export class RiffleServer {
|
|
|
82
123
|
/**
|
|
83
124
|
* 初始化服务器(必需,连接后必须先调用)
|
|
84
125
|
* @param initData 初始化数据
|
|
126
|
+
* @param callback 确认回调函数(可选)
|
|
127
|
+
* @returns 操作ID(如果已连接)
|
|
85
128
|
*/
|
|
86
|
-
init(initData: InitData):
|
|
129
|
+
init(initData: InitData, callback?: ConfirmCallback): string | undefined;
|
|
87
130
|
|
|
88
131
|
/**
|
|
89
132
|
* 更新数据(部分更新,合并到现有数据)
|
|
90
133
|
* @param updateData 更新数据
|
|
134
|
+
* @param callback 确认回调函数(可选)
|
|
135
|
+
* @returns 操作ID
|
|
136
|
+
*/
|
|
137
|
+
update(updateData: UpdateData, callback?: ConfirmCallback): string | null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 增量更新(只发送变化的数据)
|
|
141
|
+
* @param delta 增量数据
|
|
142
|
+
* @param callback 确认回调函数(可选)
|
|
143
|
+
* @returns 操作ID
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // 只追加新笔画,而不是发送整个数组
|
|
147
|
+
* server.updateDelta({
|
|
148
|
+
* world: {
|
|
149
|
+
* strokes: {
|
|
150
|
+
* $append: [newStroke]
|
|
151
|
+
* }
|
|
152
|
+
* }
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
updateDelta(delta: DeltaData, callback?: ConfirmCallback): string | null;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 获取同步统计信息
|
|
160
|
+
* @returns 统计信息对象
|
|
91
161
|
*/
|
|
92
|
-
|
|
162
|
+
getStats(): Stats;
|
|
93
163
|
|
|
94
164
|
/**
|
|
95
165
|
* 监听服务器推送的最新数据
|
package/index.js
CHANGED
|
@@ -177,37 +177,9 @@ export class RiffleServer {
|
|
|
177
177
|
console.log('[RiffleServer] Initialized');
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// 对于 self 数据:优先使用服务器数据(因为服务器会合并所有更新)
|
|
184
|
-
// 但对于 world 数据:需要合并,因为可能有其他玩家的更新
|
|
185
|
-
const mergedData = {
|
|
186
|
-
// world 数据:合并服务器数据和本地缓存,确保不丢失任何更新
|
|
187
|
-
world: this._deepMerge(this.currentData.world || {}, message.world),
|
|
188
|
-
// self 数据:服务器数据是权威的(已包含所有更新),但合并确保不丢失字段
|
|
189
|
-
self: {
|
|
190
|
-
public: this._deepMerge(
|
|
191
|
-
message.self?.public || {},
|
|
192
|
-
this.currentData.self?.public || {}
|
|
193
|
-
),
|
|
194
|
-
private: this._deepMerge(
|
|
195
|
-
message.self?.private || {},
|
|
196
|
-
this.currentData.self?.private || {}
|
|
197
|
-
)
|
|
198
|
-
},
|
|
199
|
-
// players 列表使用服务器数据(其他玩家的数据)
|
|
200
|
-
players: message.players
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// 使用合并后的数据
|
|
204
|
-
this.currentData = mergedData;
|
|
205
|
-
this.dataCallbacks.forEach(cb => cb(mergedData));
|
|
206
|
-
} else {
|
|
207
|
-
// 如果没有缓存数据,直接使用服务器数据
|
|
208
|
-
this.currentData = message;
|
|
209
|
-
this.dataCallbacks.forEach(cb => cb(message));
|
|
210
|
-
}
|
|
180
|
+
// 更新缓存并触发数据回调
|
|
181
|
+
this.currentData = message;
|
|
182
|
+
this.dataCallbacks.forEach(cb => cb(message));
|
|
211
183
|
}
|
|
212
184
|
} catch (error) {
|
|
213
185
|
console.error('[RiffleServer] Failed to parse message:', error);
|
|
@@ -268,55 +240,6 @@ export class RiffleServer {
|
|
|
268
240
|
this.isConnected = false;
|
|
269
241
|
}
|
|
270
242
|
|
|
271
|
-
/**
|
|
272
|
-
* 深度合并对象
|
|
273
|
-
* @private
|
|
274
|
-
* @param {Object} target - 目标对象(保留原有数据)
|
|
275
|
-
* @param {Object} source - 源对象(新数据,优先使用)
|
|
276
|
-
* @returns {Object} 合并后的对象
|
|
277
|
-
*/
|
|
278
|
-
_deepMerge(target, source) {
|
|
279
|
-
if (!target) return source ? (Array.isArray(source) ? [...source] : { ...source }) : {};
|
|
280
|
-
if (!source) return target;
|
|
281
|
-
|
|
282
|
-
// 处理数组:如果都是数组,合并去重(基于 JSON 字符串比较)
|
|
283
|
-
if (Array.isArray(target) && Array.isArray(source)) {
|
|
284
|
-
const merged = [...target];
|
|
285
|
-
const targetStrings = new Set(target.map(item => JSON.stringify(item)));
|
|
286
|
-
for (const item of source) {
|
|
287
|
-
const itemStr = JSON.stringify(item);
|
|
288
|
-
if (!targetStrings.has(itemStr)) {
|
|
289
|
-
merged.push(item);
|
|
290
|
-
targetStrings.add(itemStr);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return merged;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// 如果类型不匹配,使用源数据
|
|
297
|
-
if (Array.isArray(target) || Array.isArray(source)) {
|
|
298
|
-
return Array.isArray(source) ? [...source] : source;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 处理对象:递归合并
|
|
302
|
-
const result = { ...target };
|
|
303
|
-
for (const key in source) {
|
|
304
|
-
if (source.hasOwnProperty(key)) {
|
|
305
|
-
const sourceValue = source[key];
|
|
306
|
-
const targetValue = target[key];
|
|
307
|
-
|
|
308
|
-
// 如果源值是对象且不是数组,递归合并
|
|
309
|
-
if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
|
|
310
|
-
result[key] = this._deepMerge(targetValue, sourceValue);
|
|
311
|
-
} else {
|
|
312
|
-
// 否则使用源值(优先使用新数据)
|
|
313
|
-
result[key] = sourceValue;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return result;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
243
|
/**
|
|
321
244
|
* 发送更新请求
|
|
322
245
|
* @private
|
|
@@ -333,38 +256,6 @@ export class RiffleServer {
|
|
|
333
256
|
return;
|
|
334
257
|
}
|
|
335
258
|
|
|
336
|
-
// 乐观更新:立即更新本地缓存并触发回调
|
|
337
|
-
if (this.currentData) {
|
|
338
|
-
const optimisticData = { ...this.currentData };
|
|
339
|
-
|
|
340
|
-
// 合并 world 数据
|
|
341
|
-
if (updateData.world) {
|
|
342
|
-
optimisticData.world = this._deepMerge(this.currentData.world || {}, updateData.world);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 合并 self 数据
|
|
346
|
-
if (updateData.self) {
|
|
347
|
-
optimisticData.self = { ...this.currentData.self };
|
|
348
|
-
if (updateData.self.public) {
|
|
349
|
-
optimisticData.self.public = this._deepMerge(
|
|
350
|
-
this.currentData.self?.public || {},
|
|
351
|
-
updateData.self.public
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
if (updateData.self.private) {
|
|
355
|
-
optimisticData.self.private = this._deepMerge(
|
|
356
|
-
this.currentData.self?.private || {},
|
|
357
|
-
updateData.self.private
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 更新缓存并立即触发回调(乐观更新)
|
|
363
|
-
this.currentData = optimisticData;
|
|
364
|
-
this.dataCallbacks.forEach(cb => cb(optimisticData));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 发送更新请求到服务器
|
|
368
259
|
try {
|
|
369
260
|
this.ws.send(JSON.stringify(updateData));
|
|
370
261
|
} catch (error) {
|