@shipload/sdk 2.0.0-rc19 → 2.0.0-rc20
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 +227 -48
- package/lib/shipload.js +367 -62
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +362 -60
- package/lib/shipload.m.js.map +1 -1
- package/package.json +3 -2
- package/src/contracts/server.ts +3 -10
- package/src/entities/makers.ts +7 -6
- package/src/entities/ship.ts +0 -9
- package/src/entities/warehouse.ts +0 -9
- package/src/index-module.ts +2 -0
- package/src/managers/context.ts +19 -0
- package/src/resolution/resolve-item.ts +27 -20
- package/src/shipload.ts +10 -0
- 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/types.ts +0 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {ServerContract} from '../contracts'
|
|
2
|
+
import {Ship} from '../entities/ship'
|
|
3
|
+
import {Warehouse} from '../entities/warehouse'
|
|
4
|
+
import {Container} from '../entities/container'
|
|
5
|
+
import type {WireEntity} from './types'
|
|
6
|
+
|
|
7
|
+
export function mapEntity(ei: ServerContract.Types.entity_info): Ship | Warehouse | Container {
|
|
8
|
+
if (ei.type.equals('ship')) return new Ship(ei)
|
|
9
|
+
if (ei.type.equals('warehouse')) return new Warehouse(ei)
|
|
10
|
+
if (ei.type.equals('container')) return new Container(ei)
|
|
11
|
+
throw new Error(`mapEntity: unknown entity type ${ei.type.toString()}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseWireEntity(raw: WireEntity): ServerContract.Types.entity_info {
|
|
15
|
+
const shaped: Record<string, unknown> = {...raw}
|
|
16
|
+
|
|
17
|
+
if (typeof shaped.type === 'number' && typeof shaped.type_name === 'string') {
|
|
18
|
+
shaped.type = shaped.type_name
|
|
19
|
+
}
|
|
20
|
+
delete shaped.type_name
|
|
21
|
+
|
|
22
|
+
if (shaped.entity_name === undefined && typeof shaped.name === 'string') {
|
|
23
|
+
shaped.entity_name = shaped.name
|
|
24
|
+
}
|
|
25
|
+
delete shaped.name
|
|
26
|
+
|
|
27
|
+
return ServerContract.Types.entity_info.from(shaped)
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type {ServerContract} from '../contracts'
|
|
2
|
+
|
|
3
|
+
export type EntityInfo = ServerContract.Types.entity_info
|
|
4
|
+
|
|
5
|
+
export interface BoundingBox {
|
|
6
|
+
min_x: number
|
|
7
|
+
min_y: number
|
|
8
|
+
max_x: number
|
|
9
|
+
max_y: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WireCoordinates {
|
|
13
|
+
x: number
|
|
14
|
+
y: number
|
|
15
|
+
z?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Client → Server ---
|
|
19
|
+
|
|
20
|
+
export type SubscribeMessage = {
|
|
21
|
+
type: 'subscribe'
|
|
22
|
+
sub_id: string
|
|
23
|
+
bounds?: BoundingBox
|
|
24
|
+
owner?: string
|
|
25
|
+
prioritize_owner?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type UpdateBoundsMessage = {
|
|
29
|
+
type: 'update_bounds'
|
|
30
|
+
sub_id: string
|
|
31
|
+
bounds: BoundingBox
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type UnsubscribeMessage = {
|
|
35
|
+
type: 'unsubscribe'
|
|
36
|
+
sub_id: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SubscribeEntityMessage = {
|
|
40
|
+
type: 'subscribe_entity'
|
|
41
|
+
sub_id: string
|
|
42
|
+
entity_type: 'ship' | 'warehouse' | 'container'
|
|
43
|
+
entity_id: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type UnsubscribeEntityMessage = {
|
|
47
|
+
type: 'unsubscribe_entity'
|
|
48
|
+
sub_id: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type SubscribeEventsMessage = {
|
|
52
|
+
type: 'subscribe_events'
|
|
53
|
+
sub_id: string
|
|
54
|
+
event_filter?: Record<string, unknown>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type UnsubscribeEventsMessage = {
|
|
58
|
+
type: 'unsubscribe_events'
|
|
59
|
+
sub_id: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type PingMessage = {type: 'ping'}
|
|
63
|
+
|
|
64
|
+
export type ClientMessage =
|
|
65
|
+
| SubscribeMessage
|
|
66
|
+
| UpdateBoundsMessage
|
|
67
|
+
| UnsubscribeMessage
|
|
68
|
+
| SubscribeEntityMessage
|
|
69
|
+
| UnsubscribeEntityMessage
|
|
70
|
+
| SubscribeEventsMessage
|
|
71
|
+
| UnsubscribeEventsMessage
|
|
72
|
+
| PingMessage
|
|
73
|
+
|
|
74
|
+
// --- Server → Client ---
|
|
75
|
+
|
|
76
|
+
export type AckMessage = {
|
|
77
|
+
type: 'subscribed' | 'unsubscribed' | 'bounds_updated'
|
|
78
|
+
sub_id: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type WireEntity = Record<string, unknown> & {
|
|
82
|
+
type: number
|
|
83
|
+
type_name: 'ship' | 'warehouse' | 'container'
|
|
84
|
+
id: string | number
|
|
85
|
+
owner: string
|
|
86
|
+
coordinates: WireCoordinates
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type SnapshotMessage = {
|
|
90
|
+
type: 'snapshot'
|
|
91
|
+
sub_id: string
|
|
92
|
+
seq: number
|
|
93
|
+
entities: WireEntity[]
|
|
94
|
+
truncated?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type UpdateMessage = {
|
|
98
|
+
type: 'update'
|
|
99
|
+
sub_ids: string[]
|
|
100
|
+
entity_id: number
|
|
101
|
+
entity: WireEntity
|
|
102
|
+
seq: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type BoundsDeltaMessage = {
|
|
106
|
+
type: 'bounds_delta'
|
|
107
|
+
sub_id: string
|
|
108
|
+
entered: WireEntity[]
|
|
109
|
+
exited: number[]
|
|
110
|
+
seq: number
|
|
111
|
+
truncated?: boolean
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type EventMessage = {
|
|
115
|
+
type: 'event'
|
|
116
|
+
sub_id: string
|
|
117
|
+
catchup: boolean
|
|
118
|
+
events: Array<Record<string, unknown>>
|
|
119
|
+
seq?: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type EventCatchupCompleteMessage = {
|
|
123
|
+
type: 'event_catchup_complete'
|
|
124
|
+
sub_id: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type PongMessage = {type: 'pong'}
|
|
128
|
+
|
|
129
|
+
export type ErrorMessage = {
|
|
130
|
+
type: 'error'
|
|
131
|
+
error: string
|
|
132
|
+
sub_id?: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ServerMessage =
|
|
136
|
+
| AckMessage
|
|
137
|
+
| SnapshotMessage
|
|
138
|
+
| UpdateMessage
|
|
139
|
+
| BoundsDeltaMessage
|
|
140
|
+
| EventMessage
|
|
141
|
+
| EventCatchupCompleteMessage
|
|
142
|
+
| PongMessage
|
|
143
|
+
| ErrorMessage
|
package/src/types.ts
CHANGED
|
@@ -14,11 +14,8 @@ export const PRECISION = 10000
|
|
|
14
14
|
export const CRAFT_ENERGY_DIVISOR = 150000
|
|
15
15
|
|
|
16
16
|
export const WAREHOUSE_Z = 500
|
|
17
|
-
export const INITIAL_WAREHOUSE_CAPACITY = 10000000
|
|
18
17
|
|
|
19
18
|
export const CONTAINER_Z = 300
|
|
20
|
-
export const INITIAL_CONTAINER_HULLMASS = 50000
|
|
21
|
-
export const INITIAL_CONTAINER_CAPACITY = 2000000
|
|
22
19
|
|
|
23
20
|
export const TRAVEL_MAX_DURATION = 86400
|
|
24
21
|
|