@shipload/sdk 0.7.1 → 1.0.0-next.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/lib/shipload.d.ts +2730 -287
- package/lib/shipload.js +10862 -2229
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +10434 -2171
- package/lib/shipload.m.js.map +1 -1
- package/package.json +11 -20
- package/src/capabilities/crafting.ts +22 -0
- package/src/capabilities/gathering.ts +36 -0
- package/src/capabilities/guards.ts +38 -0
- package/src/capabilities/hauling.ts +22 -0
- package/src/capabilities/index.ts +8 -0
- package/src/capabilities/loading.ts +8 -0
- package/src/capabilities/modules.ts +86 -0
- package/src/capabilities/movement.ts +29 -0
- package/src/capabilities/storage.ts +159 -0
- package/src/contracts/server.ts +1389 -285
- package/src/data/capabilities.ts +408 -0
- package/src/data/catalog.ts +135 -0
- package/src/data/categories.ts +55 -0
- package/src/data/colors.ts +84 -0
- package/src/data/entities.json +50 -0
- package/src/data/item-ids.ts +75 -0
- package/src/data/items.json +252 -0
- package/src/data/locations.ts +53 -0
- package/src/data/metadata.ts +208 -0
- package/src/data/nebula-adjectives.json +211 -0
- package/src/data/nebula-nouns.json +151 -0
- package/src/data/recipes-runtime.ts +65 -0
- package/src/data/recipes.json +878 -0
- package/src/data/syllables.json +1790 -0
- package/src/data/tiers.ts +45 -0
- package/src/derivation/crafting.ts +350 -0
- package/src/derivation/index.ts +32 -0
- package/src/derivation/location-size.ts +15 -0
- package/src/derivation/resources.ts +112 -0
- package/src/derivation/stats.ts +146 -0
- package/src/derivation/strata.ts +43 -0
- package/src/derivation/stratum.ts +134 -0
- package/src/derivation/tiers.ts +54 -0
- package/src/entities/cargo-utils.ts +84 -0
- package/src/entities/container.ts +108 -0
- package/src/entities/entity-inventory.ts +39 -0
- package/src/entities/gamestate.ts +152 -0
- package/src/entities/inventory-accessor.ts +42 -0
- package/src/entities/location.ts +60 -0
- package/src/entities/makers.ts +196 -0
- package/src/entities/player.ts +15 -0
- package/src/entities/ship-deploy.ts +258 -0
- package/src/entities/ship.ts +204 -0
- package/src/entities/warehouse.ts +119 -0
- package/src/errors.ts +100 -9
- package/src/format.ts +12 -0
- package/src/index-module.ts +317 -7
- package/src/managers/actions.ts +250 -0
- package/src/managers/base.ts +25 -0
- package/src/managers/context.ts +114 -0
- package/src/managers/entities.ts +103 -0
- package/src/managers/epochs.ts +47 -0
- package/src/managers/index.ts +9 -0
- package/src/managers/locations.ts +68 -0
- package/src/managers/players.ts +13 -0
- package/src/nft/description.ts +176 -0
- package/src/nft/deserializers.ts +83 -0
- package/src/nft/index.ts +2 -0
- package/src/resolution/describe-module.ts +166 -0
- package/src/resolution/display-name.ts +39 -0
- package/src/resolution/resolve-item.ts +358 -0
- package/src/scheduling/accessor.ts +82 -0
- package/src/{epoch.ts → scheduling/epoch.ts} +1 -1
- package/src/scheduling/projection.ts +463 -0
- package/src/scheduling/schedule.ts +179 -0
- package/src/shipload.ts +47 -160
- package/src/subscriptions/connection.ts +154 -0
- package/src/subscriptions/debug.ts +17 -0
- package/src/subscriptions/index.ts +5 -0
- package/src/subscriptions/manager.ts +240 -0
- package/src/subscriptions/mappers.ts +28 -0
- package/src/subscriptions/types.ts +143 -0
- package/src/travel/travel.ts +500 -0
- package/src/types/capabilities.ts +76 -0
- package/src/types/entity-traits.ts +69 -0
- package/src/types/entity.ts +39 -0
- package/src/types/index.ts +3 -0
- package/src/types.ts +140 -35
- package/src/{hash.ts → utils/hash.ts} +2 -2
- package/src/utils/system.ts +168 -0
- package/src/goods.ts +0 -124
- package/src/market.ts +0 -214
- package/src/rolls.ts +0 -8
- package/src/ship.ts +0 -36
- package/src/state.ts +0 -0
- package/src/syllables.ts +0 -1184
- package/src/system.ts +0 -37
- package/src/travel.ts +0 -259
package/src/shipload.ts
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
APIClient,
|
|
3
|
-
Bytes,
|
|
4
|
-
Checksum256,
|
|
5
|
-
Name,
|
|
6
|
-
NameType,
|
|
7
|
-
Serializer,
|
|
8
|
-
UInt16Type,
|
|
9
|
-
UInt64,
|
|
10
|
-
UInt64Type,
|
|
11
|
-
} from '@wharfkit/antelope'
|
|
12
|
-
import {Coordinates, Distance, GoodPrice} from './types'
|
|
13
|
-
import {marketprice, marketprices} from './market'
|
|
1
|
+
import {APIClient} from '@wharfkit/antelope'
|
|
14
2
|
import {PlatformContract, ServerContract} from './contracts'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
3
|
+
import type {ChainDefinition} from '@wharfkit/common'
|
|
4
|
+
import ContractKit, {type Contract} from '@wharfkit/contract'
|
|
5
|
+
|
|
6
|
+
import {GameContext} from './managers/context'
|
|
7
|
+
import type {EntitiesManager} from './managers/entities'
|
|
8
|
+
import type {PlayersManager} from './managers/players'
|
|
9
|
+
import type {LocationsManager} from './managers/locations'
|
|
10
|
+
import type {EpochsManager} from './managers/epochs'
|
|
11
|
+
import type {ActionsManager} from './managers/actions'
|
|
12
|
+
import type {SubscriptionsManager} from './subscriptions/manager'
|
|
13
|
+
import type {GameState} from './entities/gamestate'
|
|
23
14
|
|
|
24
15
|
interface ShiploadOptions {
|
|
25
16
|
platformContractName?: string
|
|
26
17
|
serverContractName?: string
|
|
27
18
|
client?: APIClient
|
|
19
|
+
subscriptionsUrl?: string
|
|
28
20
|
}
|
|
29
21
|
|
|
30
22
|
interface ShiploadConstructorOptions extends ShiploadOptions {
|
|
@@ -33,22 +25,25 @@ interface ShiploadConstructorOptions extends ShiploadOptions {
|
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
export class Shipload {
|
|
36
|
-
|
|
37
|
-
public server: Contract
|
|
38
|
-
public platform: Contract
|
|
39
|
-
public game: PlatformContract.Types.game_row | undefined
|
|
28
|
+
private readonly _context: GameContext
|
|
40
29
|
|
|
41
30
|
constructor(chain: ChainDefinition, constructorOptions?: ShiploadConstructorOptions) {
|
|
42
31
|
const {client, platformContract, serverContract} = constructorOptions || {}
|
|
43
|
-
|
|
32
|
+
const apiClient = client || new APIClient({url: chain.url})
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
const platform = platformContract
|
|
46
35
|
? platformContract
|
|
47
|
-
: new PlatformContract.Contract({client:
|
|
36
|
+
: new PlatformContract.Contract({client: apiClient})
|
|
48
37
|
|
|
49
|
-
|
|
38
|
+
const server = serverContract
|
|
50
39
|
? serverContract
|
|
51
|
-
: new ServerContract.Contract({client:
|
|
40
|
+
: new ServerContract.Contract({client: apiClient})
|
|
41
|
+
|
|
42
|
+
this._context = new GameContext(apiClient, server, platform)
|
|
43
|
+
|
|
44
|
+
if (constructorOptions?.subscriptionsUrl) {
|
|
45
|
+
this._context.setSubscriptionsUrl(constructorOptions.subscriptionsUrl)
|
|
46
|
+
}
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
static async load(
|
|
@@ -80,155 +75,47 @@ export class Shipload {
|
|
|
80
75
|
})
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return this.game
|
|
86
|
-
}
|
|
87
|
-
const game = await this.platform.table('games').get()
|
|
88
|
-
if (!game) {
|
|
89
|
-
throw new Error(ERROR_SYSTEM_NOT_INITIALIZED)
|
|
90
|
-
}
|
|
91
|
-
this.game = game
|
|
92
|
-
return game
|
|
78
|
+
get client(): APIClient {
|
|
79
|
+
return this._context.client
|
|
93
80
|
}
|
|
94
81
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!state) {
|
|
98
|
-
throw new Error(ERROR_SYSTEM_NOT_INITIALIZED)
|
|
99
|
-
}
|
|
100
|
-
return state
|
|
82
|
+
get server(): Contract {
|
|
83
|
+
return this._context.server
|
|
101
84
|
}
|
|
102
85
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!ship) {
|
|
106
|
-
throw new Error('No ship found')
|
|
107
|
-
}
|
|
108
|
-
return new Ship(ship)
|
|
86
|
+
get platform(): Contract {
|
|
87
|
+
return this._context.platform
|
|
109
88
|
}
|
|
110
89
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (player instanceof ServerContract.Types.player_row) {
|
|
114
|
-
account = player.owner
|
|
115
|
-
} else {
|
|
116
|
-
account = Name.from(player)
|
|
117
|
-
}
|
|
118
|
-
const from = Serializer.decode({
|
|
119
|
-
data:
|
|
120
|
-
Serializer.encode({object: UInt64.from(UInt64.min)}).hexString +
|
|
121
|
-
Serializer.encode({object: Name.from(account)}).hexString,
|
|
122
|
-
type: 'uint128',
|
|
123
|
-
})
|
|
124
|
-
const to = Serializer.decode({
|
|
125
|
-
data:
|
|
126
|
-
Serializer.encode({object: UInt64.from(UInt64.max)}).hexString +
|
|
127
|
-
Serializer.encode({object: Name.from(account)}).hexString,
|
|
128
|
-
type: 'uint128',
|
|
129
|
-
})
|
|
130
|
-
const ships = await this.server
|
|
131
|
-
.table('ship')
|
|
132
|
-
.query({
|
|
133
|
-
key_type: 'i128',
|
|
134
|
-
index_position: 'secondary',
|
|
135
|
-
from,
|
|
136
|
-
to,
|
|
137
|
-
})
|
|
138
|
-
.all()
|
|
139
|
-
return ships.map((ship) => new Ship(ship))
|
|
90
|
+
get entities(): EntitiesManager {
|
|
91
|
+
return this._context.entities
|
|
140
92
|
}
|
|
141
93
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
good_id: number
|
|
145
|
-
): Promise<GoodPrice> {
|
|
146
|
-
const game = await this.getGame()
|
|
147
|
-
const state = await this.getState()
|
|
148
|
-
return marketprice(location, good_id, game.config.seed, state)
|
|
94
|
+
get players(): PlayersManager {
|
|
95
|
+
return this._context.players
|
|
149
96
|
}
|
|
150
97
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
): Promise<GoodPrice[]> {
|
|
154
|
-
const game = await this.getGame()
|
|
155
|
-
const state = await this.getState()
|
|
156
|
-
return marketprices(location, game.config.seed, state)
|
|
98
|
+
get locations(): LocationsManager {
|
|
99
|
+
return this._context.locations
|
|
157
100
|
}
|
|
158
101
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return hasSystem(game.config.seed, location)
|
|
102
|
+
get epochs(): EpochsManager {
|
|
103
|
+
return this._context.epochs
|
|
162
104
|
}
|
|
163
105
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
maxDistance: UInt16Type = 20
|
|
167
|
-
): Promise<Distance[]> {
|
|
168
|
-
const game = await this.getGame()
|
|
169
|
-
return findNearbyPlanets(game.config.seed, origin, maxDistance)
|
|
106
|
+
get actions(): ActionsManager {
|
|
107
|
+
return this._context.actions
|
|
170
108
|
}
|
|
171
109
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
origin: ServerContract.ActionParams.Type.coordinates,
|
|
175
|
-
destination: ServerContract.ActionParams.Type.coordinates,
|
|
176
|
-
recharge = false
|
|
177
|
-
): Promise<ServerContract.Types.travel_plan> {
|
|
178
|
-
const game = await this.getGame()
|
|
179
|
-
const cargos = await this.server.table('cargo').all({
|
|
180
|
-
from: ship.id,
|
|
181
|
-
to: ship.id,
|
|
182
|
-
index_position: 'secondary',
|
|
183
|
-
})
|
|
184
|
-
return travelplan(game, ship, cargos, origin, destination, recharge)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async getCargo(
|
|
188
|
-
ship: UInt64Type | ServerContract.Types.ship_row
|
|
189
|
-
): Promise<ServerContract.Types.cargo_row[]> {
|
|
190
|
-
let shipId: UInt64
|
|
191
|
-
if (ship instanceof ServerContract.Types.ship_row) {
|
|
192
|
-
shipId = UInt64.from(ship.id)
|
|
193
|
-
} else {
|
|
194
|
-
shipId = UInt64.from(ship)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const cargoItems = await this.server
|
|
198
|
-
.table('cargo')
|
|
199
|
-
.query({
|
|
200
|
-
key_type: 'i64',
|
|
201
|
-
index_position: 'secondary',
|
|
202
|
-
from: shipId,
|
|
203
|
-
to: shipId,
|
|
204
|
-
})
|
|
205
|
-
.all()
|
|
206
|
-
|
|
207
|
-
return cargoItems
|
|
110
|
+
get subscriptions(): SubscriptionsManager {
|
|
111
|
+
return this._context.subscriptions
|
|
208
112
|
}
|
|
209
113
|
|
|
210
|
-
async
|
|
211
|
-
|
|
212
|
-
return getCurrentEpoch(game)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async getCurrentEpoch(): Promise<EpochInfo> {
|
|
216
|
-
const game = await this.getGame()
|
|
217
|
-
const epoch = await this.getCurrentEpochHeight()
|
|
218
|
-
return getEpochInfo(game, epoch)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async getEpoch(height: UInt64Type): Promise<EpochInfo> {
|
|
222
|
-
const game = await this.getGame()
|
|
223
|
-
return getEpochInfo(game, UInt64.from(height))
|
|
114
|
+
async getGame(reload = false): Promise<PlatformContract.Types.game_row> {
|
|
115
|
+
return this._context.getGame(reload)
|
|
224
116
|
}
|
|
225
117
|
|
|
226
|
-
async
|
|
227
|
-
|
|
228
|
-
return this.server.table('location').all({
|
|
229
|
-
index_position: 'secondary',
|
|
230
|
-
from: hash,
|
|
231
|
-
to: hash,
|
|
232
|
-
})
|
|
118
|
+
async getState(reload = false): Promise<GameState> {
|
|
119
|
+
return this._context.getState(reload)
|
|
233
120
|
}
|
|
234
121
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type {ClientMessage, ServerMessage} from './types'
|
|
2
|
+
import {debug} from './debug'
|
|
3
|
+
|
|
4
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
|
|
5
|
+
|
|
6
|
+
export interface WebSocketConnectionOptions {
|
|
7
|
+
url: string
|
|
8
|
+
onMessage: (message: ServerMessage) => void
|
|
9
|
+
onStateChange?: (state: ConnectionState) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WebSocketConnection {
|
|
13
|
+
private ws: WebSocket | null = null
|
|
14
|
+
private url: string
|
|
15
|
+
private onMessage: (message: ServerMessage) => void
|
|
16
|
+
private onStateChange?: (state: ConnectionState) => void
|
|
17
|
+
private reconnectAttempts = 0
|
|
18
|
+
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null
|
|
19
|
+
private _state: ConnectionState = 'disconnected'
|
|
20
|
+
private shouldReconnect = true
|
|
21
|
+
private sendQueue: string[] = []
|
|
22
|
+
|
|
23
|
+
private static readonly MIN_RECONNECT_DELAY = 1000
|
|
24
|
+
private static readonly MAX_RECONNECT_DELAY = 30000
|
|
25
|
+
private static readonly RECONNECT_MULTIPLIER = 2
|
|
26
|
+
|
|
27
|
+
constructor(options: WebSocketConnectionOptions) {
|
|
28
|
+
this.url = options.url
|
|
29
|
+
this.onMessage = options.onMessage
|
|
30
|
+
this.onStateChange = options.onStateChange
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get state(): ConnectionState {
|
|
34
|
+
return this._state
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private setState(state: ConnectionState) {
|
|
38
|
+
if (this._state !== state) {
|
|
39
|
+
this._state = state
|
|
40
|
+
this.onStateChange?.(state)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
connect() {
|
|
45
|
+
if (this.ws) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.shouldReconnect = true
|
|
50
|
+
this.setState('connecting')
|
|
51
|
+
debug('Connecting to', this.url)
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
this.ws = new WebSocket(this.url)
|
|
55
|
+
|
|
56
|
+
this.ws.onopen = () => {
|
|
57
|
+
debug('Connected')
|
|
58
|
+
this.reconnectAttempts = 0
|
|
59
|
+
this.setState('connected')
|
|
60
|
+
while (
|
|
61
|
+
this.sendQueue.length > 0 &&
|
|
62
|
+
this.ws &&
|
|
63
|
+
this.ws.readyState === WebSocket.OPEN
|
|
64
|
+
) {
|
|
65
|
+
this.ws.send(this.sendQueue.shift()!)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.ws.onmessage = (event) => {
|
|
70
|
+
try {
|
|
71
|
+
const message = JSON.parse(event.data) as ServerMessage
|
|
72
|
+
this.onMessage(message)
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error('[WS] Failed to parse message:', e)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.ws.onclose = () => {
|
|
80
|
+
this.ws = null
|
|
81
|
+
this.sendQueue.length = 0
|
|
82
|
+
|
|
83
|
+
if (this.shouldReconnect) {
|
|
84
|
+
this.setState('reconnecting')
|
|
85
|
+
this.scheduleReconnect()
|
|
86
|
+
} else {
|
|
87
|
+
this.setState('disconnected')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.error('[WS] Failed to create connection:', e)
|
|
93
|
+
this.ws = null
|
|
94
|
+
if (this.shouldReconnect) {
|
|
95
|
+
this.setState('reconnecting')
|
|
96
|
+
this.scheduleReconnect()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private scheduleReconnect() {
|
|
102
|
+
if (this.reconnectTimeout) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const delay = Math.min(
|
|
107
|
+
WebSocketConnection.MIN_RECONNECT_DELAY *
|
|
108
|
+
WebSocketConnection.RECONNECT_MULTIPLIER ** this.reconnectAttempts,
|
|
109
|
+
WebSocketConnection.MAX_RECONNECT_DELAY
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`)
|
|
113
|
+
|
|
114
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
115
|
+
this.reconnectTimeout = null
|
|
116
|
+
this.reconnectAttempts++
|
|
117
|
+
this.connect()
|
|
118
|
+
}, delay)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
disconnect() {
|
|
122
|
+
this.shouldReconnect = false
|
|
123
|
+
|
|
124
|
+
if (this.reconnectTimeout) {
|
|
125
|
+
clearTimeout(this.reconnectTimeout)
|
|
126
|
+
this.reconnectTimeout = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.ws) {
|
|
130
|
+
this.ws.close()
|
|
131
|
+
this.ws = null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.sendQueue.length = 0
|
|
135
|
+
this.setState('disconnected')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
close() {
|
|
139
|
+
this.disconnect()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
send(message: ClientMessage) {
|
|
143
|
+
const data = JSON.stringify(message)
|
|
144
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
145
|
+
this.ws.send(data)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
this.sendQueue.push(data)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get isConnected(): boolean {
|
|
152
|
+
return this._state === 'connected'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
let enabled = false
|
|
4
|
+
|
|
5
|
+
export function setSubscriptionsDebug(on: boolean): void {
|
|
6
|
+
enabled = on
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isSubscriptionsDebugEnabled(): boolean {
|
|
10
|
+
return enabled
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function debug(...args: unknown[]): void {
|
|
14
|
+
if (enabled) {
|
|
15
|
+
console.log('[WS]', ...args)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {WebSocketConnection} from './connection'
|
|
2
|
+
import type {
|
|
3
|
+
BoundingBox,
|
|
4
|
+
BoundsDeltaMessage,
|
|
5
|
+
ClientMessage,
|
|
6
|
+
ServerMessage,
|
|
7
|
+
SnapshotMessage,
|
|
8
|
+
SubscribeEntityMessage,
|
|
9
|
+
SubscribeMessage,
|
|
10
|
+
UnsubscribeEntityMessage,
|
|
11
|
+
UpdateBoundsMessage,
|
|
12
|
+
UpdateMessage,
|
|
13
|
+
WireEntity,
|
|
14
|
+
} from './types'
|
|
15
|
+
import {mapEntity, parseWireEntity} from './mappers'
|
|
16
|
+
import type {Ship} from '../entities/ship'
|
|
17
|
+
import type {Warehouse} from '../entities/warehouse'
|
|
18
|
+
import type {Container} from '../entities/container'
|
|
19
|
+
|
|
20
|
+
export type SubscriptionEntityType = 'ship' | 'warehouse' | 'container'
|
|
21
|
+
export type EntityInstance = Ship | Warehouse | Container
|
|
22
|
+
|
|
23
|
+
export interface SubscriptionsOptions {
|
|
24
|
+
url: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BoundsSubscriptionHandle {
|
|
28
|
+
readonly subId: string
|
|
29
|
+
unsubscribe(): void
|
|
30
|
+
updateBounds(bounds: BoundingBox): void
|
|
31
|
+
current: Map<number, EntityInstance>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EntitySubscriptionHandle {
|
|
35
|
+
readonly subId: string
|
|
36
|
+
readonly entityType: SubscriptionEntityType
|
|
37
|
+
readonly entityId: string
|
|
38
|
+
unsubscribe(): void
|
|
39
|
+
current: EntityInstance | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class SubscriptionsManager {
|
|
43
|
+
private readonly conn: WebSocketConnection
|
|
44
|
+
private readonly entitySubs = new Map<
|
|
45
|
+
string,
|
|
46
|
+
{
|
|
47
|
+
type: SubscriptionEntityType
|
|
48
|
+
id: string
|
|
49
|
+
onUpdate: (e: EntityInstance) => void
|
|
50
|
+
handle: EntitySubscriptionHandle
|
|
51
|
+
}
|
|
52
|
+
>()
|
|
53
|
+
private readonly boundsSubs = new Map<
|
|
54
|
+
string,
|
|
55
|
+
{
|
|
56
|
+
onSnapshot?: (entities: EntityInstance[]) => void
|
|
57
|
+
onUpdate?: (entity: EntityInstance) => void
|
|
58
|
+
onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
|
|
59
|
+
handle: BoundsSubscriptionHandle
|
|
60
|
+
}
|
|
61
|
+
>()
|
|
62
|
+
private subCounter = 0
|
|
63
|
+
|
|
64
|
+
constructor(opts: SubscriptionsOptions) {
|
|
65
|
+
this.conn = new WebSocketConnection({
|
|
66
|
+
url: opts.url,
|
|
67
|
+
onMessage: (m) => this.onMessage(m),
|
|
68
|
+
})
|
|
69
|
+
this.conn.connect()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close() {
|
|
73
|
+
this.conn.close()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private generateSubID(prefix: string): string {
|
|
77
|
+
this.subCounter += 1
|
|
78
|
+
return `${prefix}-${this.subCounter}-${Math.random().toString(36).slice(2, 8)}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private sendMessage(msg: ClientMessage) {
|
|
82
|
+
this.conn.send(msg)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
subscribeEntity(
|
|
86
|
+
type: SubscriptionEntityType,
|
|
87
|
+
id: string,
|
|
88
|
+
onUpdate: (e: EntityInstance) => void
|
|
89
|
+
): EntitySubscriptionHandle {
|
|
90
|
+
const subId = this.generateSubID('ent')
|
|
91
|
+
const msg: SubscribeEntityMessage = {
|
|
92
|
+
type: 'subscribe_entity',
|
|
93
|
+
sub_id: subId,
|
|
94
|
+
entity_type: type,
|
|
95
|
+
entity_id: id,
|
|
96
|
+
}
|
|
97
|
+
const handle: EntitySubscriptionHandle = {
|
|
98
|
+
subId,
|
|
99
|
+
entityType: type,
|
|
100
|
+
entityId: id,
|
|
101
|
+
unsubscribe: () => this.unsubscribeEntity(subId),
|
|
102
|
+
current: null,
|
|
103
|
+
}
|
|
104
|
+
this.entitySubs.set(subId, {type, id, onUpdate, handle})
|
|
105
|
+
this.sendMessage(msg)
|
|
106
|
+
return handle
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private unsubscribeEntity(subId: string) {
|
|
110
|
+
const entry = this.entitySubs.get(subId)
|
|
111
|
+
if (!entry) return
|
|
112
|
+
this.entitySubs.delete(subId)
|
|
113
|
+
const msg: UnsubscribeEntityMessage = {type: 'unsubscribe_entity', sub_id: subId}
|
|
114
|
+
this.sendMessage(msg)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
subscribeBounds(
|
|
118
|
+
bounds: BoundingBox,
|
|
119
|
+
handlers: {
|
|
120
|
+
onSnapshot?: (entities: EntityInstance[]) => void
|
|
121
|
+
onUpdate?: (entity: EntityInstance) => void
|
|
122
|
+
onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
|
|
123
|
+
owner?: string
|
|
124
|
+
prioritizeOwner?: string
|
|
125
|
+
}
|
|
126
|
+
): BoundsSubscriptionHandle {
|
|
127
|
+
const subId = this.generateSubID('bnd')
|
|
128
|
+
const msg: SubscribeMessage = {
|
|
129
|
+
type: 'subscribe',
|
|
130
|
+
sub_id: subId,
|
|
131
|
+
bounds,
|
|
132
|
+
owner: handlers.owner,
|
|
133
|
+
prioritize_owner: handlers.prioritizeOwner,
|
|
134
|
+
}
|
|
135
|
+
const handle: BoundsSubscriptionHandle = {
|
|
136
|
+
subId,
|
|
137
|
+
unsubscribe: () => this.unsubscribeBounds(subId),
|
|
138
|
+
updateBounds: (b) => this.updateBounds(subId, b),
|
|
139
|
+
current: new Map(),
|
|
140
|
+
}
|
|
141
|
+
this.boundsSubs.set(subId, {
|
|
142
|
+
onSnapshot: handlers.onSnapshot,
|
|
143
|
+
onUpdate: handlers.onUpdate,
|
|
144
|
+
onBoundsDelta: handlers.onBoundsDelta,
|
|
145
|
+
handle,
|
|
146
|
+
})
|
|
147
|
+
this.sendMessage(msg)
|
|
148
|
+
return handle
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private unsubscribeBounds(subId: string) {
|
|
152
|
+
this.boundsSubs.delete(subId)
|
|
153
|
+
this.sendMessage({type: 'unsubscribe', sub_id: subId})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private updateBounds(subId: string, bounds: BoundingBox) {
|
|
157
|
+
const msg: UpdateBoundsMessage = {type: 'update_bounds', sub_id: subId, bounds}
|
|
158
|
+
this.sendMessage(msg)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private onMessage(msg: ServerMessage) {
|
|
162
|
+
switch (msg.type) {
|
|
163
|
+
case 'snapshot':
|
|
164
|
+
this.handleSnapshot(msg)
|
|
165
|
+
break
|
|
166
|
+
case 'update':
|
|
167
|
+
this.handleUpdate(msg)
|
|
168
|
+
break
|
|
169
|
+
case 'bounds_delta':
|
|
170
|
+
this.handleBoundsDelta(msg)
|
|
171
|
+
break
|
|
172
|
+
case 'error':
|
|
173
|
+
this.handleError(msg)
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private parseEntity(raw: WireEntity): EntityInstance {
|
|
179
|
+
const ei = parseWireEntity(raw)
|
|
180
|
+
return mapEntity(ei)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private handleSnapshot(msg: SnapshotMessage) {
|
|
184
|
+
const entSub = this.entitySubs.get(msg.sub_id)
|
|
185
|
+
if (entSub) {
|
|
186
|
+
if (msg.entities.length > 0) {
|
|
187
|
+
const ent = this.parseEntity(msg.entities[0])
|
|
188
|
+
entSub.handle.current = ent
|
|
189
|
+
entSub.onUpdate(ent)
|
|
190
|
+
}
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
const boundsSub = this.boundsSubs.get(msg.sub_id)
|
|
194
|
+
if (boundsSub) {
|
|
195
|
+
const ents = msg.entities.map((e) => this.parseEntity(e))
|
|
196
|
+
boundsSub.handle.current.clear()
|
|
197
|
+
for (const e of ents) boundsSub.handle.current.set(Number(e.id), e)
|
|
198
|
+
boundsSub.onSnapshot?.(ents)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private handleUpdate(msg: UpdateMessage) {
|
|
203
|
+
const ent = this.parseEntity(msg.entity)
|
|
204
|
+
for (const subId of msg.sub_ids) {
|
|
205
|
+
const entSub = this.entitySubs.get(subId)
|
|
206
|
+
if (entSub) {
|
|
207
|
+
entSub.handle.current = ent
|
|
208
|
+
entSub.onUpdate(ent)
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
const boundsSub = this.boundsSubs.get(subId)
|
|
212
|
+
if (boundsSub) {
|
|
213
|
+
boundsSub.handle.current.set(msg.entity_id, ent)
|
|
214
|
+
boundsSub.onUpdate?.(ent)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private handleBoundsDelta(msg: BoundsDeltaMessage) {
|
|
220
|
+
const sub = this.boundsSubs.get(msg.sub_id)
|
|
221
|
+
if (!sub) return
|
|
222
|
+
const entered = msg.entered.map((e) => this.parseEntity(e))
|
|
223
|
+
for (const e of entered) sub.handle.current.set(Number(e.id), e)
|
|
224
|
+
for (const id of msg.exited) sub.handle.current.delete(id)
|
|
225
|
+
sub.onBoundsDelta?.(entered, msg.exited)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private handleError(msg: {sub_id?: string; error: string}) {
|
|
229
|
+
if (!msg.sub_id) return
|
|
230
|
+
const entSub = this.entitySubs.get(msg.sub_id)
|
|
231
|
+
if (entSub) {
|
|
232
|
+
this.entitySubs.delete(msg.sub_id)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
const boundsSub = this.boundsSubs.get(msg.sub_id)
|
|
236
|
+
if (boundsSub) {
|
|
237
|
+
this.boundsSubs.delete(msg.sub_id)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|