@lzpenguin/server 1.0.1 → 1.0.3

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 (2) hide show
  1. package/index.js +187 -21
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,4 +1,32 @@
1
- import WebSocket from 'ws';
1
+ // 检测运行环境,浏览器使用原生 WebSocket,Node.js 使用 ws
2
+ let WebSocketImpl;
3
+ let isBrowser = false;
4
+
5
+ if (typeof window !== 'undefined' && window.WebSocket) {
6
+ // 浏览器环境
7
+ WebSocketImpl = window.WebSocket;
8
+ isBrowser = true;
9
+ } else {
10
+ // Node.js 环境 - 使用动态导入
11
+ try {
12
+ // 使用 require 或 import,根据环境选择
13
+ if (typeof require !== 'undefined') {
14
+ // CommonJS 环境
15
+ WebSocketImpl = require('ws');
16
+ } else {
17
+ // ES Module 环境 - 在运行时动态导入
18
+ // 注意:这需要在异步上下文中使用
19
+ WebSocketImpl = null; // 将在 connect 方法中动态导入
20
+ }
21
+ } catch (e) {
22
+ // 如果无法导入 ws,尝试使用全局 WebSocket(某些打包工具可能会提供)
23
+ if (typeof WebSocket !== 'undefined') {
24
+ WebSocketImpl = WebSocket;
25
+ } else {
26
+ throw new Error('WebSocket is not available. In Node.js, please install "ws" package.');
27
+ }
28
+ }
29
+ }
2
30
 
3
31
  /**
4
32
  * RiffleServer - 游戏服务器 WebSocket 客户端
@@ -79,29 +107,43 @@ export class RiffleServer {
79
107
  // 当前服务器数据缓存
80
108
  this.currentData = null;
81
109
 
82
- // 连接状态
83
- this.connect();
110
+ // 连接状态(异步调用,不阻塞构造函数)
111
+ this.connect().catch(err => {
112
+ console.error('[RiffleServer] Initial connection failed:', err);
113
+ });
84
114
  }
85
115
 
86
116
  /**
87
117
  * 连接到 WebSocket 服务器
88
118
  * @private
89
119
  */
90
- connect() {
91
- if (this.isConnecting || (this.isConnected && this.ws?.readyState === WebSocket.OPEN)) {
120
+ async connect() {
121
+ if (this.isConnecting || (this.isConnected && this.ws?.readyState === (WebSocketImpl?.OPEN ?? 1))) {
92
122
  return;
93
123
  }
94
124
 
95
125
  this.isConnecting = true;
96
126
 
127
+ // 如果在 Node.js 环境且 WebSocketImpl 未初始化,动态导入
128
+ if (!isBrowser && !WebSocketImpl) {
129
+ try {
130
+ const wsModule = await import('ws');
131
+ WebSocketImpl = wsModule.default;
132
+ } catch (e) {
133
+ this.isConnecting = false;
134
+ console.error('[RiffleServer] Failed to import ws module:', e);
135
+ throw new Error('WebSocket is not available. In Node.js, please install "ws" package.');
136
+ }
137
+ }
138
+
97
139
  // 构建 WebSocket URL(包含 post_id 和 token)
98
140
  let wsUrl = `${this.url}?post_id=${this.postId}&token=${this.token}`;
99
141
 
100
-
101
142
  try {
102
- this.ws = new WebSocket(wsUrl);
143
+ this.ws = new WebSocketImpl(wsUrl);
103
144
 
104
- this.ws.on('open', () => {
145
+ // 统一事件处理:浏览器用 addEventListener,Node.js 用 .on()
146
+ const handleOpen = () => {
105
147
  this.isConnected = true;
106
148
  this.isConnecting = false;
107
149
  this.isInitialized = false; // 连接后重置初始化状态
@@ -113,11 +155,13 @@ export class RiffleServer {
113
155
  this.pendingInit = null;
114
156
  this._doInit(initData);
115
157
  }
116
- });
158
+ };
117
159
 
118
- this.ws.on('message', (data) => {
160
+ const handleMessage = (event) => {
119
161
  try {
120
- const message = JSON.parse(data.toString());
162
+ // 浏览器: event.data, Node.js: event (Buffer 或 string)
163
+ const data = event.data || event;
164
+ const message = JSON.parse(typeof data === 'string' ? data : data.toString());
121
165
 
122
166
  // 检查是否是错误消息
123
167
  if (message.error) {
@@ -133,20 +177,48 @@ export class RiffleServer {
133
177
  console.log('[RiffleServer] Initialized');
134
178
  }
135
179
 
136
- // 更新缓存并触发数据回调
137
- this.currentData = message;
138
- this.dataCallbacks.forEach(cb => cb(message));
180
+ // 合并服务器数据与当前缓存,避免丢失乐观更新的数据
181
+ // 策略:服务器数据是权威数据,但需要智能合并以避免丢失本地更新
182
+ if (this.currentData) {
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
+ }
139
211
  }
140
212
  } catch (error) {
141
213
  console.error('[RiffleServer] Failed to parse message:', error);
142
214
  }
143
- });
215
+ };
144
216
 
145
- this.ws.on('error', (error) => {
217
+ const handleError = (error) => {
146
218
  console.error('[RiffleServer] WebSocket error:', error);
147
- });
219
+ };
148
220
 
149
- this.ws.on('close', () => {
221
+ const handleClose = () => {
150
222
  this.isConnected = false;
151
223
  this.isConnecting = false;
152
224
  this.isInitialized = false; // 断开连接后重置初始化状态
@@ -159,7 +231,20 @@ export class RiffleServer {
159
231
  this.connect();
160
232
  }, this.reconnectInterval);
161
233
  }
162
- });
234
+ };
235
+
236
+ // 根据环境使用不同的事件绑定方式
237
+ if (isBrowser) {
238
+ this.ws.addEventListener('open', handleOpen);
239
+ this.ws.addEventListener('message', handleMessage);
240
+ this.ws.addEventListener('error', handleError);
241
+ this.ws.addEventListener('close', handleClose);
242
+ } else {
243
+ this.ws.on('open', handleOpen);
244
+ this.ws.on('message', handleMessage);
245
+ this.ws.on('error', handleError);
246
+ this.ws.on('close', handleClose);
247
+ }
163
248
  } catch (error) {
164
249
  this.isConnecting = false;
165
250
  console.error('[RiffleServer] Connection error:', error);
@@ -183,13 +268,62 @@ export class RiffleServer {
183
268
  this.isConnected = false;
184
269
  }
185
270
 
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
+
186
320
  /**
187
321
  * 发送更新请求
188
322
  * @private
189
323
  * @param {Object} updateData - 更新数据
190
324
  */
191
325
  sendUpdate(updateData) {
192
- if (!this.isConnected || this.ws?.readyState !== WebSocket.OPEN) {
326
+ if (!this.isConnected || this.ws?.readyState !== WebSocketImpl.OPEN) {
193
327
  console.warn('[RiffleServer] Not connected, cannot send update');
194
328
  return;
195
329
  }
@@ -199,6 +333,38 @@ export class RiffleServer {
199
333
  return;
200
334
  }
201
335
 
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
+ // 发送更新请求到服务器
202
368
  try {
203
369
  this.ws.send(JSON.stringify(updateData));
204
370
  } catch (error) {
@@ -231,7 +397,7 @@ export class RiffleServer {
231
397
  }
232
398
 
233
399
  // 如果还未连接,保存 init 请求,连接建立后自动执行
234
- if (!this.isConnected || this.ws?.readyState !== WebSocket.OPEN) {
400
+ if (!this.isConnected || this.ws?.readyState !== WebSocketImpl.OPEN) {
235
401
  console.log('[RiffleServer] Not connected yet, will init after connection');
236
402
  this.pendingInit = initData;
237
403
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzpenguin/server",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Riffle 游戏服务器 WebSocket 客户端 SDK",
5
5
  "license": "ISC",
6
6
  "author": "lzpenguin",