@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.
@@ -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