@shipload/sdk 2.0.0-rc2 → 2.0.0-rc21

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.
Files changed (84) hide show
  1. package/README.md +1 -349
  2. package/lib/shipload.d.ts +1729 -1127
  3. package/lib/shipload.js +7944 -3165
  4. package/lib/shipload.js.map +1 -1
  5. package/lib/shipload.m.js +7487 -2840
  6. package/lib/shipload.m.js.map +1 -1
  7. package/package.json +6 -4
  8. package/src/capabilities/crafting.ts +22 -0
  9. package/src/capabilities/gathering.ts +36 -0
  10. package/src/capabilities/guards.ts +3 -8
  11. package/src/capabilities/hauling.ts +22 -0
  12. package/src/capabilities/index.ts +4 -1
  13. package/src/capabilities/modules.ts +86 -0
  14. package/src/capabilities/storage.ts +101 -9
  15. package/src/contracts/server.ts +785 -293
  16. package/src/data/capabilities.ts +408 -0
  17. package/src/data/categories.ts +55 -0
  18. package/src/data/colors.ts +71 -0
  19. package/src/data/entities.json +50 -0
  20. package/src/data/item-ids.ts +75 -0
  21. package/src/data/items.json +252 -0
  22. package/src/data/locations.ts +53 -0
  23. package/src/data/metadata.ts +208 -0
  24. package/src/data/nebula-adjectives.json +211 -0
  25. package/src/data/nebula-nouns.json +151 -0
  26. package/src/data/recipes-runtime.ts +65 -0
  27. package/src/data/recipes.json +878 -0
  28. package/src/data/syllables.json +1386 -780
  29. package/src/data/tiers.ts +45 -0
  30. package/src/derivation/crafting.ts +348 -0
  31. package/src/derivation/index.ts +30 -0
  32. package/src/derivation/location-size.ts +15 -0
  33. package/src/derivation/resources.ts +112 -0
  34. package/src/derivation/stats.ts +146 -0
  35. package/src/derivation/stratum.ts +134 -0
  36. package/src/derivation/tiers.ts +54 -0
  37. package/src/entities/cargo-utils.ts +10 -68
  38. package/src/entities/container.ts +37 -0
  39. package/src/entities/entity-inventory.ts +13 -13
  40. package/src/entities/inventory-accessor.ts +2 -6
  41. package/src/entities/location.ts +5 -200
  42. package/src/entities/makers.ts +144 -17
  43. package/src/entities/player.ts +1 -274
  44. package/src/entities/ship-deploy.ts +258 -0
  45. package/src/entities/ship.ts +28 -34
  46. package/src/entities/warehouse.ts +35 -7
  47. package/src/errors.ts +59 -5
  48. package/src/format.ts +12 -0
  49. package/src/index-module.ts +188 -50
  50. package/src/managers/actions.ts +138 -88
  51. package/src/managers/context.ts +19 -9
  52. package/src/managers/index.ts +0 -1
  53. package/src/managers/locations.ts +2 -85
  54. package/src/market/items.ts +41 -0
  55. package/src/nft/description.ts +176 -0
  56. package/src/nft/deserializers.ts +83 -0
  57. package/src/nft/index.ts +2 -0
  58. package/src/resolution/describe-module.ts +165 -0
  59. package/src/resolution/display-name.ts +43 -0
  60. package/src/resolution/resolve-item.ts +358 -0
  61. package/src/scheduling/projection.ts +200 -67
  62. package/src/scheduling/schedule.ts +2 -2
  63. package/src/shipload.ts +10 -5
  64. package/src/subscriptions/connection.ts +154 -0
  65. package/src/subscriptions/debug.ts +17 -0
  66. package/src/subscriptions/index.ts +5 -0
  67. package/src/subscriptions/manager.ts +240 -0
  68. package/src/subscriptions/mappers.ts +28 -0
  69. package/src/subscriptions/types.ts +143 -0
  70. package/src/travel/travel.ts +37 -23
  71. package/src/types/capabilities.ts +11 -14
  72. package/src/types/entity-traits.ts +3 -4
  73. package/src/types/entity.ts +9 -6
  74. package/src/types.ts +72 -72
  75. package/src/utils/system.ts +66 -53
  76. package/src/capabilities/extraction.ts +0 -37
  77. package/src/data/goods.json +0 -23
  78. package/src/managers/trades.ts +0 -119
  79. package/src/market/goods.ts +0 -31
  80. package/src/market/market.ts +0 -208
  81. package/src/market/rolls.ts +0 -8
  82. package/src/trading/collect.ts +0 -938
  83. package/src/trading/deal.ts +0 -207
  84. package/src/trading/trade.ts +0 -203
@@ -8,28 +8,44 @@ import {
8
8
  EntityCapabilities,
9
9
  EntityState,
10
10
  } from '../types/capabilities'
11
+ import {
12
+ ENTITY_CAPACITY_EXCEEDED,
13
+ RECIPE_INPUTS_EXCESS,
14
+ RECIPE_INPUTS_INSUFFICIENT,
15
+ RECIPE_INPUTS_INVALID,
16
+ RECIPE_NOT_FOUND,
17
+ SHIP_CARGO_NOT_LOADED,
18
+ } from '../errors'
19
+ import {getRecipe, RecipeInput} from '../data/recipes-runtime'
20
+ import {getItem} from '../market/items'
11
21
  import {distanceBetweenCoordinates, lerp} from '../travel/travel'
12
- import {calcCargoMass} from '../capabilities/storage'
13
- import {getGood} from '../market/goods'
22
+ import {
23
+ calcStacksMass,
24
+ cargoItemToStack,
25
+ CargoStack,
26
+ mergeStacks,
27
+ removeFromStacks,
28
+ stackToCargoItem,
29
+ } from '../capabilities/storage'
14
30
  import * as schedule from './schedule'
15
31
  import {ScheduleData} from './schedule'
16
32
 
17
33
  export interface ProjectedEntity {
18
34
  location: Coordinates
19
35
  energy: UInt16
20
- cargoMass: UInt64
36
+ cargo: CargoStack[]
21
37
  shipMass: UInt32
22
38
  capacity?: UInt64
23
39
  engines?: ServerContract.Types.movement_stats
24
40
  loaders?: ServerContract.Types.loader_stats
25
41
  generator?: ServerContract.Types.energy_stats
26
- trade?: ServerContract.Types.trade_stats
42
+ hauler?: ServerContract.Types.hauler_stats
43
+ readonly cargoMass: UInt64
27
44
  readonly totalMass: UInt64
28
45
 
29
46
  hasMovement(): boolean
30
47
  hasStorage(): boolean
31
48
  hasLoaders(): boolean
32
- hasTrade(): boolean
33
49
 
34
50
  capabilities(): EntityCapabilities
35
51
  state(): EntityState
@@ -42,7 +58,7 @@ export interface Projectable extends ScheduleData {
42
58
  generator?: ServerContract.Types.energy_stats
43
59
  engines?: ServerContract.Types.movement_stats
44
60
  loaders?: ServerContract.Types.loader_stats
45
- trade?: ServerContract.Types.trade_stats
61
+ hauler?: ServerContract.Types.hauler_stats
46
62
  capacity?: UInt32
47
63
  cargo: ServerContract.Types.cargo_item[]
48
64
  cargomass: UInt32
@@ -54,24 +70,29 @@ function getHullMass(entity: Projectable): UInt32 {
54
70
  }
55
71
 
56
72
  export function createProjectedEntity(entity: Projectable): ProjectedEntity {
57
- const cargoMass = calcCargoMass(entity)
58
73
  const shipMass = getHullMass(entity)
59
74
  const loaders = entity.loaders
60
75
  const engines = entity.engines
61
76
  const generator = entity.generator
62
- const trade = entity.trade
77
+ const hauler = entity.hauler
63
78
  const capacity = entity.capacity
64
79
 
80
+ const cargo: CargoStack[] = entity.cargo.map(cargoItemToStack)
81
+
65
82
  const projected: ProjectedEntity = {
66
83
  location: Coordinates.from(entity.coordinates),
67
84
  energy: UInt16.from(entity.energy ?? 0),
68
- cargoMass,
85
+ cargo,
69
86
  shipMass,
70
87
  capacity: capacity ? UInt64.from(capacity) : undefined,
71
88
  engines,
72
89
  generator,
90
+ hauler,
73
91
  loaders,
74
- trade,
92
+
93
+ get cargoMass() {
94
+ return calcStacksMass(this.cargo)
95
+ },
75
96
 
76
97
  get totalMass() {
77
98
  let mass = UInt64.from(this.shipMass).adding(this.cargoMass)
@@ -93,10 +114,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
93
114
  return capsHasLoaders(this.capabilities())
94
115
  },
95
116
 
96
- hasTrade() {
97
- return this.trade !== undefined
98
- },
99
-
100
117
  capabilities(): EntityCapabilities {
101
118
  return {
102
119
  hullmass: this.shipMass,
@@ -104,7 +121,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
104
121
  engines: this.engines,
105
122
  generator: this.generator,
106
123
  loaders: this.loaders,
107
- trade: this.trade,
108
124
  }
109
125
  },
110
126
 
@@ -114,7 +130,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
114
130
  location: ServerContract.Types.coordinates.from(this.location),
115
131
  energy: this.energy,
116
132
  cargomass: UInt32.from(this.cargoMass),
117
- cargo: entity.cargo,
133
+ cargo: this.cargo.map(stackToCargoItem),
118
134
  }
119
135
  },
120
136
  }
@@ -169,76 +185,191 @@ function applyFlightTask(
169
185
  }
170
186
  }
171
187
 
172
- function getGoodMass(good_id: UInt16 | number): UInt32 {
173
- const good = getGood(good_id)
174
- return good.mass
188
+ function addCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
189
+ projected.cargo = mergeStacks(projected.cargo, cargoItemToStack(item))
190
+ }
191
+
192
+ function removeCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
193
+ projected.cargo = removeFromStacks(projected.cargo, cargoItemToStack(item))
175
194
  }
176
195
 
177
- function applyLoadTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
196
+ function applyAddCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
178
197
  for (const item of task.cargo) {
179
- const good_mass = getGoodMass(item.good_id)
180
- projected.cargoMass = projected.cargoMass.adding(good_mass.multiplying(item.quantity))
198
+ addCargoItem(projected, item)
181
199
  }
182
200
  }
183
201
 
184
- function applyUnloadTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
202
+ function applyRemoveCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
185
203
  for (const item of task.cargo) {
186
- const good_mass = getGoodMass(item.good_id)
187
- const cargoMass = good_mass.multiplying(item.quantity)
188
- projected.cargoMass = projected.cargoMass.gt(cargoMass)
189
- ? projected.cargoMass.subtracting(cargoMass)
190
- : UInt64.from(0)
204
+ removeCargoItem(projected, item)
191
205
  }
192
206
  }
193
207
 
194
- function applyExtractTask(
208
+ function applyEnergyCost(projected: ProjectedEntity, task: ServerContract.Types.task): void {
209
+ if (!task.energy_cost) return
210
+ const energyCost = UInt16.from(task.energy_cost)
211
+ projected.energy = projected.energy.gt(energyCost)
212
+ ? UInt16.from(projected.energy.subtracting(energyCost))
213
+ : UInt16.from(0)
214
+ }
215
+
216
+ function applyGatherTask(
195
217
  projected: ProjectedEntity,
196
218
  task: ServerContract.Types.task,
197
219
  options: {complete: boolean}
198
220
  ): void {
199
221
  if (!options.complete) return
222
+ applyEnergyCost(projected, task)
223
+ if (!task.entitytarget) {
224
+ applyAddCargoTask(projected, task)
225
+ }
226
+ }
200
227
 
201
- if (task.energy_cost) {
202
- const energyCost = UInt16.from(task.energy_cost)
203
- projected.energy = projected.energy.gt(energyCost)
204
- ? UInt16.from(projected.energy.subtracting(energyCost))
205
- : UInt16.from(0)
228
+ function applyCraftTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
229
+ applyEnergyCost(projected, task)
230
+ if (task.cargo.length === 0) return
231
+
232
+ for (let i = 0; i < task.cargo.length - 1; i++) {
233
+ removeCargoItem(projected, task.cargo[i])
206
234
  }
235
+ addCargoItem(projected, task.cargo[task.cargo.length - 1])
236
+ }
207
237
 
208
- for (const item of task.cargo) {
209
- const good_mass = getGoodMass(item.good_id)
210
- projected.cargoMass = projected.cargoMass.adding(good_mass.multiplying(item.quantity))
238
+ function applyDeployTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
239
+ applyEnergyCost(projected, task)
240
+ if (task.cargo.length > 0) {
241
+ removeCargoItem(projected, task.cargo[0])
211
242
  }
212
243
  }
213
244
 
214
- export function projectEntity(entity: Projectable): ProjectedEntity {
245
+ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
246
+ switch (task.type.toNumber()) {
247
+ case TaskType.RECHARGE:
248
+ applyRechargeTask(projected, task, {complete: true})
249
+ break
250
+ case TaskType.TRAVEL:
251
+ applyFlightTask(projected, task, {complete: true})
252
+ break
253
+ case TaskType.LOAD:
254
+ case TaskType.UNWRAP:
255
+ applyAddCargoTask(projected, task)
256
+ break
257
+ case TaskType.UNLOAD:
258
+ case TaskType.WRAP:
259
+ applyRemoveCargoTask(projected, task)
260
+ break
261
+ case TaskType.GATHER:
262
+ applyGatherTask(projected, task, {complete: true})
263
+ break
264
+ case TaskType.CRAFT:
265
+ applyCraftTask(projected, task)
266
+ break
267
+ case TaskType.DEPLOY:
268
+ applyDeployTask(projected, task)
269
+ break
270
+ }
271
+ }
272
+
273
+ export interface ProjectionOptions {
274
+ upToTaskIndex?: number
275
+ }
276
+
277
+ export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
215
278
  const projected = createProjectedEntity(entity)
279
+ if (!entity.schedule || entity.schedule.tasks.length === 0) return projected
216
280
 
217
- if (!entity.schedule) {
218
- return projected
281
+ const tasks = entity.schedule.tasks
282
+ const taskCount =
283
+ options?.upToTaskIndex !== undefined
284
+ ? Math.max(0, Math.min(options.upToTaskIndex, tasks.length))
285
+ : tasks.length
286
+
287
+ for (let i = 0; i < taskCount; i++) {
288
+ applyTask(projected, tasks[i])
219
289
  }
290
+ return projected
291
+ }
220
292
 
221
- for (const task of entity.schedule.tasks) {
222
- switch (task.type.toNumber()) {
223
- case TaskType.RECHARGE:
224
- applyRechargeTask(projected, task, {complete: true})
225
- break
226
- case TaskType.TRAVEL:
227
- applyFlightTask(projected, task, {complete: true})
228
- break
229
- case TaskType.LOAD:
230
- applyLoadTask(projected, task)
231
- break
232
- case TaskType.UNLOAD:
233
- applyUnloadTask(projected, task)
234
- break
235
- case TaskType.EXTRACT:
236
- applyExtractTask(projected, task, {complete: true})
293
+ function getRecipeInputsForOutput(outputItemId: number): RecipeInput[] | undefined {
294
+ const recipe = getRecipe(outputItemId)
295
+ return recipe?.inputs
296
+ }
297
+
298
+ function validateCraftTask(task: ServerContract.Types.task, projected: ProjectedEntity): void {
299
+ if (task.cargo.length === 0) return
300
+
301
+ const output = task.cargo[task.cargo.length - 1]
302
+ const inputs = task.cargo.slice(0, -1)
303
+ const craftQuantity = output.quantity.toNumber()
304
+
305
+ const recipe = getRecipeInputsForOutput(output.item_id.toNumber())
306
+ if (!recipe) throw new Error(RECIPE_NOT_FOUND)
307
+
308
+ const groupedInputs: ServerContract.Types.cargo_item[][] = recipe.map(() => [])
309
+ for (const input of inputs) {
310
+ let matched = false
311
+ for (let ri = 0; ri < recipe.length; ri++) {
312
+ const req = recipe[ri]
313
+ if ('itemId' in req) {
314
+ if (input.item_id.toNumber() === req.itemId) {
315
+ groupedInputs[ri].push(input)
316
+ matched = true
317
+ break
318
+ }
319
+ } else {
320
+ const item = getItem(input.item_id)
321
+ if (item.category === req.category && item.tier === req.tier) {
322
+ groupedInputs[ri].push(input)
323
+ matched = true
324
+ break
325
+ }
326
+ }
327
+ }
328
+ if (!matched) throw new Error(RECIPE_INPUTS_INVALID)
329
+ }
330
+
331
+ for (let ri = 0; ri < recipe.length; ri++) {
332
+ const stacks = groupedInputs[ri]
333
+ let provided = 0
334
+ for (const stack of stacks) {
335
+ provided += stack.quantity.toNumber()
336
+ }
337
+ const required = recipe[ri].quantity * craftQuantity
338
+ if (provided < required) throw new Error(RECIPE_INPUTS_INSUFFICIENT)
339
+ if (provided !== required) throw new Error(RECIPE_INPUTS_EXCESS)
340
+ }
341
+
342
+ for (const input of inputs) {
343
+ let found = false
344
+ for (const pc of projected.cargo) {
345
+ if (
346
+ pc.item_id.toNumber() === input.item_id.toNumber() &&
347
+ pc.stats.toString() === input.stats.toString()
348
+ ) {
349
+ if (pc.quantity.toNumber() < input.quantity.toNumber()) {
350
+ throw new Error(RECIPE_INPUTS_INSUFFICIENT)
351
+ }
352
+ found = true
237
353
  break
354
+ }
238
355
  }
356
+ if (!found) throw new Error(SHIP_CARGO_NOT_LOADED)
239
357
  }
358
+ }
240
359
 
241
- return projected
360
+ export function validateSchedule(entity: Projectable): void {
361
+ if (!entity.schedule || entity.schedule.tasks.length === 0) return
362
+
363
+ const projected = createProjectedEntity(entity)
364
+ for (const task of entity.schedule.tasks) {
365
+ if (task.type.toNumber() === TaskType.CRAFT) {
366
+ validateCraftTask(task, projected)
367
+ }
368
+ applyTask(projected, task)
369
+ if (projected.capacity && projected.cargoMass.gt(projected.capacity)) {
370
+ throw new Error(ENTITY_CAPACITY_EXCEEDED)
371
+ }
372
+ }
242
373
  }
243
374
 
244
375
  export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
@@ -269,19 +400,21 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
269
400
  applyFlightTask(projected, task, {complete: taskComplete, progress})
270
401
  break
271
402
  case TaskType.LOAD:
272
- if (taskComplete) {
273
- applyLoadTask(projected, task)
274
- }
403
+ case TaskType.UNWRAP:
404
+ if (taskComplete) applyAddCargoTask(projected, task)
275
405
  break
276
406
  case TaskType.UNLOAD:
277
- if (taskComplete) {
278
- applyUnloadTask(projected, task)
279
- }
407
+ case TaskType.WRAP:
408
+ if (taskComplete) applyRemoveCargoTask(projected, task)
280
409
  break
281
- case TaskType.EXTRACT:
282
- if (taskComplete) {
283
- applyExtractTask(projected, task, {complete: true})
284
- }
410
+ case TaskType.GATHER:
411
+ if (taskComplete) applyGatherTask(projected, task, {complete: true})
412
+ break
413
+ case TaskType.CRAFT:
414
+ if (taskComplete) applyCraftTask(projected, task)
415
+ break
416
+ case TaskType.DEPLOY:
417
+ if (taskComplete) applyDeployTask(projected, task)
285
418
  break
286
419
  }
287
420
  }
@@ -174,6 +174,6 @@ export function isUnloading(entity: ScheduleData, now: Date): boolean {
174
174
  return isTaskType(entity, TaskType.UNLOAD, now)
175
175
  }
176
176
 
177
- export function isExtracting(entity: ScheduleData, now: Date): boolean {
178
- return isTaskType(entity, TaskType.EXTRACT, now)
177
+ export function isGathering(entity: ScheduleData, now: Date): boolean {
178
+ return isTaskType(entity, TaskType.GATHER, now)
179
179
  }
package/src/shipload.ts CHANGED
@@ -7,15 +7,16 @@ import {GameContext} from './managers/context'
7
7
  import {EntitiesManager} from './managers/entities'
8
8
  import {PlayersManager} from './managers/players'
9
9
  import {LocationsManager} from './managers/locations'
10
- import {TradesManager} from './managers/trades'
11
10
  import {EpochsManager} from './managers/epochs'
12
11
  import {ActionsManager} from './managers/actions'
12
+ import {SubscriptionsManager} from './subscriptions/manager'
13
13
  import {GameState} from './entities/gamestate'
14
14
 
15
15
  interface ShiploadOptions {
16
16
  platformContractName?: string
17
17
  serverContractName?: string
18
18
  client?: APIClient
19
+ subscriptionsUrl?: string
19
20
  }
20
21
 
21
22
  interface ShiploadConstructorOptions extends ShiploadOptions {
@@ -39,6 +40,10 @@ export class Shipload {
39
40
  : new ServerContract.Contract({client: apiClient})
40
41
 
41
42
  this._context = new GameContext(apiClient, server, platform)
43
+
44
+ if (constructorOptions?.subscriptionsUrl) {
45
+ this._context.setSubscriptionsUrl(constructorOptions.subscriptionsUrl)
46
+ }
42
47
  }
43
48
 
44
49
  static async load(
@@ -94,10 +99,6 @@ export class Shipload {
94
99
  return this._context.locations
95
100
  }
96
101
 
97
- get trades(): TradesManager {
98
- return this._context.trades
99
- }
100
-
101
102
  get epochs(): EpochsManager {
102
103
  return this._context.epochs
103
104
  }
@@ -106,6 +107,10 @@ export class Shipload {
106
107
  return this._context.actions
107
108
  }
108
109
 
110
+ get subscriptions(): SubscriptionsManager {
111
+ return this._context.subscriptions
112
+ }
113
+
109
114
  async getGame(reload = false): Promise<PlatformContract.Types.game_row> {
110
115
  return this._context.getGame(reload)
111
116
  }
@@ -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
+ Math.pow(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,5 @@
1
+ export * from './connection'
2
+ export * from './types'
3
+ export * from './manager'
4
+ export * from './mappers'
5
+ export {setSubscriptionsDebug, isSubscriptionsDebugEnabled} from './debug'