@spacemolt/lib 14.1.0 → 14.2.1
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/README.md +12 -2
- package/dist/account.d.ts +35 -14
- package/dist/index.js +79 -23
- package/dist/state/observation.d.ts +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ for await (const hit of account.events('battle_damage')) { /* ... */ }
|
|
|
138
138
|
|
|
139
139
|
### Subscriptions
|
|
140
140
|
|
|
141
|
-
Subscribe to a station's order book or to
|
|
141
|
+
Subscribe to a station's order book or to presence at your location; the
|
|
142
142
|
baseline snapshot seeds a local cache that the push stream keeps current:
|
|
143
143
|
|
|
144
144
|
```ts
|
|
@@ -146,9 +146,19 @@ await account.subscribeMarket(); // current docked station
|
|
|
146
146
|
account.market(baseId); // cached order book, kept live by `market_update`
|
|
147
147
|
|
|
148
148
|
await account.subscribeObservation();
|
|
149
|
-
account.observation();
|
|
149
|
+
const view = account.observation(); // kept live by `observation_update`
|
|
150
|
+
view?.nearby; // players at your POI, keyed by player_id
|
|
151
|
+
view?.system; // players system-wide
|
|
152
|
+
view?.pirates; // pirate NPCs, keyed by pirate_id
|
|
153
|
+
view?.empireNpcs; // empire NPCs, keyed by npc_id
|
|
154
|
+
view?.creatures; // wildlife, keyed by creature_id
|
|
155
|
+
view?.prizes; // intact captured ships, keyed by prize_id
|
|
150
156
|
```
|
|
151
157
|
|
|
158
|
+
The watch is not players only. It covers five of `get_nearby`'s six classes —
|
|
159
|
+
arena NPCs are the one it does not carry — and every class except wildlife is
|
|
160
|
+
also mirrored into `account.state.location`.
|
|
161
|
+
|
|
152
162
|
## Multi-account
|
|
153
163
|
|
|
154
164
|
`SpacemoltClient` manages many accounts, staggers connects to respect login
|
package/dist/account.d.ts
CHANGED
|
@@ -156,6 +156,8 @@ export declare class Account {
|
|
|
156
156
|
private readonly stateListeners;
|
|
157
157
|
private userClosing;
|
|
158
158
|
private reconnecting;
|
|
159
|
+
/** The close seen on the current reconnect attempt's socket, if any — the loop reads it to decide whether the attempt really succeeded. */
|
|
160
|
+
private attemptClose;
|
|
159
161
|
private mutationLane;
|
|
160
162
|
private marketSubscribedState;
|
|
161
163
|
private subscribedMarketBaseId;
|
|
@@ -343,14 +345,17 @@ export declare class Account {
|
|
|
343
345
|
/** The cached order book for a base, if subscribed. */
|
|
344
346
|
market(baseId: string): MarketBook | undefined;
|
|
345
347
|
/**
|
|
346
|
-
* Subscribe to a change-feed of
|
|
348
|
+
* Subscribe to a change-feed of presence at your current POI/system: players
|
|
349
|
+
* (POI and system-wide), plus the pirates, empire NPCs, wildlife and intact
|
|
350
|
+
* prizes at the POI — not players only. That is five of `get_nearby`'s six
|
|
351
|
+
* classes; arena NPCs are not in the feed, so an arena match still needs
|
|
352
|
+
* `get_nearby` to see its opponents.
|
|
347
353
|
* Returns the baseline and seeds the observation cache; `observation_update`
|
|
348
354
|
* pushes are merged automatically. Read with `account.observation()`. Also
|
|
349
|
-
* bridges POI-scoped
|
|
350
|
-
* `
|
|
351
|
-
* `
|
|
352
|
-
*
|
|
353
|
-
* `get_status`/mutation delta.
|
|
355
|
+
* bridges the POI-scoped classes into their matching `location` fields in the
|
|
356
|
+
* base state cache (see `bridgeObservationToLocation`), so
|
|
357
|
+
* `account.state.location` reflects live data too once subscribed, instead of
|
|
358
|
+
* only refreshing on the next `get_status`/mutation delta.
|
|
354
359
|
*
|
|
355
360
|
* Scoped to your current POI the same way `subscribeMarket` is scoped to
|
|
356
361
|
* your current dock — see the docked_at note there; the same silent
|
|
@@ -362,14 +367,30 @@ export declare class Account {
|
|
|
362
367
|
/** Unsubscribe from the observation watch and clear its cache. */
|
|
363
368
|
unsubscribeObservation(): Promise<void>;
|
|
364
369
|
/**
|
|
365
|
-
* Mirrors the observation watch's POI-scoped
|
|
366
|
-
* `location
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
370
|
+
* Mirrors the observation watch's POI-scoped presence into the matching
|
|
371
|
+
* `location` fields in the base state cache, so code reading
|
|
372
|
+
* `account.state.location` (rather than `account.observation()`) sees live
|
|
373
|
+
* data once subscribed: players, pirates, empire NPCs, intact prizes, their
|
|
374
|
+
* counts, and the cloaked-signature hint. Wildlife has no `location` field —
|
|
375
|
+
* read creatures from `account.observation()`.
|
|
376
|
+
*
|
|
377
|
+
* Three ways the bridged values differ from what `get_status` would return
|
|
378
|
+
* for the same POI, none of them fixable from here:
|
|
379
|
+
* - The arrays are uncapped. `get_status` truncates them (50 players, 20
|
|
380
|
+
* each of the rest) while reporting the pre-cap total in the count, so a
|
|
381
|
+
* bridged array can be longer than the server would ever send. The counts
|
|
382
|
+
* mean the same thing either way.
|
|
383
|
+
* - `offline_collapsed` is a `get_status` response cap the feed does not
|
|
384
|
+
* report, so it is left untouched at whatever the server last said. Above
|
|
385
|
+
* the crowding threshold `get_status` drops offline players from
|
|
386
|
+
* `nearby_players` and counts them there instead; the feed carries them in
|
|
387
|
+
* full, so once bridged the two no longer sum — read the array, not the sum.
|
|
388
|
+
* - `ship_name` on players and empire NPCs is the ship's display name here
|
|
389
|
+
* (its custom name, else the class name) where `get_status` sends the
|
|
390
|
+
* custom name alone. Present does not imply custom-named while subscribed.
|
|
391
|
+
*
|
|
392
|
+
* `unknown_signature` is written even when false, where `get_status` omits
|
|
393
|
+
* the key; the bridge has to write it to clear a signature that has gone.
|
|
373
394
|
* No-ops if `location` hasn't been seeded yet or nothing is subscribed.
|
|
374
395
|
*/
|
|
375
396
|
private bridgeObservationToLocation;
|
package/dist/index.js
CHANGED
|
@@ -3832,6 +3832,15 @@ function indexBy(items, key) {
|
|
|
3832
3832
|
}
|
|
3833
3833
|
return map;
|
|
3834
3834
|
}
|
|
3835
|
+
function merge(map, changed, departed, key) {
|
|
3836
|
+
for (const item of changed ?? []) {
|
|
3837
|
+
const k = key(item);
|
|
3838
|
+
if (k)
|
|
3839
|
+
map.set(k, item);
|
|
3840
|
+
}
|
|
3841
|
+
for (const id of departed ?? [])
|
|
3842
|
+
map.delete(id);
|
|
3843
|
+
}
|
|
3835
3844
|
|
|
3836
3845
|
class ObservationCache {
|
|
3837
3846
|
view = null;
|
|
@@ -3842,6 +3851,10 @@ class ObservationCache {
|
|
|
3842
3851
|
tick: 0,
|
|
3843
3852
|
nearby: indexBy(snapshot.nearby, (p) => p.player_id),
|
|
3844
3853
|
system: indexBy(snapshot.system_agents, (p) => p.player_id),
|
|
3854
|
+
pirates: indexBy(snapshot.pirates, (p) => p.pirate_id),
|
|
3855
|
+
empireNpcs: indexBy(snapshot.empire_npcs, (n) => n.npc_id),
|
|
3856
|
+
creatures: indexBy(snapshot.creatures, (c) => c.creature_id),
|
|
3857
|
+
prizes: indexBy(snapshot.prizes, (p) => p.prize_id),
|
|
3845
3858
|
cloaked: indexBy(snapshot.cloaked_contacts, (c) => c.target_id),
|
|
3846
3859
|
unknownSignature: snapshot.unknown_signature ?? false,
|
|
3847
3860
|
activeScan: snapshot.active_scan ?? false
|
|
@@ -3856,6 +3869,10 @@ class ObservationCache {
|
|
|
3856
3869
|
tick: update.tick,
|
|
3857
3870
|
nearby: new Map,
|
|
3858
3871
|
system: new Map,
|
|
3872
|
+
pirates: new Map,
|
|
3873
|
+
empireNpcs: new Map,
|
|
3874
|
+
creatures: new Map,
|
|
3875
|
+
prizes: new Map,
|
|
3859
3876
|
cloaked: new Map,
|
|
3860
3877
|
unknownSignature: false,
|
|
3861
3878
|
activeScan: false
|
|
@@ -3863,21 +3880,13 @@ class ObservationCache {
|
|
|
3863
3880
|
}
|
|
3864
3881
|
const v = this.view;
|
|
3865
3882
|
v.tick = update.tick;
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
v.system.set(p.player_id, p);
|
|
3874
|
-
for (const id of update.system_departed ?? [])
|
|
3875
|
-
v.system.delete(id);
|
|
3876
|
-
for (const c of update.cloaked_resolved ?? [])
|
|
3877
|
-
if (c.target_id)
|
|
3878
|
-
v.cloaked.set(c.target_id, c);
|
|
3879
|
-
for (const id of update.cloaked_lost ?? [])
|
|
3880
|
-
v.cloaked.delete(id);
|
|
3883
|
+
merge(v.nearby, update.nearby_changed, update.nearby_departed, (p) => p.player_id);
|
|
3884
|
+
merge(v.system, update.system_changed, update.system_departed, (p) => p.player_id);
|
|
3885
|
+
merge(v.pirates, update.pirates_changed, update.pirates_departed, (p) => p.pirate_id);
|
|
3886
|
+
merge(v.empireNpcs, update.empire_npcs_changed, update.empire_npcs_departed, (n) => n.npc_id);
|
|
3887
|
+
merge(v.creatures, update.creatures_changed, update.creatures_departed, (c) => c.creature_id);
|
|
3888
|
+
merge(v.prizes, update.prizes_changed, update.prizes_departed, (p) => p.prize_id);
|
|
3889
|
+
merge(v.cloaked, update.cloaked_resolved, update.cloaked_lost, (c) => c.target_id);
|
|
3881
3890
|
if (update.unknown_signature !== undefined)
|
|
3882
3891
|
v.unknownSignature = update.unknown_signature;
|
|
3883
3892
|
if (update.active_scan !== undefined)
|
|
@@ -4218,6 +4227,9 @@ function normalizeReconnect(opt) {
|
|
|
4218
4227
|
maxDelayMs: o.maxDelayMs ?? 30000
|
|
4219
4228
|
};
|
|
4220
4229
|
}
|
|
4230
|
+
function isTerminalClose(err) {
|
|
4231
|
+
return err.code === CLOSE_CODE.SESSION_REPLACED || err.code === CLOSE_CODE.AUTH_TIMEOUT;
|
|
4232
|
+
}
|
|
4221
4233
|
function retryAfterMs(err) {
|
|
4222
4234
|
const retryAfter = err.details?.retry_after;
|
|
4223
4235
|
const fromDetails = typeof retryAfter === "number" ? retryAfter : undefined;
|
|
@@ -4262,6 +4274,7 @@ class Account {
|
|
|
4262
4274
|
stateListeners = new Set;
|
|
4263
4275
|
userClosing = false;
|
|
4264
4276
|
reconnecting = false;
|
|
4277
|
+
attemptClose = null;
|
|
4265
4278
|
mutationLane = Promise.resolve();
|
|
4266
4279
|
marketSubscribedState = false;
|
|
4267
4280
|
subscribedMarketBaseId;
|
|
@@ -4579,9 +4592,39 @@ class Account {
|
|
|
4579
4592
|
...p.ship_name !== undefined && { ship_name: p.ship_name },
|
|
4580
4593
|
...p.username !== undefined && { username: p.username }
|
|
4581
4594
|
}));
|
|
4595
|
+
const nearbyPirates = [...view.pirates.values()].map((p) => ({
|
|
4596
|
+
pirate_id: p.pirate_id,
|
|
4597
|
+
name: p.name,
|
|
4598
|
+
tier: p.tier,
|
|
4599
|
+
is_boss: p.is_boss,
|
|
4600
|
+
status: p.status,
|
|
4601
|
+
hull: p.hull ?? 0,
|
|
4602
|
+
max_hull: p.max_hull ?? 0,
|
|
4603
|
+
shield: p.shield ?? 0,
|
|
4604
|
+
max_shield: p.max_shield ?? 0,
|
|
4605
|
+
...p.faction !== undefined && { faction: p.faction },
|
|
4606
|
+
...p.faction_name !== undefined && { faction_name: p.faction_name }
|
|
4607
|
+
}));
|
|
4608
|
+
const nearbyEmpireNpcs = [...view.empireNpcs.values()].map((n) => ({
|
|
4609
|
+
npc_id: n.npc_id,
|
|
4610
|
+
name: n.name,
|
|
4611
|
+
role: n.role,
|
|
4612
|
+
empire: n.empire,
|
|
4613
|
+
in_combat: n.in_combat,
|
|
4614
|
+
...n.fleet_name !== undefined && { fleet_name: n.fleet_name },
|
|
4615
|
+
...n.ship_class !== undefined && { ship_class: n.ship_class },
|
|
4616
|
+
...n.ship_name !== undefined && { ship_name: n.ship_name }
|
|
4617
|
+
}));
|
|
4582
4618
|
const changed = this.cache.patchSection("location", {
|
|
4583
4619
|
nearby_players: nearbyPlayers,
|
|
4584
|
-
nearby_player_count: view.nearby.size
|
|
4620
|
+
nearby_player_count: view.nearby.size,
|
|
4621
|
+
nearby_pirates: nearbyPirates,
|
|
4622
|
+
nearby_pirate_count: view.pirates.size,
|
|
4623
|
+
nearby_empire_npcs: nearbyEmpireNpcs,
|
|
4624
|
+
nearby_empire_npc_count: view.empireNpcs.size,
|
|
4625
|
+
nearby_prizes: [...view.prizes.values()],
|
|
4626
|
+
nearby_prize_count: view.prizes.size,
|
|
4627
|
+
unknown_signature: view.unknownSignature
|
|
4585
4628
|
});
|
|
4586
4629
|
if (changed.length)
|
|
4587
4630
|
this.stateRevision++;
|
|
@@ -4883,6 +4926,10 @@ class Account {
|
|
|
4883
4926
|
this.pendingAuth = null;
|
|
4884
4927
|
auth.onError(err);
|
|
4885
4928
|
}
|
|
4929
|
+
if (this.reconnecting) {
|
|
4930
|
+
this.attemptClose = err;
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4886
4933
|
if (this.shouldReconnect(err)) {
|
|
4887
4934
|
this.reconnectLoop(err);
|
|
4888
4935
|
} else if (!this.userClosing) {
|
|
@@ -4894,9 +4941,7 @@ class Account {
|
|
|
4894
4941
|
return false;
|
|
4895
4942
|
if (!this.credentialsProvider)
|
|
4896
4943
|
return false;
|
|
4897
|
-
|
|
4898
|
-
return false;
|
|
4899
|
-
return true;
|
|
4944
|
+
return !isTerminalClose(err);
|
|
4900
4945
|
}
|
|
4901
4946
|
async reconnectLoop(err) {
|
|
4902
4947
|
if (this.reconnecting || !this.reconnectConfig || !this.credentialsProvider)
|
|
@@ -4905,6 +4950,10 @@ class Account {
|
|
|
4905
4950
|
const cfg = this.reconnectConfig;
|
|
4906
4951
|
const retryAfterMs = retryAfterMsFromClose(err);
|
|
4907
4952
|
for (let attempt = 1;attempt <= cfg.maxRetries; attempt++) {
|
|
4953
|
+
if (this.userClosing) {
|
|
4954
|
+
this.reconnecting = false;
|
|
4955
|
+
return;
|
|
4956
|
+
}
|
|
4908
4957
|
notifyListeners(this.reconnectingListeners, "onReconnecting", attempt);
|
|
4909
4958
|
const backoff = attempt === 1 && retryAfterMs !== undefined ? retryAfterMs : Math.min(cfg.baseDelayMs * 2 ** (attempt - 1), cfg.maxDelayMs);
|
|
4910
4959
|
await delay(backoff);
|
|
@@ -4912,15 +4961,22 @@ class Account {
|
|
|
4912
4961
|
this.reconnecting = false;
|
|
4913
4962
|
return;
|
|
4914
4963
|
}
|
|
4964
|
+
this.attemptClose = null;
|
|
4915
4965
|
try {
|
|
4916
4966
|
await this.reconnectOnce();
|
|
4917
|
-
this.
|
|
4918
|
-
|
|
4919
|
-
|
|
4967
|
+
if (!this.attemptClose) {
|
|
4968
|
+
this.reconnecting = false;
|
|
4969
|
+
notifyListeners(this.reconnectedListeners, "onReconnected");
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
4920
4972
|
} catch {}
|
|
4973
|
+
if (this.attemptClose && isTerminalClose(this.attemptClose))
|
|
4974
|
+
break;
|
|
4921
4975
|
}
|
|
4976
|
+
const finalErr = this.attemptClose && isTerminalClose(this.attemptClose) ? this.attemptClose : err;
|
|
4922
4977
|
this.reconnecting = false;
|
|
4923
|
-
|
|
4978
|
+
if (!this.userClosing)
|
|
4979
|
+
notifyListeners(this.disconnectedListeners, "onDisconnected", finalErr);
|
|
4924
4980
|
}
|
|
4925
4981
|
async reconnectOnce() {
|
|
4926
4982
|
if (!this.credentialsProvider)
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* Local cache of the subscribed observation watch (current POI + system).
|
|
3
3
|
*
|
|
4
4
|
* Seeded from the `subscribe_observation` baseline and kept current by merging
|
|
5
|
-
* `observation_update` pushes: `*_changed` arrays upsert
|
|
6
|
-
*
|
|
5
|
+
* `observation_update` pushes: `*_changed` arrays upsert entities (keyed by
|
|
6
|
+
* their own id), `*_departed` arrays (id strings) remove them, and the
|
|
7
7
|
* cloaked-contact / signature hints are tracked alongside.
|
|
8
|
+
*
|
|
9
|
+
* Not a players-only feed: the baseline and every update also carry the
|
|
10
|
+
* pirate, empire-NPC, wildlife and intact-prize presence at the watched POI,
|
|
11
|
+
* each with the same changed/departed delta shape as players. It covers five
|
|
12
|
+
* of `get_nearby`'s six classes — arena NPCs are the one it does not carry, so
|
|
13
|
+
* a player in an arena match still has to poll `get_nearby` for their
|
|
14
|
+
* opponents. `offline_collapsed` has no equivalent here either.
|
|
8
15
|
*/
|
|
9
|
-
import type { NotificationObservationUpdate, SubscribeObservationResponse } from '../generated/openapi/types.gen.js';
|
|
16
|
+
import type { CreatureInfo, EmpireNpcInfo, NotificationObservationUpdate, PirateInfo, PrizeInfo, SubscribeObservationResponse } from '../generated/openapi/types.gen.js';
|
|
10
17
|
export type ObservedPlayer = NonNullable<NotificationObservationUpdate['nearby_changed']>[number];
|
|
11
18
|
export type CloakedContact = NonNullable<NotificationObservationUpdate['cloaked_resolved']>[number];
|
|
12
19
|
export interface ObservationView {
|
|
@@ -18,6 +25,14 @@ export interface ObservationView {
|
|
|
18
25
|
nearby: Map<string, ObservedPlayer>;
|
|
19
26
|
/** Uncloaked players system-wide, keyed by player_id. */
|
|
20
27
|
system: Map<string, ObservedPlayer>;
|
|
28
|
+
/** Pirate NPCs at the watched POI, keyed by pirate_id. */
|
|
29
|
+
pirates: Map<string, PirateInfo>;
|
|
30
|
+
/** Empire NPCs at the watched POI, keyed by npc_id. */
|
|
31
|
+
empireNpcs: Map<string, EmpireNpcInfo>;
|
|
32
|
+
/** Wildlife at the watched POI, keyed by creature_id. */
|
|
33
|
+
creatures: Map<string, CreatureInfo>;
|
|
34
|
+
/** Intact captured ships at the watched POI, keyed by prize_id. */
|
|
35
|
+
prizes: Map<string, PrizeInfo>;
|
|
21
36
|
/** Cloaked contacts resolved by an active sensor sweep, keyed by target_id. */
|
|
22
37
|
cloaked: Map<string, CloakedContact>;
|
|
23
38
|
/** A faint cloaked-ship signature is present at the watched POI. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spacemolt/lib",
|
|
3
|
-
"version": "14.1
|
|
3
|
+
"version": "14.2.1",
|
|
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",
|