@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.
- package/index.js +187 -21
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
-
|
|
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 ===
|
|
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
|
|
143
|
+
this.ws = new WebSocketImpl(wsUrl);
|
|
103
144
|
|
|
104
|
-
|
|
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
|
-
|
|
160
|
+
const handleMessage = (event) => {
|
|
119
161
|
try {
|
|
120
|
-
|
|
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
|
-
|
|
138
|
-
this.
|
|
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
|
-
|
|
217
|
+
const handleError = (error) => {
|
|
146
218
|
console.error('[RiffleServer] WebSocket error:', error);
|
|
147
|
-
}
|
|
219
|
+
};
|
|
148
220
|
|
|
149
|
-
|
|
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 !==
|
|
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 !==
|
|
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;
|