@lzpenguin/server 1.0.4 → 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.
Files changed (4) hide show
  1. package/README.md +163 -2
  2. package/index.d.ts +72 -2
  3. package/index.js +3 -117
  4. 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, "y": 25 } },
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): void;
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
- update(updateData: UpdateData): void;
162
+ getStats(): Stats;
93
163
 
94
164
  /**
95
165
  * 监听服务器推送的最新数据
package/index.js CHANGED
@@ -177,39 +177,9 @@ export class RiffleServer {
177
177
  console.log('[RiffleServer] Initialized');
178
178
  }
179
179
 
180
- // 合并服务器数据与当前缓存,避免丢失乐观更新的数据
181
- // 策略:对于 world 数据,优先保留乐观更新(当前缓存),然后合并服务器数据
182
- // 这样可以确保刚画的笔画不会因为服务器推送旧数据而丢失
183
- if (this.currentData) {
184
- const mergedData = {
185
- // world 数据:先使用当前缓存(包含乐观更新),再合并服务器数据
186
- // _deepMerge 会先保留 target(当前缓存)的所有数据,然后添加 source(服务器数据)中不存在的项
187
- // 对于数组(如 strokes),会合并去重,确保不会丢失任何笔画
188
- world: this._deepMerge(this.currentData.world || {}, message.world || {}),
189
- // self 数据:服务器数据优先(因为服务器会合并所有玩家的更新)
190
- // 但也要合并当前缓存,确保不丢失字段
191
- self: {
192
- public: this._deepMerge(
193
- message.self?.public || {},
194
- this.currentData.self?.public || {}
195
- ),
196
- private: this._deepMerge(
197
- message.self?.private || {},
198
- this.currentData.self?.private || {}
199
- )
200
- },
201
- // players 列表使用服务器数据(其他玩家的数据)
202
- players: message.players
203
- };
204
-
205
- // 使用合并后的数据
206
- this.currentData = mergedData;
207
- this.dataCallbacks.forEach(cb => cb(mergedData));
208
- } else {
209
- // 如果没有缓存数据,直接使用服务器数据
210
- this.currentData = message;
211
- this.dataCallbacks.forEach(cb => cb(message));
212
- }
180
+ // 更新缓存并触发数据回调
181
+ this.currentData = message;
182
+ this.dataCallbacks.forEach(cb => cb(message));
213
183
  }
214
184
  } catch (error) {
215
185
  console.error('[RiffleServer] Failed to parse message:', error);
@@ -270,58 +240,6 @@ export class RiffleServer {
270
240
  this.isConnected = false;
271
241
  }
272
242
 
273
- /**
274
- * 深度合并对象
275
- * @private
276
- * @param {Object} target - 目标对象(基础数据)
277
- * @param {Object} source - 源对象(新数据,优先使用)
278
- * @returns {Object} 合并后的对象
279
- */
280
- _deepMerge(target, source) {
281
- if (!target) return source ? (Array.isArray(source) ? [...source] : { ...source }) : {};
282
- if (!source) return target;
283
-
284
- // 处理数组:如果都是数组,合并去重(基于 JSON 字符串比较)
285
- // 先保留 target 的所有项,然后添加 source 中不存在的项
286
- // 这样可以确保不会丢失任何数据
287
- if (Array.isArray(target) && Array.isArray(source)) {
288
- const merged = [...target];
289
- const targetStrings = new Set(target.map(item => JSON.stringify(item)));
290
- for (const item of source) {
291
- const itemStr = JSON.stringify(item);
292
- if (!targetStrings.has(itemStr)) {
293
- merged.push(item);
294
- targetStrings.add(itemStr);
295
- }
296
- }
297
- return merged;
298
- }
299
-
300
- // 如果类型不匹配,使用源数据
301
- if (Array.isArray(target) || Array.isArray(source)) {
302
- return Array.isArray(source) ? [...source] : source;
303
- }
304
-
305
- // 处理对象:递归合并
306
- // 先复制 target,然后用 source 的值覆盖(但递归合并子对象)
307
- const result = { ...target };
308
- for (const key in source) {
309
- if (source.hasOwnProperty(key)) {
310
- const sourceValue = source[key];
311
- const targetValue = target[key];
312
-
313
- // 如果源值是对象且不是数组,递归合并
314
- if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
315
- result[key] = this._deepMerge(targetValue, sourceValue);
316
- } else {
317
- // 否则使用源值(优先使用新数据)
318
- result[key] = sourceValue;
319
- }
320
- }
321
- }
322
- return result;
323
- }
324
-
325
243
  /**
326
244
  * 发送更新请求
327
245
  * @private
@@ -338,38 +256,6 @@ export class RiffleServer {
338
256
  return;
339
257
  }
340
258
 
341
- // 乐观更新:立即更新本地缓存并触发回调
342
- if (this.currentData) {
343
- const optimisticData = { ...this.currentData };
344
-
345
- // 合并 world 数据
346
- if (updateData.world) {
347
- optimisticData.world = this._deepMerge(this.currentData.world || {}, updateData.world);
348
- }
349
-
350
- // 合并 self 数据
351
- if (updateData.self) {
352
- optimisticData.self = { ...this.currentData.self };
353
- if (updateData.self.public) {
354
- optimisticData.self.public = this._deepMerge(
355
- this.currentData.self?.public || {},
356
- updateData.self.public
357
- );
358
- }
359
- if (updateData.self.private) {
360
- optimisticData.self.private = this._deepMerge(
361
- this.currentData.self?.private || {},
362
- updateData.self.private
363
- );
364
- }
365
- }
366
-
367
- // 更新缓存并立即触发回调(乐观更新)
368
- this.currentData = optimisticData;
369
- this.dataCallbacks.forEach(cb => cb(optimisticData));
370
- }
371
-
372
- // 发送更新请求到服务器
373
259
  try {
374
260
  this.ws.send(JSON.stringify(updateData));
375
261
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzpenguin/server",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Riffle 游戏服务器 WebSocket 客户端 SDK",
5
5
  "license": "ISC",
6
6
  "author": "lzpenguin",