action-engine-js 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.
- package/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- package/package.json +35 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncSystem - Generic client-to-client state synchronization
|
|
3
|
+
*
|
|
4
|
+
* A transport-agnostic, event-driven synchronization primitive for multiplayer games.
|
|
5
|
+
*
|
|
6
|
+
* DESIGN PRINCIPLES:
|
|
7
|
+
* - Generic: Works with any data, any transport, any game
|
|
8
|
+
* - Registry-based: Register multiple sync sources by ID
|
|
9
|
+
* - Event-driven: Emit events instead of tight coupling
|
|
10
|
+
* - Self-healing: Periodic broadcasts mean dropped messages don't matter
|
|
11
|
+
* - Observable: Track liveness and staleness of remote data
|
|
12
|
+
*
|
|
13
|
+
* USAGE EXAMPLE:
|
|
14
|
+
* ```javascript
|
|
15
|
+
* // Create sync system with transport function
|
|
16
|
+
* const sync = new SyncSystem({
|
|
17
|
+
* send: (msg) => networkManager.send(msg),
|
|
18
|
+
* broadcastInterval: 16, // 60fps
|
|
19
|
+
* staleThreshold: 200 // 12 frames
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Register sync sources
|
|
23
|
+
* sync.register('match', {
|
|
24
|
+
* getFields: () => ({
|
|
25
|
+
* state: matchStateMachine.getState(),
|
|
26
|
+
* ready: isReady
|
|
27
|
+
* })
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* sync.register('player', {
|
|
31
|
+
* getFields: () => ({
|
|
32
|
+
* score: player.score,
|
|
33
|
+
* level: player.level,
|
|
34
|
+
* alive: !player.gameOver
|
|
35
|
+
* })
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Listen for remote updates
|
|
39
|
+
* sync.on('remoteUpdated', (allRemoteData) => {
|
|
40
|
+
* console.log('Remote data changed:', allRemoteData);
|
|
41
|
+
* checkGameConditions();
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* sync.on('remoteStale', () => {
|
|
45
|
+
* console.warn('Remote client stopped responding');
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Start syncing
|
|
49
|
+
* sync.start();
|
|
50
|
+
*
|
|
51
|
+
* // Query remote data
|
|
52
|
+
* const remoteMatch = sync.getRemote('match');
|
|
53
|
+
* if (remoteMatch && remoteMatch.ready) {
|
|
54
|
+
* startGame();
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* // Stop syncing
|
|
58
|
+
* sync.stop();
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
class SyncSystem {
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
// Transport function (how to send messages)
|
|
64
|
+
this.sendFunction = options.send || null;
|
|
65
|
+
|
|
66
|
+
// Configuration
|
|
67
|
+
this.broadcastInterval = options.broadcastInterval || 16; // 16ms = ~60fps
|
|
68
|
+
this.staleThreshold = options.staleThreshold || 200; // 200ms = ~12 frames
|
|
69
|
+
|
|
70
|
+
// Registry of sync sources
|
|
71
|
+
this.syncSources = new Map(); // id -> source object with getFields()
|
|
72
|
+
|
|
73
|
+
// Local data (what we broadcast)
|
|
74
|
+
this.localData = {};
|
|
75
|
+
|
|
76
|
+
// Remote data (what we receive)
|
|
77
|
+
this.remoteData = {};
|
|
78
|
+
this.lastRemoteUpdate = 0;
|
|
79
|
+
this.wasStale = false;
|
|
80
|
+
|
|
81
|
+
// Event listeners
|
|
82
|
+
this.listeners = new Map();
|
|
83
|
+
|
|
84
|
+
// Broadcast timer
|
|
85
|
+
this.broadcastTimer = null;
|
|
86
|
+
this.isRunning = false;
|
|
87
|
+
|
|
88
|
+
// console.log('[SyncSystem] Initialized', {
|
|
89
|
+
// broadcastInterval: this.broadcastInterval,
|
|
90
|
+
// staleThreshold: this.staleThreshold
|
|
91
|
+
// });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Update the send function (transport pipeline)
|
|
96
|
+
* Call this when transport conditions change (e.g., new dataChannel available)
|
|
97
|
+
*
|
|
98
|
+
* @param {Function} sendFunction - New send function
|
|
99
|
+
*/
|
|
100
|
+
setSendFunction(sendFunction) {
|
|
101
|
+
if (typeof sendFunction !== 'function') {
|
|
102
|
+
console.error('[SyncSystem] setSendFunction requires a function');
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.sendFunction = sendFunction;
|
|
107
|
+
// console.log('[SyncSystem] Updated send function');
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Register a sync source
|
|
113
|
+
*
|
|
114
|
+
* @param {String} id - Unique identifier for this sync source
|
|
115
|
+
* @param {Object} source - Object with getFields() method that returns data to sync
|
|
116
|
+
*
|
|
117
|
+
* Example:
|
|
118
|
+
* sync.register('player', {
|
|
119
|
+
* getFields: () => ({ score: player.score, level: player.level })
|
|
120
|
+
* });
|
|
121
|
+
*/
|
|
122
|
+
register(id, source) {
|
|
123
|
+
if (!source || typeof source.getFields !== 'function') {
|
|
124
|
+
console.error(`[SyncSystem] Source '${id}' must have a getFields() method`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.syncSources.set(id, source);
|
|
129
|
+
// console.log(`[SyncSystem] Registered sync source: '${id}'`);
|
|
130
|
+
|
|
131
|
+
// Immediately update local data for this source
|
|
132
|
+
this._updateLocalDataForSource(id);
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Unregister a sync source
|
|
139
|
+
*
|
|
140
|
+
* @param {String} id - ID of source to remove
|
|
141
|
+
*/
|
|
142
|
+
unregister(id) {
|
|
143
|
+
if (this.syncSources.delete(id)) {
|
|
144
|
+
delete this.localData[id];
|
|
145
|
+
// console.log(`[SyncSystem] Unregistered sync source: '${id}'`);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Register an event listener
|
|
153
|
+
*
|
|
154
|
+
* Available events:
|
|
155
|
+
* - 'remoteUpdated': (remoteData) => {} - Remote data received
|
|
156
|
+
* - 'remoteStale': () => {} - Remote stopped sending updates
|
|
157
|
+
* - 'remoteFresh': () => {} - Remote resumed sending after being stale
|
|
158
|
+
* - 'broadcast': (localData) => {} - We broadcasted data
|
|
159
|
+
*
|
|
160
|
+
* @param {String} event - Event name
|
|
161
|
+
* @param {Function} handler - Event handler
|
|
162
|
+
*/
|
|
163
|
+
on(event, handler) {
|
|
164
|
+
if (!this.listeners.has(event)) {
|
|
165
|
+
this.listeners.set(event, []);
|
|
166
|
+
}
|
|
167
|
+
this.listeners.get(event).push(handler);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove an event listener
|
|
172
|
+
*
|
|
173
|
+
* @param {String} event - Event name
|
|
174
|
+
* @param {Function} handler - Event handler to remove
|
|
175
|
+
*/
|
|
176
|
+
off(event, handler) {
|
|
177
|
+
if (!this.listeners.has(event)) return;
|
|
178
|
+
const handlers = this.listeners.get(event);
|
|
179
|
+
const index = handlers.indexOf(handler);
|
|
180
|
+
if (index > -1) {
|
|
181
|
+
handlers.splice(index, 1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Emit an event to all registered handlers
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_emit(event, ...args) {
|
|
190
|
+
if (!this.listeners.has(event)) return;
|
|
191
|
+
const handlers = this.listeners.get(event);
|
|
192
|
+
handlers.forEach(handler => {
|
|
193
|
+
try {
|
|
194
|
+
handler(...args);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(`[SyncSystem] Error in '${event}' handler:`, error);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Start periodic broadcasting
|
|
203
|
+
*/
|
|
204
|
+
start() {
|
|
205
|
+
if (this.isRunning) {
|
|
206
|
+
console.warn('[SyncSystem] Already running');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!this.sendFunction) {
|
|
211
|
+
console.error('[SyncSystem] Cannot start - no send function provided');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// console.log('[SyncSystem] Starting periodic broadcast...');
|
|
216
|
+
this.isRunning = true;
|
|
217
|
+
|
|
218
|
+
// Broadcast immediately
|
|
219
|
+
this._broadcast();
|
|
220
|
+
|
|
221
|
+
// Then broadcast periodically
|
|
222
|
+
this.broadcastTimer = setInterval(() => {
|
|
223
|
+
this._broadcast();
|
|
224
|
+
}, this.broadcastInterval);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stop periodic broadcasting
|
|
229
|
+
*/
|
|
230
|
+
stop() {
|
|
231
|
+
if (!this.isRunning) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// console.log('[SyncSystem] Stopping broadcast');
|
|
236
|
+
this.isRunning = false;
|
|
237
|
+
|
|
238
|
+
if (this.broadcastTimer) {
|
|
239
|
+
clearInterval(this.broadcastTimer);
|
|
240
|
+
this.broadcastTimer = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Update local data for a specific source
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_updateLocalDataForSource(id) {
|
|
249
|
+
const source = this.syncSources.get(id);
|
|
250
|
+
if (!source) return;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
this.localData[id] = source.getFields();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(`[SyncSystem] Error getting fields from source '${id}':`, error);
|
|
256
|
+
this.localData[id] = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Update local data from all registered sources
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
_updateAllLocalData() {
|
|
265
|
+
for (const id of this.syncSources.keys()) {
|
|
266
|
+
this._updateLocalDataForSource(id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Broadcast current data to remote clients
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
_broadcast() {
|
|
275
|
+
// Update local data from all sources
|
|
276
|
+
this._updateAllLocalData();
|
|
277
|
+
|
|
278
|
+
// Send sync update message
|
|
279
|
+
const message = {
|
|
280
|
+
type: 'syncUpdate',
|
|
281
|
+
data: this.localData,
|
|
282
|
+
timestamp: Date.now()
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
this.sendFunction(message);
|
|
286
|
+
|
|
287
|
+
// Emit broadcast event
|
|
288
|
+
this._emit('broadcast', this.localData);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handle incoming sync update from remote client
|
|
293
|
+
*
|
|
294
|
+
* @param {Object} message - Sync update message with data field
|
|
295
|
+
*/
|
|
296
|
+
handleSyncUpdate(message) {
|
|
297
|
+
if (!message || !message.data) {
|
|
298
|
+
console.warn('[SyncSystem] Received syncUpdate without data');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const wasStale = this.isRemoteStale();
|
|
304
|
+
|
|
305
|
+
// Store remote data
|
|
306
|
+
this.remoteData = message.data;
|
|
307
|
+
this.lastRemoteUpdate = now;
|
|
308
|
+
|
|
309
|
+
// Emit updated event
|
|
310
|
+
this._emit('remoteUpdated', this.remoteData);
|
|
311
|
+
|
|
312
|
+
// Check staleness transition
|
|
313
|
+
if (wasStale && !this.isRemoteStale()) {
|
|
314
|
+
// Remote was stale but is now fresh
|
|
315
|
+
this._emit('remoteFresh');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get remote data for a specific source
|
|
321
|
+
*
|
|
322
|
+
* @param {String} id - Source ID
|
|
323
|
+
* @returns {Object|null} - Remote data for that source, or null if not available
|
|
324
|
+
*/
|
|
325
|
+
getRemote(id) {
|
|
326
|
+
return this.remoteData[id] || null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get specific field from remote data
|
|
331
|
+
*
|
|
332
|
+
* @param {String} sourceId - Source ID
|
|
333
|
+
* @param {String} field - Field name
|
|
334
|
+
* @returns {*} - Field value or null
|
|
335
|
+
*/
|
|
336
|
+
getRemoteField(sourceId, field) {
|
|
337
|
+
const sourceData = this.getRemote(sourceId);
|
|
338
|
+
return sourceData ? sourceData[field] : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get all remote data
|
|
343
|
+
*
|
|
344
|
+
* @returns {Object} - All remote data from all sources
|
|
345
|
+
*/
|
|
346
|
+
getAllRemote() {
|
|
347
|
+
return { ...this.remoteData };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Check if remote client data is stale (hasn't updated recently)
|
|
352
|
+
*
|
|
353
|
+
* @returns {Boolean} - True if data is stale or never received
|
|
354
|
+
*/
|
|
355
|
+
isRemoteStale() {
|
|
356
|
+
if (this.lastRemoteUpdate === 0) {
|
|
357
|
+
return true; // Never received data
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const timeSinceUpdate = Date.now() - this.lastRemoteUpdate;
|
|
361
|
+
const isStale = timeSinceUpdate > this.staleThreshold;
|
|
362
|
+
|
|
363
|
+
// Emit stale event on transition
|
|
364
|
+
if (isStale && !this.wasStale) {
|
|
365
|
+
this.wasStale = true;
|
|
366
|
+
this._emit('remoteStale');
|
|
367
|
+
} else if (!isStale) {
|
|
368
|
+
this.wasStale = false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return isStale;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get time since last remote update in milliseconds
|
|
376
|
+
*
|
|
377
|
+
* @returns {Number} - Milliseconds since last update (Infinity if never received)
|
|
378
|
+
*/
|
|
379
|
+
getTimeSinceLastUpdate() {
|
|
380
|
+
if (this.lastRemoteUpdate === 0) {
|
|
381
|
+
return Infinity;
|
|
382
|
+
}
|
|
383
|
+
return Date.now() - this.lastRemoteUpdate;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Check if remote client has sent at least one update
|
|
388
|
+
*
|
|
389
|
+
* @returns {Boolean} - True if we've received data
|
|
390
|
+
*/
|
|
391
|
+
hasRemoteData() {
|
|
392
|
+
return this.lastRemoteUpdate > 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Force an immediate broadcast (outside normal interval)
|
|
397
|
+
*/
|
|
398
|
+
forceBroadcast() {
|
|
399
|
+
if (this.isRunning) {
|
|
400
|
+
this._broadcast();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Clear remote data (useful when remote client disconnects)
|
|
406
|
+
*/
|
|
407
|
+
clearRemoteData() {
|
|
408
|
+
this.remoteData = {};
|
|
409
|
+
this.lastRemoteUpdate = 0;
|
|
410
|
+
this.wasStale = false;
|
|
411
|
+
// console.log('[SyncSystem] Remote data cleared');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get list of registered source IDs
|
|
416
|
+
*
|
|
417
|
+
* @returns {Array<String>} - Array of source IDs
|
|
418
|
+
*/
|
|
419
|
+
getRegisteredSources() {
|
|
420
|
+
return Array.from(this.syncSources.keys());
|
|
421
|
+
}
|
|
422
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionNetPeer - Wrapper around DataConnection
|
|
3
|
+
*
|
|
4
|
+
* Simple facade that wraps DataConnection and delegates all interface calls.
|
|
5
|
+
* Exists for compatibility and to handle tracker signaling via signal() method.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class ActionNetPeer {
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.opts = {
|
|
11
|
+
initiator: opts.initiator || false,
|
|
12
|
+
trickle: opts.trickle !== false,
|
|
13
|
+
iceServers: opts.iceServers || [
|
|
14
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
15
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
16
|
+
{ urls: 'stun:stun2.l.google.com:19302' }
|
|
17
|
+
],
|
|
18
|
+
...opts
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Peer IDs for initiator determination
|
|
22
|
+
this.localPeerId = opts.localPeerId || 'local';
|
|
23
|
+
this.remotePeerId = opts.remotePeerId || 'remote';
|
|
24
|
+
|
|
25
|
+
// Create internal DataConnection (the actual RTCPeerConnection)
|
|
26
|
+
this.connection = new DataConnection({
|
|
27
|
+
localPeerId: this.localPeerId,
|
|
28
|
+
remotePeerId: this.remotePeerId,
|
|
29
|
+
iceServers: this.opts.iceServers,
|
|
30
|
+
initiator: opts.initiator
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Expose DataConnection's pc directly
|
|
34
|
+
this.pc = this.connection.pc;
|
|
35
|
+
this.dataChannel = this.connection.dataChannel;
|
|
36
|
+
|
|
37
|
+
// State
|
|
38
|
+
this.destroyed = false;
|
|
39
|
+
this.connected = false;
|
|
40
|
+
this.handlers = new Map();
|
|
41
|
+
this.readyState = 'new';
|
|
42
|
+
|
|
43
|
+
// Delegate DataConnection events
|
|
44
|
+
this.connection.on('connect', () => {
|
|
45
|
+
this.connected = true;
|
|
46
|
+
this.readyState = 'connected';
|
|
47
|
+
this.emit('connect');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.connection.on('error', (err) => {
|
|
51
|
+
this.emit('error', err);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.connection.on('close', () => {
|
|
55
|
+
this.connected = false;
|
|
56
|
+
this.readyState = 'closed';
|
|
57
|
+
this.emit('close');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.connection.on('data', (data) => {
|
|
61
|
+
this.emit('data', data);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.connection.on('signal', (msg) => {
|
|
65
|
+
// Relay DataConnection's signaling messages (offer/answer/ICE)
|
|
66
|
+
this.emit('signal', msg);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle tracker offer/answer/ICE candidates
|
|
72
|
+
*/
|
|
73
|
+
signal(data) {
|
|
74
|
+
if (!this.connection) return;
|
|
75
|
+
this.connection.signal(data);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Send data through DataConnection
|
|
80
|
+
*/
|
|
81
|
+
send(data) {
|
|
82
|
+
if (!this.connection) return false;
|
|
83
|
+
return this.connection.send(typeof data === 'string' ? JSON.parse(data) : data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Event handlers
|
|
88
|
+
*/
|
|
89
|
+
on(event, handler) {
|
|
90
|
+
if (!this.handlers.has(event)) {
|
|
91
|
+
this.handlers.set(event, []);
|
|
92
|
+
}
|
|
93
|
+
this.handlers.get(event).push(handler);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
once(event, handler) {
|
|
97
|
+
const wrapper = (...args) => {
|
|
98
|
+
handler(...args);
|
|
99
|
+
this.off(event, wrapper);
|
|
100
|
+
};
|
|
101
|
+
this.on(event, wrapper);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
off(event, handler) {
|
|
105
|
+
if (!this.handlers.has(event)) return;
|
|
106
|
+
const handlers = this.handlers.get(event);
|
|
107
|
+
const index = handlers.indexOf(handler);
|
|
108
|
+
if (index !== -1) {
|
|
109
|
+
handlers.splice(index, 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
emit(event, ...args) {
|
|
114
|
+
if (!this.handlers.has(event)) return;
|
|
115
|
+
for (const handler of this.handlers.get(event)) {
|
|
116
|
+
try {
|
|
117
|
+
handler(...args);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`Error in ${event} handler:`, e);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cleanup
|
|
126
|
+
*/
|
|
127
|
+
destroy() {
|
|
128
|
+
if (this.destroyed) return;
|
|
129
|
+
|
|
130
|
+
this.destroyed = true;
|
|
131
|
+
this.connected = false;
|
|
132
|
+
|
|
133
|
+
if (this.connection) {
|
|
134
|
+
this.connection.close();
|
|
135
|
+
this.connection = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.pc = null;
|
|
139
|
+
this.dataChannel = null;
|
|
140
|
+
this.handlers.clear();
|
|
141
|
+
}
|
|
142
|
+
}
|