spoint 0.1.0 → 0.1.10
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/README.md +134 -209
- package/SKILL.md +95 -0
- package/apps/environment/index.js +200 -1
- package/apps/environment/models/decorative/.gitkeep +0 -0
- package/apps/environment/models/hazards/.gitkeep +0 -0
- package/apps/environment/models/interactive/.gitkeep +0 -0
- package/apps/environment/models/structures/.gitkeep +0 -0
- package/apps/environment/smartObjects.js +114 -0
- package/apps/interactable/index.js +155 -0
- package/apps/physics-crate/index.js +15 -9
- package/apps/power-crate/index.js +18 -12
- package/apps/tps-game/$GDUPI.vrm +0 -0
- package/apps/tps-game/Cleetus.vrm +0 -0
- package/apps/tps-game/index.js +185 -27
- package/apps/world/index.js +68 -22
- package/bin/create-app.js +337 -0
- package/client/ARControls.js +301 -0
- package/client/LoadingManager.js +117 -0
- package/client/MobileControls.js +1122 -0
- package/client/anim-lib.glb +0 -0
- package/client/animation.js +306 -0
- package/client/app.js +1341 -65
- package/client/camera.js +191 -33
- package/client/createLoadingScreen.js +69 -0
- package/client/editor/bridge.js +113 -0
- package/client/editor/css/main.css +794 -0
- package/client/editor/images/rotate.svg +4 -0
- package/client/editor/images/scale.svg +4 -0
- package/client/editor/images/translate.svg +4 -0
- package/client/editor/index.html +103 -0
- package/client/editor/js/Command.js +41 -0
- package/client/editor/js/Config.js +81 -0
- package/client/editor/js/Editor.js +785 -0
- package/client/editor/js/EditorControls.js +438 -0
- package/client/editor/js/History.js +321 -0
- package/client/editor/js/Loader.js +987 -0
- package/client/editor/js/LoaderUtils.js +90 -0
- package/client/editor/js/Menubar.Add.js +510 -0
- package/client/editor/js/Menubar.Edit.js +145 -0
- package/client/editor/js/Menubar.File.js +466 -0
- package/client/editor/js/Menubar.Help.js +73 -0
- package/client/editor/js/Menubar.Status.js +51 -0
- package/client/editor/js/Menubar.View.js +183 -0
- package/client/editor/js/Menubar.js +27 -0
- package/client/editor/js/Player.js +53 -0
- package/client/editor/js/Resizer.js +58 -0
- package/client/editor/js/Script.js +503 -0
- package/client/editor/js/Selector.js +102 -0
- package/client/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.BufferGeometry.js +115 -0
- package/client/editor/js/Sidebar.Geometry.CapsuleGeometry.js +97 -0
- package/client/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
- package/client/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
- package/client/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
- package/client/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
- package/client/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
- package/client/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
- package/client/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
- package/client/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
- package/client/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
- package/client/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
- package/client/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
- package/client/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
- package/client/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
- package/client/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
- package/client/editor/js/Sidebar.Geometry.js +332 -0
- package/client/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
- package/client/editor/js/Sidebar.Material.ColorProperty.js +87 -0
- package/client/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
- package/client/editor/js/Sidebar.Material.MapProperty.js +249 -0
- package/client/editor/js/Sidebar.Material.NumberProperty.js +60 -0
- package/client/editor/js/Sidebar.Material.Program.js +73 -0
- package/client/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
- package/client/editor/js/Sidebar.Material.js +751 -0
- package/client/editor/js/Sidebar.Object.Animation.js +102 -0
- package/client/editor/js/Sidebar.Object.js +898 -0
- package/client/editor/js/Sidebar.Project.App.js +165 -0
- package/client/editor/js/Sidebar.Project.Image.js +225 -0
- package/client/editor/js/Sidebar.Project.Materials.js +82 -0
- package/client/editor/js/Sidebar.Project.Renderer.js +144 -0
- package/client/editor/js/Sidebar.Project.Video.js +242 -0
- package/client/editor/js/Sidebar.Project.js +31 -0
- package/client/editor/js/Sidebar.Properties.js +73 -0
- package/client/editor/js/Sidebar.Scene.js +585 -0
- package/client/editor/js/Sidebar.Script.js +129 -0
- package/client/editor/js/Sidebar.Settings.History.js +146 -0
- package/client/editor/js/Sidebar.Settings.Shortcuts.js +175 -0
- package/client/editor/js/Sidebar.Settings.js +60 -0
- package/client/editor/js/Sidebar.js +41 -0
- package/client/editor/js/Storage.js +98 -0
- package/client/editor/js/Strings.js +2028 -0
- package/client/editor/js/Toolbar.js +84 -0
- package/client/editor/js/Viewport.Controls.js +92 -0
- package/client/editor/js/Viewport.Info.js +136 -0
- package/client/editor/js/Viewport.Pathtracer.js +91 -0
- package/client/editor/js/Viewport.ViewHelper.js +39 -0
- package/client/editor/js/Viewport.XR.js +222 -0
- package/client/editor/js/Viewport.js +900 -0
- package/client/editor/js/commands/AddObjectCommand.js +68 -0
- package/client/editor/js/commands/AddScriptCommand.js +75 -0
- package/client/editor/js/commands/Commands.js +23 -0
- package/client/editor/js/commands/MoveObjectCommand.js +111 -0
- package/client/editor/js/commands/MultiCmdsCommand.js +85 -0
- package/client/editor/js/commands/RemoveObjectCommand.js +88 -0
- package/client/editor/js/commands/RemoveScriptCommand.js +81 -0
- package/client/editor/js/commands/SetColorCommand.js +73 -0
- package/client/editor/js/commands/SetGeometryCommand.js +87 -0
- package/client/editor/js/commands/SetGeometryValueCommand.js +70 -0
- package/client/editor/js/commands/SetMaterialColorCommand.js +86 -0
- package/client/editor/js/commands/SetMaterialCommand.js +79 -0
- package/client/editor/js/commands/SetMaterialMapCommand.js +143 -0
- package/client/editor/js/commands/SetMaterialRangeCommand.js +91 -0
- package/client/editor/js/commands/SetMaterialValueCommand.js +90 -0
- package/client/editor/js/commands/SetMaterialVectorCommand.js +79 -0
- package/client/editor/js/commands/SetPositionCommand.js +84 -0
- package/client/editor/js/commands/SetRotationCommand.js +84 -0
- package/client/editor/js/commands/SetScaleCommand.js +84 -0
- package/client/editor/js/commands/SetSceneCommand.js +103 -0
- package/client/editor/js/commands/SetScriptValueCommand.js +80 -0
- package/client/editor/js/commands/SetShadowValueCommand.js +73 -0
- package/client/editor/js/commands/SetUuidCommand.js +70 -0
- package/client/editor/js/commands/SetValueCommand.js +75 -0
- package/client/editor/js/libs/acorn/acorn.js +3236 -0
- package/client/editor/js/libs/acorn/acorn_loose.js +1299 -0
- package/client/editor/js/libs/acorn/walk.js +344 -0
- package/client/editor/js/libs/app/index.html +57 -0
- package/client/editor/js/libs/app.js +251 -0
- package/client/editor/js/libs/codemirror/addon/dialog.css +32 -0
- package/client/editor/js/libs/codemirror/addon/dialog.js +163 -0
- package/client/editor/js/libs/codemirror/addon/show-hint.css +36 -0
- package/client/editor/js/libs/codemirror/addon/show-hint.js +529 -0
- package/client/editor/js/libs/codemirror/addon/tern.css +87 -0
- package/client/editor/js/libs/codemirror/addon/tern.js +750 -0
- package/client/editor/js/libs/codemirror/codemirror.css +344 -0
- package/client/editor/js/libs/codemirror/codemirror.js +9849 -0
- package/client/editor/js/libs/codemirror/mode/glsl.js +233 -0
- package/client/editor/js/libs/codemirror/mode/javascript.js +959 -0
- package/client/editor/js/libs/codemirror/theme/monokai.css +41 -0
- package/client/editor/js/libs/esprima.js +6401 -0
- package/client/editor/js/libs/jsonlint.js +453 -0
- package/client/editor/js/libs/signals.min.js +14 -0
- package/client/editor/js/libs/tern-threejs/threejs.js +5031 -0
- package/client/editor/js/libs/ternjs/comment.js +87 -0
- package/client/editor/js/libs/ternjs/def.js +588 -0
- package/client/editor/js/libs/ternjs/doc_comment.js +401 -0
- package/client/editor/js/libs/ternjs/infer.js +1635 -0
- package/client/editor/js/libs/ternjs/polyfill.js +80 -0
- package/client/editor/js/libs/ternjs/signal.js +26 -0
- package/client/editor/js/libs/ternjs/tern.js +993 -0
- package/client/editor/js/libs/ui.js +1346 -0
- package/client/editor/js/libs/ui.three.js +855 -0
- package/client/facial-animation.js +455 -0
- package/client/index.html +7 -4
- package/client/loading.css +147 -0
- package/client/loading.html +25 -0
- package/client/style.css +251 -0
- package/package.json +7 -3
- package/server.js +9 -1
- package/src/apps/AppContext.js +1 -1
- package/src/apps/AppLoader.js +50 -37
- package/src/apps/AppRuntime.js +32 -8
- package/src/client/InputHandler.js +233 -0
- package/src/client/JitterBuffer.js +207 -0
- package/src/client/KalmanFilter.js +125 -0
- package/src/client/MessageHandler.js +101 -0
- package/src/client/PhysicsNetworkClient.js +141 -68
- package/src/client/ReconnectManager.js +62 -0
- package/src/client/SmoothInterpolation.js +127 -0
- package/src/client/SnapshotProcessor.js +144 -0
- package/src/connection/ConnectionManager.js +21 -3
- package/src/connection/SessionStore.js +13 -3
- package/src/index.client.js +4 -6
- package/src/netcode/EventLog.js +29 -15
- package/src/netcode/LagCompensator.js +25 -26
- package/src/netcode/NetworkState.js +4 -1
- package/src/netcode/PhysicsIntegration.js +20 -6
- package/src/netcode/PlayerManager.js +10 -2
- package/src/netcode/SnapshotEncoder.js +66 -19
- package/src/netcode/TickSystem.js +13 -4
- package/src/physics/World.js +66 -13
- package/src/protocol/msgpack.js +90 -63
- package/src/sdk/ReloadHandlers.js +12 -2
- package/src/sdk/ReloadManager.js +5 -0
- package/src/sdk/ServerHandlers.js +50 -11
- package/src/sdk/StaticHandler.js +22 -6
- package/src/sdk/TickHandler.js +101 -34
- package/src/sdk/scaffold.js +28 -0
- package/src/sdk/server.js +59 -33
- package/src/shared/movement.js +2 -1
- package/src/spatial/Octree.js +5 -0
- package/apps/interactive-door/index.js +0 -33
- package/apps/patrol-npc/index.js +0 -37
- package/src/connection/QualityMonitor.js +0 -46
- package/src/debug/StateInspector.js +0 -42
- package/src/index.js +0 -1
- package/src/index.server.js +0 -27
- package/src/protocol/Codec.js +0 -60
- package/src/protocol/SequenceTracker.js +0 -71
- package/src/sdk/ClientMessageHandler.js +0 -80
- package/src/sdk/client.js +0 -122
- package/world/kaira.glb +0 -0
- package/world/schwust.glb +0 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { PredictionEngine } from './PredictionEngine.js'
|
|
2
1
|
import { pack, unpack } from '../protocol/msgpack.js'
|
|
3
2
|
import { MSG } from '../protocol/MessageTypes.js'
|
|
3
|
+
import { ReconnectManager } from './ReconnectManager.js'
|
|
4
|
+
import { SnapshotProcessor } from './SnapshotProcessor.js'
|
|
5
|
+
import { MessageHandler } from './MessageHandler.js'
|
|
4
6
|
|
|
5
7
|
export class PhysicsNetworkClient {
|
|
6
8
|
constructor(config = {}) {
|
|
7
|
-
this.config = { url: config.url || 'ws://localhost:
|
|
9
|
+
this.config = { url: config.url || 'ws://localhost:3000/ws', tickRate: config.tickRate || 128, predictionEnabled: config.predictionEnabled !== false, smoothInterpolation: config.smoothInterpolation !== false, debug: config.debug || false, ...config }
|
|
8
10
|
this.ws = null
|
|
9
|
-
this.playerId = null
|
|
10
11
|
this.connected = false
|
|
11
|
-
this.
|
|
12
|
-
this._playerStates = new Map()
|
|
13
|
-
this._entityStates = new Map()
|
|
12
|
+
this.state = { players: [], entities: [] }
|
|
14
13
|
this.lastSnapshotTick = 0
|
|
15
14
|
this.currentTick = 0
|
|
16
|
-
this.
|
|
15
|
+
this._pingSent = 0
|
|
17
16
|
this.heartbeatTimer = null
|
|
17
|
+
this._destroyed = false
|
|
18
|
+
this._visibilityListener = null
|
|
18
19
|
this.callbacks = {
|
|
19
20
|
onConnect: config.onConnect || (() => {}),
|
|
20
21
|
onDisconnect: config.onDisconnect || (() => {}),
|
|
@@ -28,41 +29,63 @@ export class PhysicsNetworkClient {
|
|
|
28
29
|
onWorldDef: config.onWorldDef || (() => {}),
|
|
29
30
|
onAppModule: config.onAppModule || (() => {}),
|
|
30
31
|
onAssetUpdate: config.onAssetUpdate || (() => {}),
|
|
31
|
-
onAppEvent: config.onAppEvent || (() => {})
|
|
32
|
+
onAppEvent: config.onAppEvent || (() => {}),
|
|
33
|
+
onHotReload: config.onHotReload || (() => {})
|
|
32
34
|
}
|
|
35
|
+
this._reconnect = new ReconnectManager(config)
|
|
36
|
+
this._snapProc = new SnapshotProcessor({ callbacks: this.callbacks })
|
|
37
|
+
this._msgHandler = new MessageHandler({ ...config, callbacks: this.callbacks })
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
get playerId() { return this._msgHandler.getPlayerId() }
|
|
41
|
+
|
|
35
42
|
async connect() {
|
|
36
|
-
return new Promise((resolve
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
let settled = false
|
|
37
45
|
try {
|
|
38
46
|
this.ws = new WebSocket(this.config.url)
|
|
39
47
|
this.ws.binaryType = 'arraybuffer'
|
|
40
|
-
this.ws.onopen = () => this._onOpen(resolve)
|
|
48
|
+
this.ws.onopen = () => { settled = true; this._onOpen(resolve) }
|
|
41
49
|
this.ws.onmessage = (event) => this.onMessage(event.data)
|
|
42
50
|
this.ws.onclose = () => this._onClose()
|
|
43
|
-
this.ws.onerror = (
|
|
44
|
-
} catch (
|
|
51
|
+
this.ws.onerror = () => { if (!settled) { settled = true; resolve() } }
|
|
52
|
+
} catch (e) { resolve() }
|
|
45
53
|
})
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
_onOpen(resolve) {
|
|
49
57
|
this.connected = true
|
|
50
58
|
this._startHeartbeat()
|
|
59
|
+
this._reconnect.sendReconnectMessage(this.ws)
|
|
60
|
+
this._reconnect.onConnected()
|
|
51
61
|
this.callbacks.onConnect()
|
|
52
|
-
resolve()
|
|
62
|
+
resolve?.()
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
_onClose() {
|
|
56
66
|
this.connected = false
|
|
57
67
|
this._stopHeartbeat()
|
|
58
68
|
this.callbacks.onDisconnect()
|
|
69
|
+
this._reconnect.onDisconnected(() => this._doReconnect())
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
_doReconnect() {
|
|
73
|
+
try {
|
|
74
|
+
this.ws = new WebSocket(this.config.url)
|
|
75
|
+
this.ws.binaryType = 'arraybuffer'
|
|
76
|
+
this.ws.onopen = () => this._onOpen(null)
|
|
77
|
+
this.ws.onmessage = (event) => this.onMessage(event.data)
|
|
78
|
+
this.ws.onclose = () => this._onClose()
|
|
79
|
+
this.ws.onerror = () => {}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
this._reconnect.onDisconnected(() => this._doReconnect())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
62
84
|
|
|
63
85
|
sendInput(input) {
|
|
64
86
|
if (!this._isOpen()) return
|
|
65
|
-
|
|
87
|
+
const predEngine = this._msgHandler.getPredEngine()
|
|
88
|
+
if (this.config.predictionEnabled && predEngine) predEngine.addInput(input)
|
|
66
89
|
this.ws.send(pack({ type: MSG.INPUT, payload: { input } }))
|
|
67
90
|
}
|
|
68
91
|
|
|
@@ -71,89 +94,120 @@ export class PhysicsNetworkClient {
|
|
|
71
94
|
this.ws.send(pack({ type: MSG.APP_EVENT, payload: { type: 'fire', shooterId: this.playerId, ...data } }))
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
sendReload() {
|
|
98
|
+
if (!this._isOpen()) return
|
|
99
|
+
this.ws.send(pack({ type: MSG.APP_EVENT, payload: { type: 'reload', playerId: this.playerId } }))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_isOpen() {
|
|
103
|
+
return this.ws && this.ws.readyState === WebSocket.OPEN
|
|
104
|
+
}
|
|
75
105
|
|
|
76
106
|
onMessage(data) {
|
|
77
107
|
try {
|
|
78
108
|
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
|
79
109
|
const msg = unpack(bytes)
|
|
80
110
|
this._handleMessage(msg.type, msg.payload || {})
|
|
81
|
-
} catch (e) {
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error('[client] parse error:', e)
|
|
113
|
+
}
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
_handleMessage(type, payload) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
} else if (
|
|
91
|
-
this.
|
|
92
|
-
} else if (type === MSG.
|
|
93
|
-
this.
|
|
94
|
-
this.callbacks.onPlayerLeft(payload.playerId)
|
|
95
|
-
} else if (type === MSG.WORLD_DEF) {
|
|
96
|
-
if (payload.movement && this._predEngine) this._predEngine.setMovement(payload.movement)
|
|
97
|
-
if (payload.gravity && this._predEngine) this._predEngine.setGravity(payload.gravity)
|
|
98
|
-
this.callbacks.onWorldDef?.(payload)
|
|
99
|
-
} else if (type === MSG.APP_EVENT) {
|
|
100
|
-
this.callbacks.onAppEvent?.(payload)
|
|
101
|
-
} else if (type === MSG.HOT_RELOAD || type === MSG.APP_MODULE || type === MSG.ASSET_UPDATE) {
|
|
102
|
-
const cb = { [MSG.HOT_RELOAD]: 'onHotReload', [MSG.APP_MODULE]: 'onAppModule', [MSG.ASSET_UPDATE]: 'onAssetUpdate' }[type]
|
|
103
|
-
this.callbacks[cb]?.(payload)
|
|
117
|
+
const result = this._msgHandler.handleMessage(type, payload, this._snapProc)
|
|
118
|
+
if (type === MSG.HANDSHAKE_ACK && result?.sessionToken) {
|
|
119
|
+
this._reconnect.setSessionToken(result.sessionToken)
|
|
120
|
+
} else if (type === MSG.RECONNECT_ACK && result?.sessionToken) {
|
|
121
|
+
this._reconnect.setSessionToken(result.sessionToken)
|
|
122
|
+
} else if (result?.invalidate) {
|
|
123
|
+
this._reconnect.invalidateSession()
|
|
124
|
+
} else if (result && (type === MSG.SNAPSHOT || type === MSG.STATE_CORRECTION || type === MSG.STATE_RECOVERY)) {
|
|
125
|
+
this._onSnapshot(result)
|
|
104
126
|
}
|
|
105
127
|
}
|
|
106
128
|
|
|
107
129
|
_onSnapshot(data) {
|
|
108
130
|
this.lastSnapshotTick = this.currentTick = data.tick || 0
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (playerId === this.playerId && this.config.predictionEnabled && this._predEngine) {
|
|
114
|
-
this._predEngine.onServerSnapshot({ players: [state] }, this.currentTick)
|
|
115
|
-
}
|
|
131
|
+
const snapshotForBuffer = this._snapProc.processSnapshot(data, this.currentTick)
|
|
132
|
+
const smoothInterp = this._msgHandler.getSmoothInterp()
|
|
133
|
+
if (smoothInterp) {
|
|
134
|
+
smoothInterp.addSnapshot(snapshotForBuffer)
|
|
116
135
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
const predEngine = this._msgHandler.getPredEngine()
|
|
137
|
+
if (this.playerId && this.config.predictionEnabled && predEngine) {
|
|
138
|
+
const localState = this._snapProc.getPlayerState(this.playerId)
|
|
139
|
+
if (localState) {
|
|
140
|
+
predEngine.onServerSnapshot({ players: [localState] }, this.currentTick)
|
|
141
|
+
}
|
|
121
142
|
}
|
|
122
|
-
this.state.players = Array.from(this.
|
|
123
|
-
this.state.entities = Array.from(this.
|
|
143
|
+
this.state.players = Array.from(this._snapProc.getAllPlayerStates().values())
|
|
144
|
+
this.state.entities = Array.from(this._snapProc.getAllEntities().values())
|
|
124
145
|
this.callbacks.onSnapshot(data)
|
|
125
146
|
this.callbacks.onStateUpdate(this.state)
|
|
126
147
|
this._render()
|
|
127
148
|
}
|
|
128
149
|
|
|
129
|
-
_parsePlayer(p) {
|
|
130
|
-
if (Array.isArray(p)) return { playerId: p[0], state: { id: p[0], position: [p[1], p[2], p[3]], rotation: [p[4], p[5], p[6], p[7]], velocity: [p[8], p[9], p[10]], onGround: p[11] === 1, health: p[12], inputSequence: p[13] } }
|
|
131
|
-
return { playerId: p.id || p.i, state: { id: p.id || p.i, position: p.position || [0, 0, 0], rotation: p.rotation || [0, 0, 0, 1], velocity: p.velocity || [0, 0, 0], onGround: p.onGround ?? false, health: p.health ?? 100 } }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
_parseEntity(e) {
|
|
135
|
-
if (Array.isArray(e)) return { entityId: e[0], state: { id: e[0], model: e[1], position: [e[2], e[3], e[4]], rotation: [e[5], e[6], e[7], e[8]], bodyType: e[9], custom: e[10] } }
|
|
136
|
-
return { entityId: e.id, state: { id: e.id, model: e.model, position: e.position || [0, 0, 0], rotation: e.rotation || [0, 0, 0, 1], bodyType: e.bodyType || 'static', custom: e.custom || null } }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
150
|
_render() {
|
|
140
151
|
const displayStates = new Map()
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
const smoothInterp = this._msgHandler.getSmoothInterp()
|
|
153
|
+
if (smoothInterp) {
|
|
154
|
+
const smoothState = smoothInterp.getDisplayState()
|
|
155
|
+
for (const p of smoothState.players) {
|
|
156
|
+
displayStates.set(p.id, p)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
const predEngine = this._msgHandler.getPredEngine()
|
|
160
|
+
for (const [playerId, serverState] of this._snapProc.getAllPlayerStates()) {
|
|
161
|
+
if (playerId === this.playerId && this.config.predictionEnabled && predEngine) {
|
|
162
|
+
displayStates.set(playerId, predEngine.getDisplayState(this.currentTick, 0))
|
|
163
|
+
} else {
|
|
164
|
+
displayStates.set(playerId, serverState)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
143
167
|
}
|
|
144
168
|
this.callbacks.onRender(displayStates)
|
|
145
169
|
}
|
|
146
170
|
|
|
171
|
+
getSmoothState() {
|
|
172
|
+
const smoothInterp = this._msgHandler.getSmoothInterp()
|
|
173
|
+
if (smoothInterp) {
|
|
174
|
+
return smoothInterp.getDisplayState()
|
|
175
|
+
}
|
|
176
|
+
return { players: this.state.players, entities: this.state.entities }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getRTT() {
|
|
180
|
+
return this._msgHandler.getRTT()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getBufferHealth() {
|
|
184
|
+
return this._msgHandler.getBufferHealth()
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
getLocalState() {
|
|
148
|
-
|
|
188
|
+
const predEngine = this._msgHandler.getPredEngine()
|
|
189
|
+
return this.config.predictionEnabled && predEngine ? predEngine.localState : this._snapProc.getPlayerState(this.playerId)
|
|
149
190
|
}
|
|
150
191
|
|
|
151
|
-
getRemoteState(playerId) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
getRemoteState(playerId) {
|
|
193
|
+
return this._snapProc.getPlayerState(playerId)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getAllStates() {
|
|
197
|
+
return this._snapProc.getAllPlayerStates()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getEntity(entityId) {
|
|
201
|
+
return this._snapProc.getEntity(entityId)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getAllEntities() {
|
|
205
|
+
return this._snapProc.getAllEntities()
|
|
206
|
+
}
|
|
155
207
|
|
|
156
208
|
disconnect() {
|
|
209
|
+
this._destroyed = true
|
|
210
|
+
this._reconnect.clear()
|
|
157
211
|
this._stopHeartbeat()
|
|
158
212
|
if (this.ws) this.ws.close()
|
|
159
213
|
}
|
|
@@ -161,11 +215,30 @@ export class PhysicsNetworkClient {
|
|
|
161
215
|
_startHeartbeat() {
|
|
162
216
|
this._stopHeartbeat()
|
|
163
217
|
this.heartbeatTimer = setInterval(() => {
|
|
164
|
-
if (this._isOpen())
|
|
218
|
+
if (this._isOpen()) {
|
|
219
|
+
this._pingSent = Date.now()
|
|
220
|
+
this.ws.send(pack({ type: MSG.HEARTBEAT, payload: { timestamp: this._pingSent } }))
|
|
221
|
+
}
|
|
165
222
|
}, 1000)
|
|
223
|
+
if (typeof document !== 'undefined' && !this._visibilityListener) {
|
|
224
|
+
this._visibilityListener = () => {
|
|
225
|
+
if (!document.hidden && this._isOpen()) {
|
|
226
|
+
this._pingSent = Date.now()
|
|
227
|
+
this.ws.send(pack({ type: MSG.HEARTBEAT, payload: { timestamp: this._pingSent } }))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
document.addEventListener('visibilitychange', this._visibilityListener)
|
|
231
|
+
}
|
|
166
232
|
}
|
|
167
233
|
|
|
168
234
|
_stopHeartbeat() {
|
|
169
|
-
if (this.heartbeatTimer) {
|
|
235
|
+
if (this.heartbeatTimer) {
|
|
236
|
+
clearInterval(this.heartbeatTimer)
|
|
237
|
+
this.heartbeatTimer = null
|
|
238
|
+
}
|
|
239
|
+
if (this._visibilityListener && typeof document !== 'undefined') {
|
|
240
|
+
document.removeEventListener('visibilitychange', this._visibilityListener)
|
|
241
|
+
this._visibilityListener = null
|
|
242
|
+
}
|
|
170
243
|
}
|
|
171
244
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { pack } from '../protocol/msgpack.js'
|
|
2
|
+
import { MSG } from '../protocol/MessageTypes.js'
|
|
3
|
+
|
|
4
|
+
export class ReconnectManager {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this._sessionToken = null
|
|
7
|
+
this._reconnectAttempts = 0
|
|
8
|
+
this._reconnectTimer = null
|
|
9
|
+
this._reconnecting = false
|
|
10
|
+
this._maxReconnectDelay = config.maxReconnectDelay || 5000
|
|
11
|
+
this._destroyed = false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setSessionToken(token) {
|
|
15
|
+
this._sessionToken = token
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isReconnecting() {
|
|
19
|
+
return this._reconnecting
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sendReconnectMessage(ws) {
|
|
23
|
+
if (this._sessionToken && this._reconnecting && ws?.readyState === WebSocket.OPEN) {
|
|
24
|
+
ws.send(pack({ type: MSG.RECONNECT, payload: { sessionToken: this._sessionToken } }))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onConnected() {
|
|
29
|
+
this._reconnectAttempts = 0
|
|
30
|
+
this._reconnecting = false
|
|
31
|
+
if (this._reconnectTimer) {
|
|
32
|
+
clearTimeout(this._reconnectTimer)
|
|
33
|
+
this._reconnectTimer = null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onDisconnected(callback) {
|
|
38
|
+
if (this._destroyed) return
|
|
39
|
+
if (this._reconnectTimer) return
|
|
40
|
+
const delay = Math.min(1000 * Math.pow(1.5, this._reconnectAttempts), this._maxReconnectDelay)
|
|
41
|
+
this._reconnectAttempts++
|
|
42
|
+
this._reconnecting = true
|
|
43
|
+
this._reconnectTimer = setTimeout(() => {
|
|
44
|
+
this._reconnectTimer = null
|
|
45
|
+
if (!this._destroyed) callback()
|
|
46
|
+
}, delay)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clear() {
|
|
50
|
+
this._destroyed = true
|
|
51
|
+
if (this._reconnectTimer) {
|
|
52
|
+
clearTimeout(this._reconnectTimer)
|
|
53
|
+
this._reconnectTimer = null
|
|
54
|
+
}
|
|
55
|
+
this._sessionToken = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
invalidateSession() {
|
|
59
|
+
this._sessionToken = null
|
|
60
|
+
this._reconnecting = false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { KalmanFilter3D, SmoothStateTracker } from './KalmanFilter.js'
|
|
2
|
+
import { JitterBuffer } from './JitterBuffer.js'
|
|
3
|
+
|
|
4
|
+
export class SmoothInterpolation {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.jitterBuffer = new JitterBuffer(config.jitter || {})
|
|
7
|
+
this.playerFilters = new Map()
|
|
8
|
+
this.entityFilters = new Map()
|
|
9
|
+
this.kalmanConfig = config.kalman || {
|
|
10
|
+
processNoise: 0.08,
|
|
11
|
+
measurementNoise: 0.3,
|
|
12
|
+
uncertainty: 0.5
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.lastFrameTime = Date.now()
|
|
16
|
+
this.localPlayerId = null
|
|
17
|
+
this.predictionEnabled = config.predictionEnabled !== false
|
|
18
|
+
this.extrapolationLimit = config.extrapolationLimit || 100
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setLocalPlayer(id) {
|
|
22
|
+
this.localPlayerId = id
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addSnapshot(snapshot) {
|
|
26
|
+
this.jitterBuffer.addSnapshot(snapshot)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getDisplayState(now = Date.now()) {
|
|
30
|
+
const snapshot = this.jitterBuffer.getSnapshotToRender(now)
|
|
31
|
+
if (!snapshot) return { players: [], entities: [] }
|
|
32
|
+
|
|
33
|
+
const dt = Math.min((now - this.lastFrameTime) / 1000, 0.1)
|
|
34
|
+
this.lastFrameTime = now
|
|
35
|
+
|
|
36
|
+
const displayPlayers = []
|
|
37
|
+
for (const player of snapshot.players || []) {
|
|
38
|
+
if (player.id === this.localPlayerId && this.predictionEnabled) {
|
|
39
|
+
displayPlayers.push(player)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const smoothed = this._smoothPlayer(player, dt)
|
|
44
|
+
displayPlayers.push(smoothed)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const displayEntities = []
|
|
48
|
+
for (const entity of snapshot.entities || []) {
|
|
49
|
+
const smoothed = this._smoothEntity(entity, dt)
|
|
50
|
+
displayEntities.push(smoothed)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { players: displayPlayers, entities: displayEntities }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_smoothPlayer(player, dt) {
|
|
57
|
+
let filter = this.playerFilters.get(player.id)
|
|
58
|
+
if (!filter) {
|
|
59
|
+
filter = new KalmanFilter3D(this.kalmanConfig)
|
|
60
|
+
this.playerFilters.set(player.id, filter)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const state = filter.update(player.position, player.velocity)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...player,
|
|
67
|
+
position: state.position,
|
|
68
|
+
velocity: state.velocity
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_smoothEntity(entity, dt) {
|
|
73
|
+
let filter = this.entityFilters.get(entity.id)
|
|
74
|
+
if (!filter) {
|
|
75
|
+
filter = new KalmanFilter3D(this.kalmanConfig)
|
|
76
|
+
this.entityFilters.set(entity.id, filter)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const state = filter.update(entity.position)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...entity,
|
|
83
|
+
position: state.position
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
predictStep(dt) {
|
|
88
|
+
for (const [id, filter] of this.playerFilters) {
|
|
89
|
+
filter.predict(dt)
|
|
90
|
+
}
|
|
91
|
+
for (const [id, filter] of this.entityFilters) {
|
|
92
|
+
filter.predict(dt)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removePlayer(id) {
|
|
97
|
+
this.playerFilters.delete(id)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
removeEntity(id) {
|
|
101
|
+
this.entityFilters.delete(id)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updateRTT(pingTime, pongTime) {
|
|
105
|
+
this.jitterBuffer.updateRTT(pingTime, pongTime)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getRTT() {
|
|
109
|
+
return this.jitterBuffer.getRTT()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getBufferHealth() {
|
|
113
|
+
return this.jitterBuffer.getBufferHealth()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
reset() {
|
|
117
|
+
this.jitterBuffer.clear()
|
|
118
|
+
this.playerFilters.clear()
|
|
119
|
+
this.entityFilters.clear()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setConfig(config) {
|
|
123
|
+
if (config.kalman) {
|
|
124
|
+
this.kalmanConfig = { ...this.kalmanConfig, ...config.kalman }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export class SnapshotProcessor {
|
|
2
|
+
constructor(config = {}) {
|
|
3
|
+
this._playerStates = new Map()
|
|
4
|
+
this._entityStates = new Map()
|
|
5
|
+
this.lastSnapshotTick = 0
|
|
6
|
+
this._callbacks = config.callbacks || {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
processSnapshot(data, tick) {
|
|
10
|
+
this.lastSnapshotTick = tick
|
|
11
|
+
|
|
12
|
+
const snapshotForBuffer = {
|
|
13
|
+
tick: data.tick || 0,
|
|
14
|
+
timestamp: data.timestamp || Date.now(),
|
|
15
|
+
players: [],
|
|
16
|
+
entities: []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const seenPlayers = new Set()
|
|
20
|
+
for (const p of data.players || []) {
|
|
21
|
+
const { playerId, state } = this._parsePlayer(p)
|
|
22
|
+
seenPlayers.add(playerId)
|
|
23
|
+
if (!this._playerStates.has(playerId)) {
|
|
24
|
+
this._callbacks.onPlayerJoined?.(playerId, state)
|
|
25
|
+
}
|
|
26
|
+
this._playerStates.set(playerId, state)
|
|
27
|
+
snapshotForBuffer.players.push(state)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const playerId of this._playerStates.keys()) {
|
|
31
|
+
if (!seenPlayers.has(playerId)) {
|
|
32
|
+
this._playerStates.delete(playerId)
|
|
33
|
+
this._callbacks.onPlayerLeft?.(playerId)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._processEntities(data, snapshotForBuffer)
|
|
38
|
+
return snapshotForBuffer
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_processEntities(data, snapshotForBuffer) {
|
|
42
|
+
if (data.delta) {
|
|
43
|
+
for (const e of data.entities || []) {
|
|
44
|
+
const { entityId, state } = this._parseEntity(e)
|
|
45
|
+
if (!this._entityStates.has(entityId)) {
|
|
46
|
+
this._callbacks.onEntityAdded?.(entityId, state)
|
|
47
|
+
}
|
|
48
|
+
this._entityStates.set(entityId, state)
|
|
49
|
+
snapshotForBuffer.entities.push(state)
|
|
50
|
+
}
|
|
51
|
+
if (data.removed) {
|
|
52
|
+
for (const eid of data.removed) {
|
|
53
|
+
if (this._entityStates.has(eid)) {
|
|
54
|
+
this._entityStates.delete(eid)
|
|
55
|
+
this._callbacks.onEntityRemoved?.(eid)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const seen = new Set()
|
|
61
|
+
for (const e of data.entities || []) {
|
|
62
|
+
const { entityId, state } = this._parseEntity(e)
|
|
63
|
+
seen.add(entityId)
|
|
64
|
+
if (!this._entityStates.has(entityId)) {
|
|
65
|
+
this._callbacks.onEntityAdded?.(entityId, state)
|
|
66
|
+
}
|
|
67
|
+
this._entityStates.set(entityId, state)
|
|
68
|
+
snapshotForBuffer.entities.push(state)
|
|
69
|
+
}
|
|
70
|
+
for (const eid of this._entityStates.keys()) {
|
|
71
|
+
if (!seen.has(eid)) {
|
|
72
|
+
this._entityStates.delete(eid)
|
|
73
|
+
this._callbacks.onEntityRemoved?.(eid)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_parsePlayer(p) {
|
|
80
|
+
if (Array.isArray(p)) {
|
|
81
|
+
return {
|
|
82
|
+
playerId: p[0],
|
|
83
|
+
state: {
|
|
84
|
+
id: p[0], position: [p[1], p[2], p[3]], rotation: [p[4], p[5], p[6], p[7]],
|
|
85
|
+
velocity: [p[8], p[9], p[10]], onGround: p[11] === 1, health: p[12],
|
|
86
|
+
inputSequence: p[13], crouch: p[14] || 0,
|
|
87
|
+
lookPitch: (p[15] || 0) / 255 * 2 * Math.PI - Math.PI,
|
|
88
|
+
lookYaw: (p[16] || 0) / 255 * 2 * Math.PI
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
playerId: p.id || p.i,
|
|
94
|
+
state: {
|
|
95
|
+
id: p.id || p.i, position: p.position || [0, 0, 0], rotation: p.rotation || [0, 0, 0, 1],
|
|
96
|
+
velocity: p.velocity || [0, 0, 0], onGround: p.onGround ?? false, health: p.health ?? 100
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_parseEntity(e) {
|
|
102
|
+
if (Array.isArray(e)) {
|
|
103
|
+
return {
|
|
104
|
+
entityId: e[0],
|
|
105
|
+
state: {
|
|
106
|
+
id: e[0], model: e[1], position: [e[2], e[3], e[4]], rotation: [e[5], e[6], e[7], e[8]],
|
|
107
|
+
bodyType: e[9], custom: e[10]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
entityId: e.id,
|
|
113
|
+
state: {
|
|
114
|
+
id: e.id, model: e.model, position: e.position || [0, 0, 0], rotation: e.rotation || [0, 0, 0, 1],
|
|
115
|
+
bodyType: e.bodyType || 'static', custom: e.custom || null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getPlayerState(playerId) {
|
|
121
|
+
return this._playerStates.get(playerId)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getAllPlayerStates() {
|
|
125
|
+
return new Map(this._playerStates)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getEntity(entityId) {
|
|
129
|
+
return this._entityStates.get(entityId)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getAllEntities() {
|
|
133
|
+
return new Map(this._entityStates)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
removePlayer(playerId) {
|
|
137
|
+
this._playerStates.delete(playerId)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
clear() {
|
|
141
|
+
this._playerStates.clear()
|
|
142
|
+
this._entityStates.clear()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -31,14 +31,14 @@ export class ConnectionManager extends EventEmitter {
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
transport.on('close', () => {
|
|
34
|
-
this.removeClient(clientId)
|
|
35
34
|
this.emit('disconnect', clientId, 'closed')
|
|
35
|
+
this.removeClient(clientId)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
transport.on('error', (err) => {
|
|
39
39
|
console.error(`[connection] transport error for ${clientId}:`, err.message)
|
|
40
|
-
this.removeClient(clientId)
|
|
41
40
|
this.emit('disconnect', clientId, 'error')
|
|
41
|
+
this.removeClient(clientId)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
this.clients.set(clientId, client)
|
|
@@ -52,8 +52,8 @@ export class ConnectionManager extends EventEmitter {
|
|
|
52
52
|
if (!client) return
|
|
53
53
|
const age = Date.now() - client.lastHeartbeat
|
|
54
54
|
if (age > this.heartbeatTimeout) {
|
|
55
|
-
this.removeClient(clientId)
|
|
56
55
|
this.emit('disconnect', clientId, 'timeout')
|
|
56
|
+
this.removeClient(clientId)
|
|
57
57
|
return
|
|
58
58
|
}
|
|
59
59
|
const timer = setTimeout(check, this.heartbeatInterval)
|
|
@@ -63,6 +63,11 @@ export class ConnectionManager extends EventEmitter {
|
|
|
63
63
|
this.timers.set(`hb-${clientId}`, timer)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
resetHeartbeat(clientId) {
|
|
67
|
+
const client = this.clients.get(clientId)
|
|
68
|
+
if (client) client.lastHeartbeat = Date.now()
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
removeClient(clientId) {
|
|
67
72
|
const client = this.clients.get(clientId)
|
|
68
73
|
if (!client) return
|
|
@@ -75,6 +80,19 @@ export class ConnectionManager extends EventEmitter {
|
|
|
75
80
|
this.timers.delete(`hb-${clientId}`)
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
detachClient(clientId) {
|
|
84
|
+
const client = this.clients.get(clientId)
|
|
85
|
+
if (client?.transport) {
|
|
86
|
+
client.transport.removeAllListeners('message')
|
|
87
|
+
client.transport.removeAllListeners('close')
|
|
88
|
+
client.transport.removeAllListeners('error')
|
|
89
|
+
}
|
|
90
|
+
this.clients.delete(clientId)
|
|
91
|
+
const timer = this.timers.get(`hb-${clientId}`)
|
|
92
|
+
if (timer) clearTimeout(timer)
|
|
93
|
+
this.timers.delete(`hb-${clientId}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
getClient(clientId) {
|
|
79
97
|
return this.clients.get(clientId)
|
|
80
98
|
}
|