modd-network 1.0.0 → 1.0.1

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/dist/index.d.ts CHANGED
@@ -1,37 +1,2 @@
1
- export interface MODDOptions {
2
- centralServiceUrl?: string;
3
- nodeUrl?: string;
4
- }
5
- export interface OrderedInput {
6
- data: any;
7
- sequenceNumber: number;
8
- timestamp: number;
9
- clientId: string;
10
- id: string;
11
- }
12
- export type InputHandler = (input: OrderedInput) => void;
13
- export type SnapshotHandler = (snapshot: any) => void;
14
- export declare class MODD {
15
- private ws;
16
- private roomId;
17
- private appId;
18
- private centralServiceUrl;
19
- private nodeUrl;
20
- private snapshot;
21
- private inputHandlers;
22
- private snapshotHandlers;
23
- constructor(appId: string, options?: MODDOptions);
24
- createRoom(roomId: string, initialSnapshot: any): Promise<void>;
25
- joinRoom(roomId: string): Promise<void>;
26
- private setupMessageHandlers;
27
- sendInput(data: any): void;
28
- sendSnapshot(snapshot: any): void;
29
- onInput(handler: InputHandler): void;
30
- onSnapshot(handler: SnapshotHandler): void;
31
- getSnapshot(): any;
32
- disconnect(): void;
33
- private getNodeUrl;
34
- private hashSnapshot;
35
- }
36
- export declare class MeshClient extends MODD {
37
- }
1
+ export * from './modd-network';
2
+ export * from './modd-engine';
package/dist/index.js CHANGED
@@ -1,177 +1,18 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
4
15
  };
5
16
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MeshClient = exports.MODD = void 0;
7
- const ws_1 = __importDefault(require("ws"));
8
- const crypto_1 = __importDefault(require("crypto"));
9
- // Primary SDK client: MODD
10
- class MODD {
11
- ws = null;
12
- roomId = null;
13
- appId = null;
14
- centralServiceUrl;
15
- nodeUrl = null;
16
- snapshot = null;
17
- inputHandlers = [];
18
- snapshotHandlers = [];
19
- constructor(appId, options = {}) {
20
- this.appId = appId;
21
- this.centralServiceUrl = options.centralServiceUrl || 'ws://localhost:9001';
22
- this.nodeUrl = options.nodeUrl || null;
23
- }
24
- async createRoom(roomId, initialSnapshot) {
25
- if (!this.appId) {
26
- throw new Error('appId is required. Initialize MODD with appId in constructor.');
27
- }
28
- this.roomId = roomId;
29
- this.snapshot = initialSnapshot;
30
- const nodeUrl = this.nodeUrl || await this.getNodeUrl(roomId);
31
- return new Promise((resolve, reject) => {
32
- this.ws = new ws_1.default(nodeUrl);
33
- this.ws.on('open', () => {
34
- this.ws.send(JSON.stringify({
35
- type: 'CREATE_ROOM',
36
- payload: { roomId, appId: this.appId, snapshot: initialSnapshot }
37
- }));
38
- });
39
- this.ws.on('message', (data) => {
40
- const message = JSON.parse(data.toString());
41
- if (message.type === 'ROOM_CREATED') {
42
- console.log('Room created:', roomId);
43
- this.setupMessageHandlers();
44
- resolve();
45
- }
46
- });
47
- this.ws.on('error', reject);
48
- });
49
- }
50
- async joinRoom(roomId) {
51
- if (!this.appId) {
52
- throw new Error('appId is required. Initialize MODD with appId in constructor.');
53
- }
54
- this.roomId = roomId;
55
- const nodeUrl = this.nodeUrl || await this.getNodeUrl(roomId);
56
- return new Promise((resolve, reject) => {
57
- this.ws = new ws_1.default(nodeUrl);
58
- this.ws.on('open', () => {
59
- this.ws.send(JSON.stringify({
60
- type: 'JOIN_ROOM',
61
- payload: { roomId, appId: this.appId }
62
- }));
63
- });
64
- this.ws.on('message', (data) => {
65
- const message = JSON.parse(data.toString());
66
- if (message.type === 'INITIAL_STATE') {
67
- this.snapshot = message.payload.snapshot;
68
- const inputs = message.payload.inputs || [];
69
- // Apply any pending inputs to catch up (in sequence order)
70
- const sortedInputs = [...inputs].sort((a, b) => a.sequenceNumber - b.sequenceNumber);
71
- sortedInputs.forEach((input) => {
72
- const orderedInput = {
73
- data: input.data,
74
- sequenceNumber: input.sequenceNumber,
75
- timestamp: input.timestamp,
76
- clientId: input.clientId,
77
- id: input.id
78
- };
79
- this.inputHandlers.forEach(handler => handler(orderedInput));
80
- });
81
- }
82
- if (message.type === 'ROOM_JOINED') {
83
- console.log('Joined room:', roomId);
84
- this.setupMessageHandlers();
85
- resolve();
86
- }
87
- });
88
- this.ws.on('error', reject);
89
- });
90
- }
91
- setupMessageHandlers() {
92
- if (!this.ws)
93
- return;
94
- this.ws.on('message', (data) => {
95
- const message = JSON.parse(data.toString());
96
- if (message.type === 'ORDERED_INPUTS') {
97
- const inputs = message.payload.inputs || [];
98
- inputs.forEach((input) => {
99
- const orderedInput = {
100
- data: input.data,
101
- sequenceNumber: input.sequenceNumber,
102
- timestamp: input.timestamp,
103
- clientId: input.clientId,
104
- id: input.id
105
- };
106
- this.inputHandlers.forEach(handler => handler(orderedInput));
107
- });
108
- }
109
- if (message.type === 'SNAPSHOT_UPDATE') {
110
- this.snapshot = message.payload.snapshot;
111
- this.snapshotHandlers.forEach(handler => handler(this.snapshot));
112
- }
113
- });
114
- }
115
- sendInput(data) {
116
- if (!this.ws || !this.roomId) {
117
- throw new Error('Not connected to a room');
118
- }
119
- this.ws.send(JSON.stringify({
120
- type: 'SEND_INPUT',
121
- payload: { roomId: this.roomId, data }
122
- }));
123
- }
124
- sendSnapshot(snapshot) {
125
- if (!this.ws || !this.roomId) {
126
- throw new Error('Not connected to a room');
127
- }
128
- this.snapshot = snapshot;
129
- const hash = this.hashSnapshot(snapshot);
130
- this.ws.send(JSON.stringify({
131
- type: 'SEND_SNAPSHOT',
132
- payload: { roomId: this.roomId, snapshot, hash }
133
- }));
134
- }
135
- onInput(handler) {
136
- this.inputHandlers.push(handler);
137
- }
138
- onSnapshot(handler) {
139
- this.snapshotHandlers.push(handler);
140
- }
141
- getSnapshot() {
142
- return this.snapshot;
143
- }
144
- disconnect() {
145
- if (this.ws) {
146
- this.ws.close();
147
- this.ws = null;
148
- }
149
- }
150
- async getNodeUrl(roomId) {
151
- if (!this.appId) {
152
- throw new Error('appId is required');
153
- }
154
- // Use REST API instead of WebSocket for better error handling
155
- const httpUrl = this.centralServiceUrl.replace('ws://', 'http://').replace('wss://', 'https://');
156
- const response = await fetch(`${httpUrl}/api/apps/${this.appId}/rooms/${roomId}/connect`, {
157
- method: 'POST',
158
- headers: { 'Content-Type': 'application/json' },
159
- body: JSON.stringify({})
160
- });
161
- if (!response.ok) {
162
- const error = await response.json().catch(() => ({ error: 'Failed to connect to room' }));
163
- throw new Error(error.error || `HTTP ${response.status}`);
164
- }
165
- const data = await response.json();
166
- return data.url;
167
- }
168
- hashSnapshot(snapshot) {
169
- const data = JSON.stringify(snapshot);
170
- return crypto_1.default.createHash('sha256').update(data).digest('hex');
171
- }
172
- }
173
- exports.MODD = MODD;
174
- // Backwards-compatible alias
175
- class MeshClient extends MODD {
176
- }
177
- exports.MeshClient = MeshClient;
17
+ __exportStar(require("./modd-network"), exports);
18
+ __exportStar(require("./modd-engine"), exports);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * MODD Engine SDK
3
+ * Game synchronization utilities for multiplayer games
4
+ * - Proximity-based authority
5
+ * - Delta compression
6
+ * - Rest detection
7
+ * - Soft sync / interpolation
8
+ */
9
+ import type { Connection } from './modd-network';
10
+ export interface EntityState {
11
+ x: number;
12
+ y: number;
13
+ z: number;
14
+ vx?: number;
15
+ vy?: number;
16
+ vz?: number;
17
+ qx?: number;
18
+ qy?: number;
19
+ qz?: number;
20
+ qw?: number;
21
+ avx?: number;
22
+ avy?: number;
23
+ avz?: number;
24
+ [key: string]: any;
25
+ }
26
+ export interface PhysicsAdapter {
27
+ getState(): EntityState;
28
+ setState(state: EntityState): void;
29
+ }
30
+ export interface EngineOptions {
31
+ softSyncLerp?: number;
32
+ positionThreshold?: number;
33
+ velocityThreshold?: number;
34
+ rotationThreshold?: number;
35
+ restVelocityThreshold?: number;
36
+ restFramesRequired?: number;
37
+ touchBonusScore?: number;
38
+ touchBonusDecay?: number;
39
+ }
40
+ export interface Entity {
41
+ id: string;
42
+ adapter: PhysicsAdapter;
43
+ state: EntityState;
44
+ lastSentState?: EntityState;
45
+ restFrames: number;
46
+ lastTouchedBy?: string;
47
+ lastTouchFrame?: number;
48
+ }
49
+ export interface Player {
50
+ id: string;
51
+ x: number;
52
+ y: number;
53
+ z: number;
54
+ dead?: boolean;
55
+ }
56
+ export declare function createEngine(options?: EngineOptions): {
57
+ readonly config: {
58
+ softSyncLerp: number;
59
+ positionThreshold: number;
60
+ velocityThreshold: number;
61
+ rotationThreshold: number;
62
+ restVelocityThreshold: number;
63
+ restFramesRequired: number;
64
+ touchBonusScore: number;
65
+ touchBonusDecay: number;
66
+ };
67
+ setConnection(conn: Connection): void;
68
+ setLocalPlayer(playerId: string): void;
69
+ setFrame(frame: number): void;
70
+ registerEntity(id: string, adapter: PhysicsAdapter): void;
71
+ unregisterEntity(id: string): void;
72
+ getEntity(id: string): Entity | undefined;
73
+ updatePlayer(id: string, x: number, y: number, z: number, dead?: boolean): void;
74
+ removePlayer(id: string): void;
75
+ computeAuthority: (entityId: string) => string | null;
76
+ isLocalAuthority: (entityId: string) => boolean;
77
+ syncFromPhysics(entityId: string): void;
78
+ syncAllFromPhysics(): void;
79
+ sendUpdates: () => void;
80
+ handleEntityState: (id: string, state: EntityState) => void;
81
+ handleEntityTouch: (id: string, touchedBy: string, frame: number) => void;
82
+ checkPlayerTouches(playerId: string, playerX: number, playerZ: number, touchDistance?: number): void;
83
+ checkEntityCollisions(touchDistance?: number): void;
84
+ wakeEntity: (entityId: string) => void;
85
+ isAtRest: (entityId: string) => boolean;
86
+ hasStateChanged: (entityId: string) => boolean;
87
+ };
88
+ /**
89
+ * Rapier 3D physics adapter
90
+ */
91
+ export declare function rapierAdapter(body: any): PhysicsAdapter;
92
+ /**
93
+ * Generic 2D physics adapter (for Matter.js, Planck, etc.)
94
+ */
95
+ export declare function generic2DAdapter(getBody: () => any, setBody: (state: any) => void): PhysicsAdapter;
@@ -0,0 +1,389 @@
1
+ "use strict";
2
+ /**
3
+ * MODD Engine SDK
4
+ * Game synchronization utilities for multiplayer games
5
+ * - Proximity-based authority
6
+ * - Delta compression
7
+ * - Rest detection
8
+ * - Soft sync / interpolation
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createEngine = createEngine;
12
+ exports.rapierAdapter = rapierAdapter;
13
+ exports.generic2DAdapter = generic2DAdapter;
14
+ // ============================================
15
+ // Engine Implementation
16
+ // ============================================
17
+ function createEngine(options = {}) {
18
+ // Configuration with defaults
19
+ const config = {
20
+ softSyncLerp: options.softSyncLerp ?? 0.5,
21
+ positionThreshold: options.positionThreshold ?? 0.01,
22
+ velocityThreshold: options.velocityThreshold ?? 0.1,
23
+ rotationThreshold: options.rotationThreshold ?? 0.01,
24
+ restVelocityThreshold: options.restVelocityThreshold ?? 0.05,
25
+ restFramesRequired: options.restFramesRequired ?? 30,
26
+ touchBonusScore: options.touchBonusScore ?? 50,
27
+ touchBonusDecay: options.touchBonusDecay ?? 100,
28
+ };
29
+ // State
30
+ const entities = new Map();
31
+ const players = new Map();
32
+ let localPlayerId = null;
33
+ let currentFrame = 0;
34
+ let connection = null;
35
+ // ==========================================
36
+ // Authority Computation
37
+ // ==========================================
38
+ function simpleHash(str) {
39
+ let h = 0;
40
+ for (let i = 0; i < str.length; i++) {
41
+ h = ((h << 5) - h + str.charCodeAt(i)) | 0;
42
+ }
43
+ return Math.abs(h);
44
+ }
45
+ function computeAuthority(entityId) {
46
+ const entity = entities.get(entityId);
47
+ if (!entity)
48
+ return null;
49
+ const playerIds = Array.from(players.keys()).sort();
50
+ if (playerIds.length === 0)
51
+ return null;
52
+ let bestPlayer = null;
53
+ let bestScore = -Infinity;
54
+ for (const pid of playerIds) {
55
+ const player = players.get(pid);
56
+ if (player.dead)
57
+ continue;
58
+ // Distance-based score (closer = higher)
59
+ const dx = player.x - entity.state.x;
60
+ const dz = player.z - entity.state.z;
61
+ const dist = Math.sqrt(dx * dx + dz * dz);
62
+ let score = 1000 - dist * 10;
63
+ // Touch bonus (decays over time)
64
+ if (entity.lastTouchedBy === pid) {
65
+ const framesSinceTouch = currentFrame - (entity.lastTouchFrame || 0);
66
+ const decayRate = config.touchBonusScore / config.touchBonusDecay;
67
+ score += Math.max(0, config.touchBonusScore - framesSinceTouch * decayRate);
68
+ }
69
+ // Deterministic tiebreaker
70
+ score += simpleHash(pid + entityId) * 0.0001;
71
+ if (score > bestScore) {
72
+ bestScore = score;
73
+ bestPlayer = pid;
74
+ }
75
+ }
76
+ return bestPlayer;
77
+ }
78
+ function isLocalAuthority(entityId) {
79
+ return localPlayerId !== null && computeAuthority(entityId) === localPlayerId;
80
+ }
81
+ // ==========================================
82
+ // Delta Compression
83
+ // ==========================================
84
+ function hasStateChanged(entityId) {
85
+ const entity = entities.get(entityId);
86
+ if (!entity || !entity.lastSentState)
87
+ return true;
88
+ const curr = entity.state;
89
+ const last = entity.lastSentState;
90
+ // Position
91
+ if (Math.abs(curr.x - last.x) > config.positionThreshold)
92
+ return true;
93
+ if (Math.abs(curr.y - last.y) > config.positionThreshold)
94
+ return true;
95
+ if (Math.abs(curr.z - last.z) > config.positionThreshold)
96
+ return true;
97
+ // Velocity
98
+ if (Math.abs((curr.vx || 0) - (last.vx || 0)) > config.velocityThreshold)
99
+ return true;
100
+ if (Math.abs((curr.vy || 0) - (last.vy || 0)) > config.velocityThreshold)
101
+ return true;
102
+ if (Math.abs((curr.vz || 0) - (last.vz || 0)) > config.velocityThreshold)
103
+ return true;
104
+ // Rotation
105
+ if (Math.abs((curr.qx || 0) - (last.qx || 0)) > config.rotationThreshold)
106
+ return true;
107
+ if (Math.abs((curr.qy || 0) - (last.qy || 0)) > config.rotationThreshold)
108
+ return true;
109
+ if (Math.abs((curr.qz || 0) - (last.qz || 0)) > config.rotationThreshold)
110
+ return true;
111
+ if (Math.abs((curr.qw || 1) - (last.qw || 1)) > config.rotationThreshold)
112
+ return true;
113
+ return false;
114
+ }
115
+ // ==========================================
116
+ // Rest Detection
117
+ // ==========================================
118
+ function isAtRest(entityId) {
119
+ const entity = entities.get(entityId);
120
+ if (!entity)
121
+ return false;
122
+ const state = entity.state;
123
+ const speed = Math.sqrt((state.vx || 0) ** 2 +
124
+ (state.vy || 0) ** 2 +
125
+ (state.vz || 0) ** 2);
126
+ const angSpeed = Math.sqrt((state.avx || 0) ** 2 +
127
+ (state.avy || 0) ** 2 +
128
+ (state.avz || 0) ** 2);
129
+ if (speed < config.restVelocityThreshold && angSpeed < config.restVelocityThreshold) {
130
+ entity.restFrames++;
131
+ return entity.restFrames > config.restFramesRequired;
132
+ }
133
+ else {
134
+ entity.restFrames = 0;
135
+ return false;
136
+ }
137
+ }
138
+ function wakeEntity(entityId) {
139
+ const entity = entities.get(entityId);
140
+ if (entity) {
141
+ entity.restFrames = 0;
142
+ }
143
+ }
144
+ // ==========================================
145
+ // Soft Sync
146
+ // ==========================================
147
+ function applySoftSync(entityId, targetState) {
148
+ const entity = entities.get(entityId);
149
+ if (!entity)
150
+ return;
151
+ // Skip if we're authority
152
+ if (isLocalAuthority(entityId))
153
+ return;
154
+ const lerp = config.softSyncLerp;
155
+ // Lerp position
156
+ entity.state.x += (targetState.x - entity.state.x) * lerp;
157
+ entity.state.y += (targetState.y - entity.state.y) * lerp;
158
+ entity.state.z += (targetState.z - entity.state.z) * lerp;
159
+ // Hard sync velocity
160
+ entity.state.vx = targetState.vx;
161
+ entity.state.vy = targetState.vy;
162
+ entity.state.vz = targetState.vz;
163
+ // Hard sync rotation
164
+ entity.state.qx = targetState.qx;
165
+ entity.state.qy = targetState.qy;
166
+ entity.state.qz = targetState.qz;
167
+ entity.state.qw = targetState.qw;
168
+ // Hard sync angular velocity
169
+ entity.state.avx = targetState.avx;
170
+ entity.state.avy = targetState.avy;
171
+ entity.state.avz = targetState.avz;
172
+ // Apply to physics
173
+ entity.adapter.setState(entity.state);
174
+ // Wake the entity
175
+ wakeEntity(entityId);
176
+ }
177
+ // ==========================================
178
+ // Network Integration
179
+ // ==========================================
180
+ function sendEntityUpdates() {
181
+ if (!connection?.connected)
182
+ return;
183
+ for (const [id, entity] of entities) {
184
+ // Skip if not authority
185
+ if (!isLocalAuthority(id))
186
+ continue;
187
+ // Skip if at rest
188
+ if (isAtRest(id))
189
+ continue;
190
+ // Skip if no meaningful change
191
+ if (!hasStateChanged(id))
192
+ continue;
193
+ // Sync state from physics
194
+ entity.state = entity.adapter.getState();
195
+ // Send update
196
+ connection.send({
197
+ type: 'entityState',
198
+ id,
199
+ ...entity.state
200
+ });
201
+ // Remember what we sent
202
+ entity.lastSentState = { ...entity.state };
203
+ }
204
+ }
205
+ function handleEntityState(id, state) {
206
+ const entity = entities.get(id);
207
+ if (!entity)
208
+ return;
209
+ // Skip if we're authority
210
+ if (isLocalAuthority(id))
211
+ return;
212
+ applySoftSync(id, state);
213
+ }
214
+ function handleEntityTouch(id, touchedBy, frame) {
215
+ const entity = entities.get(id);
216
+ if (entity) {
217
+ entity.lastTouchedBy = touchedBy;
218
+ entity.lastTouchFrame = frame;
219
+ wakeEntity(id);
220
+ }
221
+ }
222
+ // ==========================================
223
+ // Public API
224
+ // ==========================================
225
+ return {
226
+ // Configuration
227
+ get config() {
228
+ return { ...config };
229
+ },
230
+ // Setup
231
+ setConnection(conn) {
232
+ connection = conn;
233
+ },
234
+ setLocalPlayer(playerId) {
235
+ localPlayerId = playerId;
236
+ },
237
+ setFrame(frame) {
238
+ currentFrame = frame;
239
+ },
240
+ // Entity management
241
+ registerEntity(id, adapter) {
242
+ entities.set(id, {
243
+ id,
244
+ adapter,
245
+ state: adapter.getState(),
246
+ restFrames: 0
247
+ });
248
+ },
249
+ unregisterEntity(id) {
250
+ entities.delete(id);
251
+ },
252
+ getEntity(id) {
253
+ return entities.get(id);
254
+ },
255
+ // Player management (for authority calculation)
256
+ updatePlayer(id, x, y, z, dead = false) {
257
+ players.set(id, { id, x, y, z, dead });
258
+ },
259
+ removePlayer(id) {
260
+ players.delete(id);
261
+ },
262
+ // Authority
263
+ computeAuthority,
264
+ isLocalAuthority,
265
+ // Sync operations
266
+ syncFromPhysics(entityId) {
267
+ const entity = entities.get(entityId);
268
+ if (entity) {
269
+ entity.state = entity.adapter.getState();
270
+ }
271
+ },
272
+ syncAllFromPhysics() {
273
+ for (const entity of entities.values()) {
274
+ entity.state = entity.adapter.getState();
275
+ }
276
+ },
277
+ // Network operations (call each tick)
278
+ sendUpdates: sendEntityUpdates,
279
+ // Input handlers (call from processInput)
280
+ handleEntityState,
281
+ handleEntityTouch,
282
+ // Touch detection (call after physics step)
283
+ checkPlayerTouches(playerId, playerX, playerZ, touchDistance = 2.0) {
284
+ if (!connection?.connected || playerId !== localPlayerId)
285
+ return;
286
+ for (const [id, entity] of entities) {
287
+ const dx = playerX - entity.state.x;
288
+ const dz = playerZ - entity.state.z;
289
+ const dist = Math.sqrt(dx * dx + dz * dz);
290
+ if (dist < touchDistance && entity.lastTouchedBy !== playerId) {
291
+ connection.send({
292
+ type: 'entityTouch',
293
+ id,
294
+ touchedBy: playerId,
295
+ frame: currentFrame
296
+ });
297
+ }
298
+ }
299
+ },
300
+ checkEntityCollisions(touchDistance = 4.0) {
301
+ if (!connection?.connected)
302
+ return;
303
+ const entityIds = Array.from(entities.keys());
304
+ for (let i = 0; i < entityIds.length; i++) {
305
+ const idA = entityIds[i];
306
+ if (!isLocalAuthority(idA))
307
+ continue;
308
+ const entityA = entities.get(idA);
309
+ for (let j = i + 1; j < entityIds.length; j++) {
310
+ const idB = entityIds[j];
311
+ const entityB = entities.get(idB);
312
+ const dx = entityA.state.x - entityB.state.x;
313
+ const dz = entityA.state.z - entityB.state.z;
314
+ const dist = Math.sqrt(dx * dx + dz * dz);
315
+ if (dist < touchDistance && entityB.lastTouchedBy !== localPlayerId) {
316
+ connection.send({
317
+ type: 'entityTouch',
318
+ id: idB,
319
+ touchedBy: localPlayerId,
320
+ frame: currentFrame
321
+ });
322
+ }
323
+ }
324
+ }
325
+ },
326
+ // Utility
327
+ wakeEntity,
328
+ isAtRest,
329
+ hasStateChanged,
330
+ };
331
+ }
332
+ // ============================================
333
+ // Physics Adapters
334
+ // ============================================
335
+ /**
336
+ * Rapier 3D physics adapter
337
+ */
338
+ function rapierAdapter(body) {
339
+ return {
340
+ getState() {
341
+ const pos = body.translation();
342
+ const vel = body.linvel();
343
+ const rot = body.rotation();
344
+ const angvel = body.angvel();
345
+ return {
346
+ x: pos.x, y: pos.y, z: pos.z,
347
+ vx: vel.x, vy: vel.y, vz: vel.z,
348
+ qx: rot.x, qy: rot.y, qz: rot.z, qw: rot.w,
349
+ avx: angvel.x, avy: angvel.y, avz: angvel.z
350
+ };
351
+ },
352
+ setState(state) {
353
+ body.setTranslation({ x: state.x, y: state.y, z: state.z }, true);
354
+ body.setLinvel({ x: state.vx || 0, y: state.vy || 0, z: state.vz || 0 }, true);
355
+ body.setRotation({ x: state.qx || 0, y: state.qy || 0, z: state.qz || 0, w: state.qw || 1 }, true);
356
+ body.setAngvel({ x: state.avx || 0, y: state.avy || 0, z: state.avz || 0 }, true);
357
+ }
358
+ };
359
+ }
360
+ /**
361
+ * Generic 2D physics adapter (for Matter.js, Planck, etc.)
362
+ */
363
+ function generic2DAdapter(getBody, setBody) {
364
+ return {
365
+ getState() {
366
+ const body = getBody();
367
+ return {
368
+ x: body.x || body.position?.x || 0,
369
+ y: body.y || body.position?.y || 0,
370
+ z: 0,
371
+ vx: body.vx || body.velocity?.x || 0,
372
+ vy: body.vy || body.velocity?.y || 0,
373
+ vz: 0
374
+ };
375
+ },
376
+ setState(state) {
377
+ setBody({
378
+ x: state.x,
379
+ y: state.y,
380
+ vx: state.vx,
381
+ vy: state.vy
382
+ });
383
+ }
384
+ };
385
+ }
386
+ // Browser global fallback
387
+ if (typeof window !== 'undefined') {
388
+ window.moddEngine = { createEngine, rapierAdapter, generic2DAdapter };
389
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * MODD Network SDK
3
+ * Pure network layer for real-time multiplayer applications
4
+ * Works with any app type: games, chat, collaboration, etc.
5
+ */
6
+ export interface ConnectOptions {
7
+ appId?: string;
8
+ snapshot?: any;
9
+ user?: any;
10
+ centralServiceUrl?: string;
11
+ creatorNodeId?: string;
12
+ getStateHash?: () => string;
13
+ onConnect?: (snapshot: any, inputs: Input[]) => void;
14
+ onDisconnect?: () => void;
15
+ onError?: (error: string) => void;
16
+ onMessage?: (data: any, seq: number) => void;
17
+ onTick?: (frame: number, inputs: Input[], isCatchUp?: boolean) => void;
18
+ onSnapshot?: (snapshot: any, hash: string) => void;
19
+ }
20
+ export interface Input {
21
+ sequenceNumber: number;
22
+ data: any;
23
+ clientId?: string;
24
+ timestamp?: number;
25
+ }
26
+ export interface Connection {
27
+ send(data: any): void;
28
+ sendSnapshot(snapshot: any, hash: string): void;
29
+ leaveRoom(): void;
30
+ close(): void;
31
+ readonly connected: boolean;
32
+ readonly node: string | null;
33
+ readonly bandwidthIn: number;
34
+ readonly bandwidthOut: number;
35
+ readonly totalBytesIn: number;
36
+ readonly totalBytesOut: number;
37
+ readonly frame: number;
38
+ }
39
+ export declare function connect(roomId: string, options?: ConnectOptions): Promise<Connection>;
40
+ export declare const modd: typeof connect;
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+ /**
3
+ * MODD Network SDK
4
+ * Pure network layer for real-time multiplayer applications
5
+ * Works with any app type: games, chat, collaboration, etc.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.modd = void 0;
12
+ exports.connect = connect;
13
+ const ws_1 = __importDefault(require("ws"));
14
+ // ============================================
15
+ // Binary Protocol
16
+ // ============================================
17
+ const BinaryMessageType = {
18
+ TICK: 0x01,
19
+ INITIAL_STATE: 0x02,
20
+ ROOM_JOINED: 0x03,
21
+ ROOM_CREATED: 0x04,
22
+ ERROR: 0x05,
23
+ SNAPSHOT_UPDATE: 0x06,
24
+ ROOM_LEFT: 0x07,
25
+ SYNC_HASH: 0x08
26
+ };
27
+ function encodeSyncHash(roomId, hash, seq, frame) {
28
+ const roomIdBytes = new TextEncoder().encode(roomId);
29
+ const hashBytes = new TextEncoder().encode(hash);
30
+ const totalLen = 1 + 2 + roomIdBytes.length + 2 + hashBytes.length + 4 + 4;
31
+ const buffer = new ArrayBuffer(totalLen);
32
+ const view = new DataView(buffer);
33
+ const uint8 = new Uint8Array(buffer);
34
+ let offset = 0;
35
+ view.setUint8(offset, BinaryMessageType.SYNC_HASH);
36
+ offset += 1;
37
+ view.setUint16(offset, roomIdBytes.length, true);
38
+ offset += 2;
39
+ uint8.set(roomIdBytes, offset);
40
+ offset += roomIdBytes.length;
41
+ view.setUint16(offset, hashBytes.length, true);
42
+ offset += 2;
43
+ uint8.set(hashBytes, offset);
44
+ offset += hashBytes.length;
45
+ view.setUint32(offset, seq, true);
46
+ offset += 4;
47
+ view.setUint32(offset, frame, true);
48
+ return buffer;
49
+ }
50
+ function decodeBinaryMessage(buffer) {
51
+ const view = new DataView(buffer);
52
+ if (buffer.byteLength === 0)
53
+ return null;
54
+ const type = view.getUint8(0);
55
+ switch (type) {
56
+ case BinaryMessageType.TICK: {
57
+ const frame = view.getUint32(1, true);
58
+ let inputs = [];
59
+ if (buffer.byteLength > 5) {
60
+ const inputCount = view.getUint16(5, true);
61
+ if (inputCount > 0) {
62
+ const inputsJson = new TextDecoder().decode(buffer.slice(7));
63
+ inputs = JSON.parse(inputsJson);
64
+ }
65
+ }
66
+ return { type: 'TICK', frame, inputs };
67
+ }
68
+ case BinaryMessageType.INITIAL_STATE: {
69
+ let offset = 1;
70
+ const frame = view.getUint32(offset, true);
71
+ offset += 4;
72
+ const roomIdLen = view.getUint16(offset, true);
73
+ offset += 2;
74
+ const roomId = new TextDecoder().decode(buffer.slice(offset, offset + roomIdLen));
75
+ offset += roomIdLen;
76
+ const snapshotLen = view.getUint32(offset, true);
77
+ offset += 4;
78
+ const snapshotJson = new TextDecoder().decode(buffer.slice(offset, offset + snapshotLen));
79
+ offset += snapshotLen;
80
+ const inputsLen = view.getUint32(offset, true);
81
+ offset += 4;
82
+ const inputsJson = new TextDecoder().decode(buffer.slice(offset, offset + inputsLen));
83
+ const { snapshot, snapshotHash } = JSON.parse(snapshotJson);
84
+ const inputs = JSON.parse(inputsJson);
85
+ return { type: 'INITIAL_STATE', roomId, frame, snapshot, snapshotHash, inputs };
86
+ }
87
+ case BinaryMessageType.ROOM_JOINED: {
88
+ const roomIdLen = view.getUint16(1, true);
89
+ const roomId = new TextDecoder().decode(buffer.slice(3, 3 + roomIdLen));
90
+ return { type: 'ROOM_JOINED', roomId };
91
+ }
92
+ case BinaryMessageType.ROOM_CREATED: {
93
+ let offset = 1;
94
+ const roomIdLen = view.getUint16(offset, true);
95
+ offset += 2;
96
+ const roomId = new TextDecoder().decode(buffer.slice(offset, offset + roomIdLen));
97
+ offset += roomIdLen;
98
+ const snapshotLen = view.getUint32(offset, true);
99
+ offset += 4;
100
+ const snapshotJson = new TextDecoder().decode(buffer.slice(offset, offset + snapshotLen));
101
+ const { snapshot, snapshotHash } = JSON.parse(snapshotJson);
102
+ return { type: 'ROOM_CREATED', roomId, snapshot, snapshotHash };
103
+ }
104
+ case BinaryMessageType.ERROR: {
105
+ const msgLen = view.getUint16(1, true);
106
+ const message = new TextDecoder().decode(buffer.slice(3, 3 + msgLen));
107
+ return { type: 'ERROR', message };
108
+ }
109
+ case BinaryMessageType.SNAPSHOT_UPDATE: {
110
+ let offset = 1;
111
+ const roomIdLen = view.getUint16(offset, true);
112
+ offset += 2;
113
+ const roomId = new TextDecoder().decode(buffer.slice(offset, offset + roomIdLen));
114
+ offset += roomIdLen;
115
+ const snapshotLen = view.getUint32(offset, true);
116
+ offset += 4;
117
+ const snapshotJson = new TextDecoder().decode(buffer.slice(offset, offset + snapshotLen));
118
+ const { snapshot, snapshotHash } = JSON.parse(snapshotJson);
119
+ return { type: 'SNAPSHOT_UPDATE', roomId, snapshot, snapshotHash };
120
+ }
121
+ case BinaryMessageType.ROOM_LEFT: {
122
+ const roomIdLen = view.getUint16(1, true);
123
+ const roomId = new TextDecoder().decode(buffer.slice(3, 3 + roomIdLen));
124
+ return { type: 'ROOM_LEFT', roomId };
125
+ }
126
+ default:
127
+ return null;
128
+ }
129
+ }
130
+ // Helper to normalize binary data to ArrayBuffer
131
+ async function toArrayBuffer(data) {
132
+ if (data instanceof ArrayBuffer) {
133
+ return data;
134
+ }
135
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
136
+ return await data.arrayBuffer();
137
+ }
138
+ // Fallback for Buffer, ArrayBufferView, or array-like
139
+ // creating a new Uint8Array(data) copies the data to a new ArrayBuffer
140
+ return new Uint8Array(data).buffer;
141
+ }
142
+ // ============================================
143
+ // Main Connect Function
144
+ // ============================================
145
+ async function connect(roomId, options = {}) {
146
+ const appId = options.appId || 'app';
147
+ const initialSnapshot = options.snapshot || {};
148
+ const user = options.user || null;
149
+ const onConnect = options.onConnect || (() => { });
150
+ const onDisconnect = options.onDisconnect || (() => { });
151
+ const onError = options.onError || ((err) => console.error('[modd-network]', err));
152
+ const onMessage = options.onMessage || (() => { });
153
+ const onTick = options.onTick || null;
154
+ const getStateHash = options.getStateHash || null;
155
+ // Determine central service URL
156
+ const centralServiceUrl = options.centralServiceUrl || (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
157
+ ? 'http://localhost:9001'
158
+ : 'https://nodes.modd.io');
159
+ let connected = false;
160
+ let initialStateReceived = null;
161
+ let deliveredSeqs = new Set();
162
+ let pendingTicks = [];
163
+ let ws = null;
164
+ let nodeUrl = null;
165
+ // Bandwidth tracking
166
+ let bytesIn = 0;
167
+ let bytesOut = 0;
168
+ let lastBytesIn = 0;
169
+ let lastBytesOut = 0;
170
+ let bandwidthIn = 0;
171
+ let bandwidthOut = 0;
172
+ let bandwidthInterval = null;
173
+ // Hash sync tracking
174
+ let hashInterval = null;
175
+ let lastSyncSeq = 0;
176
+ let lastSyncFrame = 0;
177
+ let currentFrame = 0;
178
+ try {
179
+ const res = await fetch(`${centralServiceUrl}/api/apps/${appId}/rooms/${roomId}/connect`, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({ creatorNodeId: options.creatorNodeId })
183
+ });
184
+ if (!res.ok) {
185
+ throw new Error(`Central service error: ${res.status}`);
186
+ }
187
+ const { url } = await res.json();
188
+ nodeUrl = url;
189
+ // Use imported WebSocket (work in Node) or global WebSocket if present (Browser)
190
+ // Note: importing 'ws' gives a constructor. In browser env, 'ws' module might be shimmed.
191
+ // However, for best compat, let's use globalThis.WebSocket if available, else WS.
192
+ const WS = (typeof globalThis !== 'undefined' && globalThis.WebSocket) ? globalThis.WebSocket : ws_1.default;
193
+ // @ts-ignore - TS might complain about mixing types
194
+ ws = new WS(nodeUrl);
195
+ ws.binaryType = 'arraybuffer'; // Ensure we get ArrayBuffer if possible (in Browser)
196
+ ws.onopen = () => {
197
+ bandwidthInterval = setInterval(() => {
198
+ bandwidthIn = bytesIn - lastBytesIn;
199
+ bandwidthOut = bytesOut - lastBytesOut;
200
+ lastBytesIn = bytesIn;
201
+ lastBytesOut = bytesOut;
202
+ }, 1000);
203
+ if (getStateHash) {
204
+ hashInterval = setInterval(() => {
205
+ if (!connected || !ws || ws.readyState !== 1)
206
+ return;
207
+ try {
208
+ const hash = getStateHash();
209
+ if (hash) {
210
+ const hashMsg = encodeSyncHash(roomId, hash, lastSyncSeq, lastSyncFrame);
211
+ bytesOut += hashMsg.byteLength;
212
+ ws.send(hashMsg);
213
+ }
214
+ }
215
+ catch (err) {
216
+ console.warn('[modd-network] Error getting state hash:', err);
217
+ }
218
+ }, 1000);
219
+ }
220
+ const joinMsg = JSON.stringify({ type: 'JOIN_ROOM', payload: { roomId } });
221
+ bytesOut += joinMsg.length;
222
+ ws.send(joinMsg);
223
+ };
224
+ ws.onerror = (e) => onError(`Failed to connect to ${nodeUrl}: ${e.message || 'Unknown error'}`);
225
+ ws.onclose = () => {
226
+ connected = false;
227
+ if (bandwidthInterval)
228
+ clearInterval(bandwidthInterval);
229
+ if (hashInterval)
230
+ clearInterval(hashInterval);
231
+ onDisconnect();
232
+ };
233
+ ws.onmessage = async (e) => {
234
+ // Handle various binary formats
235
+ let buffer;
236
+ try {
237
+ buffer = await toArrayBuffer(e.data);
238
+ }
239
+ catch (err) {
240
+ console.warn('[modd-network] Failed to read message data:', err);
241
+ return;
242
+ }
243
+ bytesIn += buffer.byteLength;
244
+ const msg = decodeBinaryMessage(buffer);
245
+ if (!msg) {
246
+ console.warn('[modd-network] Failed to decode binary message');
247
+ return;
248
+ }
249
+ switch (msg.type) {
250
+ case 'TICK': {
251
+ if (!connected) {
252
+ pendingTicks.push(msg);
253
+ break;
254
+ }
255
+ currentFrame = msg.frame;
256
+ lastSyncFrame = msg.frame;
257
+ if (msg.inputs && msg.inputs.length > 0) {
258
+ const maxSeq = Math.max(...msg.inputs.map(i => i.sequenceNumber || 0));
259
+ if (maxSeq > lastSyncSeq)
260
+ lastSyncSeq = maxSeq;
261
+ }
262
+ if (onTick) {
263
+ const newInputs = msg.inputs.filter(i => !deliveredSeqs.has(i.sequenceNumber));
264
+ newInputs.forEach(i => deliveredSeqs.add(i.sequenceNumber));
265
+ onTick(msg.frame, newInputs);
266
+ }
267
+ break;
268
+ }
269
+ case 'ERROR': {
270
+ if (msg.message === 'Room not found') {
271
+ const createMsg = JSON.stringify({
272
+ type: 'CREATE_ROOM',
273
+ payload: { roomId, appId, snapshot: initialSnapshot }
274
+ });
275
+ bytesOut += createMsg.length;
276
+ ws.send(createMsg);
277
+ }
278
+ else {
279
+ onError(msg.message);
280
+ }
281
+ break;
282
+ }
283
+ case 'ROOM_CREATED': {
284
+ connected = true;
285
+ if (user) {
286
+ const joinInputMsg = JSON.stringify({ type: 'SEND_INPUT', payload: { roomId, data: { type: 'join', user } } });
287
+ bytesOut += joinInputMsg.length;
288
+ ws.send(joinInputMsg);
289
+ }
290
+ onConnect(initialSnapshot, []);
291
+ break;
292
+ }
293
+ case 'INITIAL_STATE': {
294
+ initialStateReceived = {
295
+ snapshot: msg.snapshot,
296
+ inputs: msg.inputs || [],
297
+ frame: msg.frame
298
+ };
299
+ lastSyncFrame = msg.frame;
300
+ if (msg.inputs && msg.inputs.length > 0) {
301
+ const maxSeq = Math.max(...msg.inputs.map(i => i.sequenceNumber || 0));
302
+ if (maxSeq > lastSyncSeq)
303
+ lastSyncSeq = maxSeq;
304
+ }
305
+ break;
306
+ }
307
+ case 'ROOM_JOINED': {
308
+ connected = true;
309
+ if (user) {
310
+ const joinInputMsg = JSON.stringify({ type: 'SEND_INPUT', payload: { roomId, data: { type: 'join', user } } });
311
+ bytesOut += joinInputMsg.length;
312
+ ws.send(joinInputMsg);
313
+ }
314
+ const processInitialState = () => {
315
+ if (initialStateReceived) {
316
+ const { snapshot, inputs, frame } = initialStateReceived;
317
+ onConnect(snapshot, inputs);
318
+ const newInputs = inputs.filter(i => !deliveredSeqs.has(i.sequenceNumber));
319
+ newInputs.forEach(i => deliveredSeqs.add(i.sequenceNumber));
320
+ if (onTick && newInputs.length > 0) {
321
+ onTick(frame, newInputs, true);
322
+ }
323
+ else {
324
+ newInputs.forEach(i => onMessage(i.data, i.sequenceNumber));
325
+ }
326
+ initialStateReceived = null;
327
+ // Process buffered ticks
328
+ if (pendingTicks.length > 0) {
329
+ pendingTicks.sort((a, b) => a.frame - b.frame);
330
+ for (const tickMsg of pendingTicks) {
331
+ if (onTick) {
332
+ const tickInputs = tickMsg.inputs.filter(i => !deliveredSeqs.has(i.sequenceNumber));
333
+ tickInputs.forEach(i => deliveredSeqs.add(i.sequenceNumber));
334
+ if (tickInputs.length > 0 || tickMsg.frame > frame) {
335
+ onTick(tickMsg.frame, tickInputs);
336
+ }
337
+ }
338
+ }
339
+ pendingTicks = [];
340
+ }
341
+ }
342
+ else {
343
+ onConnect(null, []);
344
+ }
345
+ };
346
+ if (initialStateReceived) {
347
+ processInitialState();
348
+ }
349
+ else {
350
+ setTimeout(processInitialState, 100);
351
+ }
352
+ break;
353
+ }
354
+ case 'SNAPSHOT_UPDATE': {
355
+ if (options.onSnapshot) {
356
+ options.onSnapshot(msg.snapshot, msg.snapshotHash);
357
+ }
358
+ break;
359
+ }
360
+ case 'ROOM_LEFT': {
361
+ console.log(`[modd-network] Left room ${msg.roomId}`);
362
+ break;
363
+ }
364
+ }
365
+ };
366
+ // Auto leave on page unload
367
+ if (user && typeof window !== 'undefined') {
368
+ window.addEventListener('beforeunload', () => {
369
+ if (connected && ws) {
370
+ const leaveMsg = JSON.stringify({ type: 'SEND_INPUT', payload: { roomId, data: { type: 'leave', user } } });
371
+ bytesOut += leaveMsg.length;
372
+ ws.send(leaveMsg);
373
+ }
374
+ });
375
+ }
376
+ return {
377
+ send(data) {
378
+ if (connected && ws) {
379
+ const msg = JSON.stringify({ type: 'SEND_INPUT', payload: { roomId, data } });
380
+ bytesOut += msg.length;
381
+ ws.send(msg);
382
+ }
383
+ },
384
+ sendSnapshot(snapshot, hash) {
385
+ if (connected && ws) {
386
+ const msg = JSON.stringify({ type: 'SEND_SNAPSHOT', payload: { roomId, snapshot, hash } });
387
+ bytesOut += msg.length;
388
+ ws.send(msg);
389
+ }
390
+ },
391
+ leaveRoom() {
392
+ if (connected && user && ws) {
393
+ const msg = JSON.stringify({ type: 'SEND_INPUT', payload: { roomId, data: { type: 'leave', user } } });
394
+ bytesOut += msg.length;
395
+ ws.send(msg);
396
+ }
397
+ if (ws)
398
+ ws.close();
399
+ },
400
+ close() {
401
+ if (ws)
402
+ ws.close();
403
+ },
404
+ get connected() {
405
+ return connected;
406
+ },
407
+ get node() {
408
+ return nodeUrl ? (nodeUrl.match(/:(\d+)/)?.[1] || nodeUrl) : null;
409
+ },
410
+ get bandwidthIn() {
411
+ return bandwidthIn;
412
+ },
413
+ get bandwidthOut() {
414
+ return bandwidthOut;
415
+ },
416
+ get totalBytesIn() {
417
+ return bytesIn;
418
+ },
419
+ get totalBytesOut() {
420
+ return bytesOut;
421
+ },
422
+ get frame() {
423
+ return currentFrame;
424
+ }
425
+ };
426
+ }
427
+ catch (err) {
428
+ onError(`Failed to get node assignment: ${err.message}`);
429
+ return {
430
+ send() { },
431
+ sendSnapshot() { },
432
+ leaveRoom() { },
433
+ close() { },
434
+ get connected() { return false; },
435
+ get node() { return null; },
436
+ get bandwidthIn() { return 0; },
437
+ get bandwidthOut() { return 0; },
438
+ get totalBytesIn() { return 0; },
439
+ get totalBytesOut() { return 0; },
440
+ get frame() { return 0; }
441
+ };
442
+ }
443
+ }
444
+ // Legacy alias for backwards compatibility
445
+ exports.modd = connect;
446
+ // Browser global fallback
447
+ if (typeof window !== 'undefined') {
448
+ window.moddNetwork = { connect, modd: exports.modd };
449
+ }
package/package.json CHANGED
@@ -1,18 +1,22 @@
1
1
  {
2
2
  "name": "modd-network",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "SDK for connecting to mesh network for multiplayer applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist"
9
9
  ],
10
+ "browser": {
11
+ "ws": false
12
+ },
10
13
  "publishConfig": {
11
14
  "access": "public"
12
15
  },
13
16
  "scripts": {
14
17
  "build": "tsc",
15
- "dev": "tsx watch src/index.ts"
18
+ "build:browser": "node build-browser.js",
19
+ "prepublishOnly": "npm run build"
16
20
  },
17
21
  "keywords": [
18
22
  "mesh",
@@ -28,7 +32,7 @@
28
32
  "devDependencies": {
29
33
  "@types/node": "^20.10.6",
30
34
  "@types/ws": "^8.5.10",
31
- "tsx": "^4.7.0",
35
+ "esbuild": "^0.27.2",
32
36
  "typescript": "^5.3.3"
33
37
  }
34
38
  }