@shipload/sdk 1.0.0-next.32 → 1.0.0-next.34

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.
@@ -3,6 +3,7 @@ import type {
3
3
  BoundingBox,
4
4
  BoundsDeltaMessage,
5
5
  ClientMessage,
6
+ EntityDeletedMessage,
6
7
  ServerMessage,
7
8
  SnapshotMessage,
8
9
  SubscribeEntityMessage,
@@ -25,50 +26,73 @@ export interface SubscriptionsOptions {
25
26
  pongTimeoutMs?: number
26
27
  }
27
28
 
28
- export interface BoundsSubscriptionHandle {
29
- readonly subId: string
30
- unsubscribe(): void
31
- updateBounds(bounds: BoundingBox): void
32
- current: Map<number, EntityInstance>
29
+ export type ExactEntitySubscriptionFilter = {
30
+ id: string | number
31
+ owner?: never
32
+ bounds?: never
33
+ prioritizeOwner?: never
34
+ }
35
+
36
+ export type BroadEntitySubscriptionFilter = {
37
+ id?: undefined
38
+ owner?: string
39
+ bounds?: BoundingBox
40
+ prioritizeOwner?: string
41
+ }
42
+
43
+ export type EntitySubscriptionFilter = ExactEntitySubscriptionFilter | BroadEntitySubscriptionFilter
44
+
45
+ export interface EntitySubscriptionMeta {
46
+ seq?: number
47
+ truncated?: boolean
48
+ }
49
+
50
+ export interface EntitySubscriptionHandlers {
51
+ onSnapshot?: (entities: EntityInstance[], meta: EntitySubscriptionMeta) => void
52
+ onUpdate?: (entity: EntityInstance, meta: EntitySubscriptionMeta) => void
53
+ onBoundsDelta?: (
54
+ entered: EntityInstance[],
55
+ exited: number[],
56
+ meta: EntitySubscriptionMeta
57
+ ) => void
58
+ onDeleted?: (id: string, meta: EntitySubscriptionMeta) => void
59
+ onError?: (error: Error) => void
33
60
  }
34
61
 
35
- export interface OwnerSubscriptionHandle {
62
+ export interface EntitiesSubscriptionHandle {
36
63
  readonly subId: string
64
+ readonly filter: EntitySubscriptionFilter
37
65
  unsubscribe(): void
38
66
  current: Map<number, EntityInstance>
39
67
  }
40
68
 
41
- export interface EntitySubscriptionHandle {
42
- readonly subId: string
43
- readonly entityType: SubscriptionEntityType
44
- readonly entityId: string
45
- unsubscribe(): void
46
- current: EntityInstance | null
69
+ export type BoundsSubscriptionHandle = EntitiesSubscriptionHandle & {
70
+ readonly filter: BroadEntitySubscriptionFilter & {bounds: BoundingBox}
71
+ updateBounds(bounds: BoundingBox): void
72
+ }
73
+ export type OwnerSubscriptionHandle = EntitiesSubscriptionHandle & {
74
+ readonly filter: BroadEntitySubscriptionFilter
75
+ }
76
+ export type EntitySubscriptionHandle = EntitiesSubscriptionHandle & {
77
+ readonly filter: ExactEntitySubscriptionFilter
78
+ }
79
+
80
+ type EntitiesSubscriptionEntry = {
81
+ filter: InternalEntitySubscriptionFilter
82
+ handlers: EntitySubscriptionHandlers
83
+ handle: EntitiesSubscriptionHandle
84
+ }
85
+
86
+ type InternalEntitySubscriptionFilter = {
87
+ id?: string | number
88
+ owner?: string
89
+ bounds?: BoundingBox
90
+ prioritizeOwner?: string
47
91
  }
48
92
 
49
93
  export class SubscriptionsManager {
50
94
  private readonly conn: WebSocketConnection
51
- private readonly entitySubs = new Map<
52
- string,
53
- {
54
- type: SubscriptionEntityType
55
- id: string
56
- onUpdate: (e: EntityInstance) => void
57
- handle: EntitySubscriptionHandle
58
- }
59
- >()
60
- private readonly boundsSubs = new Map<
61
- string,
62
- {
63
- bounds?: BoundingBox
64
- owner?: string
65
- prioritizeOwner?: string
66
- onSnapshot?: (entities: EntityInstance[]) => void
67
- onUpdate?: (entity: EntityInstance) => void
68
- onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
69
- handle: BoundsSubscriptionHandle | OwnerSubscriptionHandle
70
- }
71
- >()
95
+ private readonly entitySubs = new Map<string, EntitiesSubscriptionEntry>()
72
96
  private subCounter = 0
73
97
  private hasConnected = false
74
98
 
@@ -97,113 +121,162 @@ export class SubscriptionsManager {
97
121
  this.conn.send(msg)
98
122
  }
99
123
 
100
- subscribeEntity(
101
- type: SubscriptionEntityType,
102
- id: string,
103
- onUpdate: (e: EntityInstance) => void
104
- ): EntitySubscriptionHandle {
105
- const subId = this.generateSubID('ent')
106
- const msg: SubscribeEntityMessage = {
107
- type: 'subscribe_entity',
108
- sub_id: subId,
109
- entity_type: type,
110
- entity_id: id,
111
- }
112
- const handle: EntitySubscriptionHandle = {
124
+ subscribeEntities(
125
+ filter: BroadEntitySubscriptionFilter & {bounds: BoundingBox},
126
+ handlers?: EntitySubscriptionHandlers
127
+ ): BoundsSubscriptionHandle
128
+ subscribeEntities(
129
+ filter: ExactEntitySubscriptionFilter,
130
+ handlers?: EntitySubscriptionHandlers
131
+ ): EntitySubscriptionHandle
132
+ subscribeEntities(
133
+ filter: BroadEntitySubscriptionFilter,
134
+ handlers?: EntitySubscriptionHandlers
135
+ ): EntitiesSubscriptionHandle
136
+ subscribeEntities(
137
+ filter: EntitySubscriptionFilter,
138
+ handlers?: EntitySubscriptionHandlers
139
+ ): EntitiesSubscriptionHandle
140
+ subscribeEntities(
141
+ filter: EntitySubscriptionFilter,
142
+ handlers: EntitySubscriptionHandlers = {}
143
+ ): EntitiesSubscriptionHandle {
144
+ const storedFilter = this.normalizeFilter(filter)
145
+ const subId = this.generateSubID(this.subscriptionPrefix(storedFilter))
146
+ const handle: EntitiesSubscriptionHandle = {
113
147
  subId,
114
- entityType: type,
115
- entityId: id,
116
- unsubscribe: () => this.unsubscribeEntity(subId),
117
- current: null,
148
+ get filter() {
149
+ return SubscriptionsManager.publicFilter(storedFilter)
150
+ },
151
+ unsubscribe: () => this.unsubscribeEntities(subId),
152
+ current: new Map(),
118
153
  }
119
- this.entitySubs.set(subId, {type, id, onUpdate, handle})
120
- this.sendMessage(msg)
154
+ if (storedFilter.id === undefined && storedFilter.bounds) {
155
+ ;(handle as BoundsSubscriptionHandle).updateBounds = (bounds) =>
156
+ this.updateBounds(subId, bounds)
157
+ }
158
+
159
+ this.entitySubs.set(subId, {filter: storedFilter, handlers, handle})
160
+ this.sendMessage(this.subscribeMessage(subId, storedFilter))
121
161
  return handle
122
162
  }
123
163
 
124
- private unsubscribeEntity(subId: string) {
125
- const entry = this.entitySubs.get(subId)
126
- if (!entry) return
127
- this.entitySubs.delete(subId)
128
- const msg: UnsubscribeEntityMessage = {type: 'unsubscribe_entity', sub_id: subId}
129
- this.sendMessage(msg)
164
+ subscribeEntity(
165
+ id: string | number,
166
+ handlers: EntitySubscriptionHandlers
167
+ ): EntitySubscriptionHandle {
168
+ return this.subscribeEntities({id}, handlers)
169
+ }
170
+
171
+ subscribeOwner(
172
+ owner: string,
173
+ handlers: EntitySubscriptionHandlers = {}
174
+ ): OwnerSubscriptionHandle {
175
+ return this.subscribeEntities({owner}, handlers) as OwnerSubscriptionHandle
130
176
  }
131
177
 
132
178
  subscribeBounds(
133
179
  bounds: BoundingBox,
134
- handlers: {
135
- onSnapshot?: (entities: EntityInstance[]) => void
136
- onUpdate?: (entity: EntityInstance) => void
137
- onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
180
+ handlers: EntitySubscriptionHandlers & {
138
181
  owner?: string
139
182
  prioritizeOwner?: string
140
- }
183
+ } = {}
141
184
  ): BoundsSubscriptionHandle {
142
- const subId = this.generateSubID('bnd')
143
- const msg: SubscribeMessage = {
144
- type: 'subscribe',
145
- sub_id: subId,
146
- bounds,
147
- owner: handlers.owner,
148
- prioritize_owner: handlers.prioritizeOwner,
185
+ return this.subscribeEntities(
186
+ {bounds, owner: handlers.owner, prioritizeOwner: handlers.prioritizeOwner},
187
+ handlers
188
+ )
189
+ }
190
+
191
+ subscribeAllEntities(handlers: EntitySubscriptionHandlers = {}): EntitiesSubscriptionHandle {
192
+ return this.subscribeEntities({}, handlers)
193
+ }
194
+
195
+ private normalizeFilter(filter: EntitySubscriptionFilter): InternalEntitySubscriptionFilter {
196
+ const raw = filter as InternalEntitySubscriptionFilter
197
+ if (
198
+ raw.id !== undefined &&
199
+ (raw.owner !== undefined ||
200
+ raw.bounds !== undefined ||
201
+ raw.prioritizeOwner !== undefined)
202
+ ) {
203
+ throw new Error(
204
+ 'Exact entity subscription filters cannot include owner, bounds, or prioritizeOwner'
205
+ )
149
206
  }
150
- const handle: BoundsSubscriptionHandle = {
151
- subId,
152
- unsubscribe: () => this.unsubscribeBounds(subId),
153
- updateBounds: (b) => this.updateBounds(subId, b),
154
- current: new Map(),
207
+ if (raw.id !== undefined) {
208
+ return {id: raw.id}
209
+ }
210
+ return {
211
+ owner: raw.owner,
212
+ bounds: raw.bounds ? this.cloneBounds(raw.bounds) : undefined,
213
+ prioritizeOwner: raw.prioritizeOwner,
155
214
  }
156
- this.boundsSubs.set(subId, {
157
- bounds,
158
- owner: handlers.owner,
159
- prioritizeOwner: handlers.prioritizeOwner,
160
- onSnapshot: handlers.onSnapshot,
161
- onUpdate: handlers.onUpdate,
162
- onBoundsDelta: handlers.onBoundsDelta,
163
- handle,
164
- })
165
- this.sendMessage(msg)
166
- return handle
167
215
  }
168
216
 
169
- subscribeOwner(
170
- owner: string,
171
- handlers: {
172
- onSnapshot?: (entities: EntityInstance[]) => void
173
- onUpdate?: (entity: EntityInstance) => void
174
- } = {}
175
- ): OwnerSubscriptionHandle {
176
- const subId = this.generateSubID('own')
177
- const msg: SubscribeMessage = {
217
+ private static publicFilter(
218
+ filter: InternalEntitySubscriptionFilter
219
+ ): EntitySubscriptionFilter {
220
+ if (filter.id !== undefined) {
221
+ return Object.freeze({id: filter.id}) as ExactEntitySubscriptionFilter
222
+ }
223
+
224
+ return Object.freeze({
225
+ owner: filter.owner,
226
+ bounds: filter.bounds ? (Object.freeze({...filter.bounds}) as BoundingBox) : undefined,
227
+ prioritizeOwner: filter.prioritizeOwner,
228
+ }) as BroadEntitySubscriptionFilter
229
+ }
230
+
231
+ private cloneBounds(bounds: BoundingBox): BoundingBox {
232
+ return {...bounds}
233
+ }
234
+
235
+ private subscriptionPrefix(filter: InternalEntitySubscriptionFilter): string {
236
+ if (filter.id !== undefined) return 'ent'
237
+ if (filter.bounds) return 'bnd'
238
+ if (filter.owner) return 'own'
239
+ return 'all'
240
+ }
241
+
242
+ private subscribeMessage(
243
+ subId: string,
244
+ filter: InternalEntitySubscriptionFilter
245
+ ): SubscribeEntityMessage | SubscribeMessage {
246
+ if (filter.id !== undefined) {
247
+ return {
248
+ type: 'subscribe_entity',
249
+ sub_id: subId,
250
+ entity_id: String(filter.id),
251
+ }
252
+ }
253
+
254
+ return {
178
255
  type: 'subscribe',
179
256
  sub_id: subId,
180
- owner,
257
+ owner: filter.owner,
258
+ bounds: filter.bounds,
259
+ prioritize_owner: filter.prioritizeOwner,
181
260
  }
182
- const handle: OwnerSubscriptionHandle = {
183
- subId,
184
- unsubscribe: () => this.unsubscribeBounds(subId),
185
- current: new Map(),
186
- }
187
- this.boundsSubs.set(subId, {
188
- bounds: undefined,
189
- owner,
190
- prioritizeOwner: undefined,
191
- onSnapshot: handlers.onSnapshot,
192
- onUpdate: handlers.onUpdate,
193
- handle,
194
- })
195
- this.sendMessage(msg)
196
- return handle
197
261
  }
198
262
 
199
- private unsubscribeBounds(subId: string) {
200
- this.boundsSubs.delete(subId)
263
+ private unsubscribeEntities(subId: string) {
264
+ const entry = this.entitySubs.get(subId)
265
+ if (!entry) return
266
+ this.entitySubs.delete(subId)
267
+ if (entry.filter.id !== undefined) {
268
+ const msg: UnsubscribeEntityMessage = {type: 'unsubscribe_entity', sub_id: subId}
269
+ this.sendMessage(msg)
270
+ return
271
+ }
201
272
  this.sendMessage({type: 'unsubscribe', sub_id: subId})
202
273
  }
203
274
 
204
275
  private updateBounds(subId: string, bounds: BoundingBox) {
205
- const entry = this.boundsSubs.get(subId)
206
- if (entry) entry.bounds = bounds
276
+ const entry = this.entitySubs.get(subId)
277
+ if (!entry) return
278
+ if (entry.filter.id !== undefined || !entry.filter.bounds) return
279
+ entry.filter.bounds = this.cloneBounds(bounds)
207
280
  const msg: UpdateBoundsMessage = {type: 'update_bounds', sub_id: subId, bounds}
208
281
  this.sendMessage(msg)
209
282
  }
@@ -215,23 +288,7 @@ export class SubscriptionsManager {
215
288
  return
216
289
  }
217
290
  for (const [subId, entry] of this.entitySubs) {
218
- const msg: SubscribeEntityMessage = {
219
- type: 'subscribe_entity',
220
- sub_id: subId,
221
- entity_type: entry.type,
222
- entity_id: entry.id,
223
- }
224
- this.sendMessage(msg)
225
- }
226
- for (const [subId, entry] of this.boundsSubs) {
227
- const msg: SubscribeMessage = {
228
- type: 'subscribe',
229
- sub_id: subId,
230
- bounds: entry.bounds,
231
- owner: entry.owner,
232
- prioritize_owner: entry.prioritizeOwner,
233
- }
234
- this.sendMessage(msg)
291
+ this.sendMessage(this.subscribeMessage(subId, entry.filter))
235
292
  }
236
293
  }
237
294
 
@@ -246,6 +303,9 @@ export class SubscriptionsManager {
246
303
  case 'bounds_delta':
247
304
  this.handleBoundsDelta(msg)
248
305
  break
306
+ case 'entity_deleted':
307
+ this.handleEntityDeleted(msg)
308
+ break
249
309
  case 'error':
250
310
  this.handleError(msg)
251
311
  break
@@ -258,60 +318,53 @@ export class SubscriptionsManager {
258
318
  }
259
319
 
260
320
  private handleSnapshot(msg: SnapshotMessage) {
261
- const entSub = this.entitySubs.get(msg.sub_id)
262
- if (entSub) {
263
- if (msg.entities.length > 0) {
264
- const ent = this.parseEntity(msg.entities[0])
265
- entSub.handle.current = ent
266
- entSub.onUpdate(ent)
267
- }
268
- return
269
- }
270
- const boundsSub = this.boundsSubs.get(msg.sub_id)
271
- if (boundsSub) {
272
- const ents = msg.entities.map((e) => this.parseEntity(e))
273
- boundsSub.handle.current.clear()
274
- for (const e of ents) boundsSub.handle.current.set(Number(e.id), e)
275
- boundsSub.onSnapshot?.(ents)
321
+ const sub = this.entitySubs.get(msg.sub_id)
322
+ if (!sub) return
323
+ const meta = {seq: msg.seq, truncated: msg.truncated === true}
324
+ const ents = msg.entities.map((e) => this.parseEntity(e))
325
+ sub.handle.current.clear()
326
+ for (const e of ents) sub.handle.current.set(Number(e.id), e)
327
+ sub.handlers.onSnapshot?.(ents, meta)
328
+ if (sub.filter.id !== undefined && ents[0]) {
329
+ sub.handlers.onUpdate?.(ents[0], {seq: msg.seq})
276
330
  }
277
331
  }
278
332
 
279
333
  private handleUpdate(msg: UpdateMessage) {
280
334
  const ent = this.parseEntity(msg.entity)
281
335
  for (const subId of msg.sub_ids) {
282
- const entSub = this.entitySubs.get(subId)
283
- if (entSub) {
284
- entSub.handle.current = ent
285
- entSub.onUpdate(ent)
286
- continue
287
- }
288
- const boundsSub = this.boundsSubs.get(subId)
289
- if (boundsSub) {
290
- boundsSub.handle.current.set(msg.entity_id, ent)
291
- boundsSub.onUpdate?.(ent)
292
- }
336
+ const sub = this.entitySubs.get(subId)
337
+ if (!sub) continue
338
+ sub.handle.current.set(msg.entity_id, ent)
339
+ sub.handlers.onUpdate?.(ent, {seq: msg.seq})
293
340
  }
294
341
  }
295
342
 
296
343
  private handleBoundsDelta(msg: BoundsDeltaMessage) {
297
- const sub = this.boundsSubs.get(msg.sub_id)
344
+ const sub = this.entitySubs.get(msg.sub_id)
298
345
  if (!sub) return
346
+ const meta = {seq: msg.seq, truncated: msg.truncated === true}
299
347
  const entered = msg.entered.map((e) => this.parseEntity(e))
300
348
  for (const e of entered) sub.handle.current.set(Number(e.id), e)
301
349
  for (const id of msg.exited) sub.handle.current.delete(id)
302
- sub.onBoundsDelta?.(entered, msg.exited)
350
+ sub.handlers.onBoundsDelta?.(entered, msg.exited, meta)
303
351
  }
304
352
 
305
353
  private handleError(msg: {sub_id?: string; error: string}) {
306
354
  if (!msg.sub_id) return
307
- const entSub = this.entitySubs.get(msg.sub_id)
308
- if (entSub) {
355
+ const sub = this.entitySubs.get(msg.sub_id)
356
+ if (!sub) return
357
+ this.entitySubs.delete(msg.sub_id)
358
+ sub.handlers.onError?.(new Error(msg.error))
359
+ }
360
+
361
+ private handleEntityDeleted(msg: EntityDeletedMessage) {
362
+ const sub = this.entitySubs.get(msg.sub_id)
363
+ if (!sub) return
364
+ sub.handle.current.delete(msg.entity_id)
365
+ if (sub.filter.id !== undefined) {
309
366
  this.entitySubs.delete(msg.sub_id)
310
- return
311
- }
312
- const boundsSub = this.boundsSubs.get(msg.sub_id)
313
- if (boundsSub) {
314
- this.boundsSubs.delete(msg.sub_id)
315
367
  }
368
+ sub.handlers.onDeleted?.(String(msg.entity_id), {seq: msg.seq})
316
369
  }
317
370
  }
@@ -39,7 +39,6 @@ export type UnsubscribeMessage = {
39
39
  export type SubscribeEntityMessage = {
40
40
  type: 'subscribe_entity'
41
41
  sub_id: string
42
- entity_type: 'ship' | 'warehouse' | 'container' | 'nexus'
43
42
  entity_id: string
44
43
  }
45
44
 
@@ -79,8 +78,8 @@ export type AckMessage = {
79
78
  }
80
79
 
81
80
  export type WireEntity = Record<string, unknown> & {
82
- type: number
83
- type_name: 'ship' | 'warehouse' | 'container' | 'nexus'
81
+ type: number | string
82
+ type_name?: string
84
83
  id: string | number
85
84
  owner: string
86
85
  coordinates: WireCoordinates
@@ -112,6 +111,13 @@ export type BoundsDeltaMessage = {
112
111
  truncated?: boolean
113
112
  }
114
113
 
114
+ export type EntityDeletedMessage = {
115
+ type: 'entity_deleted'
116
+ sub_id: string
117
+ entity_id: number
118
+ seq: number
119
+ }
120
+
115
121
  export type EventMessage = {
116
122
  type: 'event'
117
123
  sub_id: string
@@ -138,6 +144,7 @@ export type ServerMessage =
138
144
  | SnapshotMessage
139
145
  | UpdateMessage
140
146
  | BoundsDeltaMessage
147
+ | EntityDeletedMessage
141
148
  | EventMessage
142
149
  | EventCatchupCompleteMessage
143
150
  | PongMessage
package/src/types.ts CHANGED
@@ -23,7 +23,7 @@ export const MAX_ORBITAL_ALTITUDE = 3000
23
23
  export const BASE_ORBITAL_MASS = 100000
24
24
 
25
25
  export const MIN_TRANSFER_DISTANCE_PLANETARY_STRUCTURE = 100
26
- export const MIN_TRANSFER_DISTANCE_ORBITAL_VESSEL = 300
26
+ export const MIN_TRANSFER_DISTANCE_ORBITAL_VESSEL = 200
27
27
 
28
28
  export interface ShipLike {
29
29
  coordinates: ServerContract.Types.coordinates