@spacemolt/lib 12.2.0 → 12.2.2

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/dist/account.d.ts CHANGED
@@ -160,6 +160,8 @@ export declare class Account {
160
160
  private subscribedMarketBaseId;
161
161
  private observationSubscribedState;
162
162
  private observationActiveScanState;
163
+ /** Invalidates an in-flight reconciliation whenever a newer cache update arrives. */
164
+ private stateRevision;
163
165
  private subscribedObservationPoiId;
164
166
  private readonly reconnectedListeners;
165
167
  private readonly reconnectingListeners;
@@ -374,6 +376,22 @@ export declare class Account {
374
376
  * after every state update that could carry a `location` change.
375
377
  */
376
378
  private checkSubscriptionsAgainstLocation;
379
+ /**
380
+ * Fleet followers do not submit the movement mutation themselves, so their
381
+ * arrival is delivered as a plain `ok` push rather than an `action_result`
382
+ * carrying a state delta. Bridge the location data present on those pushes
383
+ * into the cache. Fleet docking is the one exception: its push only contains
384
+ * the station's display name, not the canonical base ID used by `docked_at`,
385
+ * so refresh the canonical snapshot instead of caching the wrong identifier.
386
+ */
387
+ private applyFleetMovementPush;
388
+ /**
389
+ * Fleet movement pushes omit follower fuel and skill deltas, while docking
390
+ * omits the canonical base ID. Reconcile those fields from one authoritative
391
+ * snapshot. A newer movement push invalidates an older in-flight query so a
392
+ * delayed dock/arrival snapshot cannot move the cache backwards.
393
+ */
394
+ private reconcileFleetState;
377
395
  /** Close the connection deliberately. Suppresses auto-reconnect. */
378
396
  close(): void;
379
397
  /** Fired after a successful reconnect + re-auth. Returns an unsubscribe function. */
package/dist/index.js CHANGED
@@ -4027,6 +4027,7 @@ class Account {
4027
4027
  subscribedMarketBaseId;
4028
4028
  observationSubscribedState = false;
4029
4029
  observationActiveScanState = false;
4030
+ stateRevision = 0;
4030
4031
  subscribedObservationPoiId;
4031
4032
  reconnectedListeners = new Set;
4032
4033
  reconnectingListeners = new Set;
@@ -4195,6 +4196,8 @@ class Account {
4195
4196
  const result = await this.commands.spacemolt.get_status();
4196
4197
  const snapshot = requireStructuredContent(result, "spacemolt/get_status");
4197
4198
  const changed = this.cache.seed(snapshot);
4199
+ if (changed.length)
4200
+ this.stateRevision++;
4198
4201
  if (changed.includes("location"))
4199
4202
  this.checkSubscriptionsAgainstLocation();
4200
4203
  if (changed.length)
@@ -4227,8 +4230,8 @@ class Account {
4227
4230
  return this.withRateLimitRetry(async () => {
4228
4231
  const id = this.claimRequestId(requestId);
4229
4232
  const promise = this.correlator.awaitQuery(id);
4230
- this.sendFrame(tool, action, payload, id);
4231
4233
  try {
4234
+ this.sendFrame(tool, action, payload, id);
4232
4235
  return await withTimeout(promise, this.queryTimeoutMs, `No response to ${tool}/${action} within ${this.queryTimeoutMs}ms`);
4233
4236
  } catch (err) {
4234
4237
  this.correlator.cancel(id);
@@ -4240,12 +4243,10 @@ class Account {
4240
4243
  const mutationTimeoutMs = TRANSIT_ACTIONS.has(`${tool}/${action}`) ? this.mutationTimeoutMs : this.fastMutationTimeoutMs;
4241
4244
  return this.enqueueMutation(() => this.withRateLimitRetry(() => {
4242
4245
  const id = this.claimRequestId(requestId);
4243
- const promise = this.awaitMutationWithTimeout(id, mutationTimeoutMs, onAck);
4244
- this.sendFrame(tool, action, payload, id);
4245
- return promise;
4246
+ return this.awaitMutationWithTimeout(id, mutationTimeoutMs, () => this.sendFrame(tool, action, payload, id), onAck);
4246
4247
  }));
4247
4248
  }
4248
- awaitMutationWithTimeout(requestId, mutationTimeoutMs, onAck) {
4249
+ awaitMutationWithTimeout(requestId, mutationTimeoutMs, send, onAck) {
4249
4250
  return new Promise((resolve, reject) => {
4250
4251
  let timer;
4251
4252
  const arm = (ms, message) => {
@@ -4269,6 +4270,14 @@ class Account {
4269
4270
  clearTimeout(timer);
4270
4271
  reject(err instanceof Error ? err : new Error(String(err)));
4271
4272
  });
4273
+ try {
4274
+ send();
4275
+ } catch (err) {
4276
+ if (timer)
4277
+ clearTimeout(timer);
4278
+ this.correlator.cancel(requestId);
4279
+ reject(err instanceof Error ? err : new Error(String(err)));
4280
+ }
4272
4281
  });
4273
4282
  }
4274
4283
  send(tool, action, payload, requestId) {
@@ -4331,6 +4340,8 @@ class Account {
4331
4340
  nearby_players: nearbyPlayers,
4332
4341
  nearby_player_count: view.nearby.size
4333
4342
  });
4343
+ if (changed.length)
4344
+ this.stateRevision++;
4334
4345
  if (changed.length)
4335
4346
  this.emitStateChange(changed);
4336
4347
  }
@@ -4349,6 +4360,123 @@ class Account {
4349
4360
  this.subscribedObservationPoiId = undefined;
4350
4361
  }
4351
4362
  }
4363
+ applyFleetMovementPush(frame) {
4364
+ if (frame.type !== "ok" || !isRecord(frame.payload) || typeof frame.payload.action !== "string")
4365
+ return;
4366
+ const payload = frame.payload;
4367
+ let changed = [];
4368
+ let reconcile = false;
4369
+ const currentLocation = this.cache.location;
4370
+ const currentSystem = {
4371
+ connections: currentLocation?.connections,
4372
+ empire: currentLocation?.empire,
4373
+ security_status: currentLocation?.security_status,
4374
+ system_id: currentLocation?.system_id,
4375
+ system_name: currentLocation?.system_name
4376
+ };
4377
+ switch (payload.action) {
4378
+ case "fleet_jump": {
4379
+ if (typeof payload.destination !== "string" || typeof payload.arrival_tick !== "number") {
4380
+ this.warnMalformedFrame(frame);
4381
+ return;
4382
+ }
4383
+ changed = this.cache.applyDelta({
4384
+ location: {
4385
+ in_transit: true,
4386
+ transit_arrival_tick: payload.arrival_tick,
4387
+ transit_dest_system_id: payload.destination,
4388
+ transit_type: "jump"
4389
+ }
4390
+ });
4391
+ reconcile = true;
4392
+ break;
4393
+ }
4394
+ case "jumped":
4395
+ case "pathfinder_arrival": {
4396
+ if (typeof payload.system_id !== "string" || typeof payload.system !== "string") {
4397
+ this.warnMalformedFrame(frame);
4398
+ return;
4399
+ }
4400
+ changed = this.cache.applyDelta({
4401
+ location: {
4402
+ in_transit: false,
4403
+ poi_id: typeof payload.poi === "string" ? payload.poi : undefined,
4404
+ system_id: payload.system_id,
4405
+ system_name: payload.system
4406
+ }
4407
+ });
4408
+ reconcile = true;
4409
+ break;
4410
+ }
4411
+ case "fleet_travel": {
4412
+ if (typeof payload.destination !== "string" || typeof payload.arrival_tick !== "number") {
4413
+ this.warnMalformedFrame(frame);
4414
+ return;
4415
+ }
4416
+ changed = this.cache.applyDelta({
4417
+ location: {
4418
+ ...currentSystem,
4419
+ in_transit: true,
4420
+ transit_arrival_tick: payload.arrival_tick,
4421
+ transit_dest_poi_id: payload.destination,
4422
+ transit_type: "travel"
4423
+ }
4424
+ });
4425
+ reconcile = true;
4426
+ break;
4427
+ }
4428
+ case "arrived": {
4429
+ if (typeof payload.poi_id !== "string" || typeof payload.poi !== "string") {
4430
+ this.warnMalformedFrame(frame);
4431
+ return;
4432
+ }
4433
+ changed = this.cache.applyDelta({
4434
+ location: {
4435
+ ...currentSystem,
4436
+ in_transit: false,
4437
+ poi_id: payload.poi_id,
4438
+ poi_name: payload.poi
4439
+ }
4440
+ });
4441
+ reconcile = true;
4442
+ break;
4443
+ }
4444
+ case "fleet_undock":
4445
+ changed = currentLocation ? this.cache.patchSection("location", { docked_at: undefined }) : this.cache.applyDelta({ location: {} });
4446
+ break;
4447
+ case "fleet_dock":
4448
+ reconcile = true;
4449
+ break;
4450
+ default:
4451
+ return;
4452
+ }
4453
+ const revision = ++this.stateRevision;
4454
+ if (changed.includes("location"))
4455
+ this.checkSubscriptionsAgainstLocation();
4456
+ if (changed.length)
4457
+ this.emitStateChange(changed);
4458
+ if (reconcile)
4459
+ this.reconcileFleetState(revision, payload.action);
4460
+ }
4461
+ async reconcileFleetState(revision, action) {
4462
+ try {
4463
+ const result = await this.commands.spacemolt.get_status();
4464
+ const snapshot = requireStructuredContent(result, "spacemolt/get_status");
4465
+ if (revision !== this.stateRevision)
4466
+ return;
4467
+ const changed = this.cache.seed(snapshot);
4468
+ if (changed.length)
4469
+ this.stateRevision++;
4470
+ if (changed.includes("location"))
4471
+ this.checkSubscriptionsAgainstLocation();
4472
+ if (changed.length)
4473
+ this.emitStateChange(changed);
4474
+ } catch (err) {
4475
+ if (revision === this.stateRevision) {
4476
+ console.warn(`[spacemolt] failed to reconcile state after ${action}`, err);
4477
+ }
4478
+ }
4479
+ }
4352
4480
  close() {
4353
4481
  this.userClosing = true;
4354
4482
  this.socket.close();
@@ -4465,6 +4593,8 @@ class Account {
4465
4593
  const delta = frame.payload.result;
4466
4594
  if (delta) {
4467
4595
  const changed = this.cache.applyDelta(delta);
4596
+ if (changed.length)
4597
+ this.stateRevision++;
4468
4598
  if (changed.includes("location"))
4469
4599
  this.checkSubscriptionsAgainstLocation();
4470
4600
  if (changed.length)
@@ -4499,6 +4629,7 @@ class Account {
4499
4629
  return;
4500
4630
  }
4501
4631
  default:
4632
+ this.applyFleetMovementPush(frame);
4502
4633
  this.emitter.emit(frame);
4503
4634
  }
4504
4635
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacemolt/lib",
3
- "version": "12.2.0",
3
+ "version": "12.2.2",
4
4
  "description": "The definitive TypeScript library for SpaceMolt. WebSocket-v2-first, multi-account, with local state caches. Internals are regenerated from the server's OpenAPI spec.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",