@spacemolt/lib 13.2.0 → 14.0.0

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/index.js CHANGED
@@ -1,3 +1,22 @@
1
+ // src/generated/frames.gen.ts
2
+ var WELCOME_PAYLOAD_FIELDS = {
3
+ current_tick: { kind: "number", required: true },
4
+ game_info: { kind: "string", required: true },
5
+ help_text: { kind: "string", required: true },
6
+ motd: { kind: "string", required: false },
7
+ release_date: { kind: "string", required: true },
8
+ release_notes: { kind: "string[]", required: true },
9
+ server_time: { kind: "number", required: true },
10
+ terms: { kind: "string", required: true },
11
+ tick_rate: { kind: "number", required: true },
12
+ version: { kind: "string", required: true },
13
+ website: { kind: "string", required: true }
14
+ };
15
+ var REGISTERED_PAYLOAD_FIELDS = {
16
+ password: { kind: "string", required: true },
17
+ player_id: { kind: "string", required: true }
18
+ };
19
+
1
20
  // src/validation.ts
2
21
  function isRecord(value) {
3
22
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -24,14 +43,28 @@ function isActionErrorFrame(frame) {
24
43
  function isErrorFrame(frame) {
25
44
  return frame.type === "error" && hasPayload(frame) && typeof frame.payload.code === "string" && typeof frame.payload.message === "string" && (frame.payload.details === undefined || isRecord(frame.payload.details)) && (frame.payload.pending_command === undefined || typeof frame.payload.pending_command === "string");
26
45
  }
46
+ function matchesKind(value, kind) {
47
+ const element = kind.endsWith("[]") ? kind.slice(0, -2) : undefined;
48
+ if (element)
49
+ return Array.isArray(value) && value.every((item) => typeof item === element);
50
+ return typeof value === kind;
51
+ }
52
+ function matchesFields(payload, fields) {
53
+ return Object.entries(fields).every(([field, { kind, required }]) => {
54
+ const value = payload[field];
55
+ if (value === undefined)
56
+ return !required;
57
+ return matchesKind(value, kind);
58
+ });
59
+ }
27
60
  function isWelcomeFrame(frame) {
28
- return frame.type === "welcome" && hasPayload(frame) && typeof frame.payload.version === "string" && typeof frame.payload.release_date === "string" && Array.isArray(frame.payload.release_notes) && frame.payload.release_notes.every((note) => typeof note === "string") && typeof frame.payload.tick_rate === "number" && typeof frame.payload.current_tick === "number" && typeof frame.payload.server_time === "number" && (frame.payload.motd === undefined || typeof frame.payload.motd === "string") && typeof frame.payload.game_info === "string" && typeof frame.payload.website === "string" && typeof frame.payload.help_text === "string" && typeof frame.payload.terms === "string";
61
+ return frame.type === "welcome" && hasPayload(frame) && matchesFields(frame.payload, WELCOME_PAYLOAD_FIELDS);
29
62
  }
30
63
  function isLoggedInFrame(frame) {
31
64
  return frame.type === "logged_in" && hasPayload(frame);
32
65
  }
33
66
  function isRegisteredFrame(frame) {
34
- return frame.type === "registered" && hasPayload(frame) && typeof frame.payload.password === "string" && typeof frame.payload.player_id === "string";
67
+ return frame.type === "registered" && hasPayload(frame) && matchesFields(frame.payload, REGISTERED_PAYLOAD_FIELDS);
35
68
  }
36
69
  var STATE_SECTIONS = [
37
70
  "player",
@@ -45,7 +78,7 @@ var STATE_SECTIONS = [
45
78
  "prize_recoveries"
46
79
  ];
47
80
  // src/generated/actions.gen.ts
48
- var GENERATED_SPEC_VERSION = "v0.586.0";
81
+ var GENERATED_SPEC_VERSION = "v0.593.0";
49
82
  var ACTIONS = {
50
83
  "spacemolt_arena/accept": {
51
84
  tool: "spacemolt_arena",
@@ -74,6 +107,14 @@ var ACTIONS = {
74
107
  { name: "max_side_size", type: "number", required: false, description: "For 'challenge': maximum ships per side. 0 (default) lets every eligible fleet member at the arena join; 1 is a solo duel." }
75
108
  ]
76
109
  },
110
+ "spacemolt_arena/challenges": {
111
+ tool: "spacemolt_arena",
112
+ action: "challenges",
113
+ kind: "query",
114
+ summary: "Consequence-free combat at an arena POI: challenge a pilot, fight on the normal battle engine, leave with ship and crew intact",
115
+ responseType: "ArenaChallengesResponse",
116
+ params: []
117
+ },
77
118
  "spacemolt_arena/decline": {
78
119
  tool: "spacemolt_arena",
79
120
  action: "decline",
@@ -82,6 +123,16 @@ var ACTIONS = {
82
123
  detailsType: "ArenaActionResponse",
83
124
  params: []
84
125
  },
126
+ "spacemolt_arena/fight": {
127
+ tool: "spacemolt_arena",
128
+ action: "fight",
129
+ kind: "mutation",
130
+ summary: "Consequence-free combat at an arena POI: challenge a pilot, fight on the normal battle engine, leave with ship and crew intact",
131
+ detailsType: "ArenaFightResponse",
132
+ params: [
133
+ { name: "id", type: "string", required: true, description: "For 'fight': the NPC challenge to start, from 'challenges'.", positional: 0 }
134
+ ]
135
+ },
85
136
  "spacemolt_arena/status": {
86
137
  tool: "spacemolt_arena",
87
138
  action: "status",
@@ -224,7 +275,7 @@ var ACTIONS = {
224
275
  summary: "Manage your battle — maneuver, target enemies, adopt combat stances, or self-destruct",
225
276
  responseType: "BattleResponse",
226
277
  params: [
227
- { name: "id", type: '"fire" | "evade" | "brace" | "flee" | "board"', required: true, description: "Battle stance: fire (100% dmg dealt/taken), evade (0%/50%, costs fuel), brace (0%/25%, shields regen 2x), flee (0%/100%, auto-retreats to escape), or board (0%/100%, automatically closes for repeated latch attempts; requires target_id and marines). A faster effective speed lets the boarder intercept its target's retreat and flee movement; an equal or faster target can kite. Changing away from board begins non-instant withdrawal.", enumValues: ["fire", "evade", "brace", "flee", "board"], positional: 0 },
278
+ { name: "id", type: '"fire" | "evade" | "brace" | "flee" | "board"', required: true, description: "Battle stance: fire (100% dmg dealt/taken), evade (0%/50%, costs fuel), brace (0%/25%, shields regen 2x), flee (0%/100%, auto-retreats to escape), or board (0%/100%, automatically closes for repeated latch attempts; requires target_id and marines). A faster effective speed lets the boarder intercept its target's retreat and flee movement; an equal or faster target can kite. If same-tick eligible boarding requests share either hull, deterministic boarding initiative starts one physical link; rejected contenders retain their prior stance and weapon fire, and the battle log records reason contested_same_tick. Changing away from board begins non-instant withdrawal.", enumValues: ["fire", "evade", "brace", "flee", "board"], positional: 0 },
228
279
  { name: "marines", type: "number", required: false, description: "Positive fit-marine commitment required when setting stance=board. The battle tick caps it to the fit marines actually available." },
229
280
  { name: "target", type: "string", required: false, description: "ID or name of the enemy — required when focusing a target and when entering the board stance. Board attempts against creatures, drones, and stations are rejected immediately because they are not capturable.", positional: 1 }
230
281
  ]
@@ -978,7 +1029,7 @@ var ACTIONS = {
978
1029
  { name: "rewards", type: "{ credits?: number; items?: Record<string, unknown>[] }", required: true, description: "Mission rewards (credits, items, reputation)" },
979
1030
  { name: "title", type: "string", required: true, description: "Mission title" },
980
1031
  { name: "triggers", type: "string[]", required: false, description: "Optional: triggers like 'open_to_all' to allow non-members" },
981
- { name: "type", type: "string", required: true, description: "Mission type (delivery, combat, exploration, etc.)" }
1032
+ { name: "type", type: "string", required: true, description: "Mission type (delivery, combat, exploration, bounty, etc.)" }
982
1033
  ]
983
1034
  },
984
1035
  "spacemolt_faction_admin/promote": {
@@ -1665,7 +1716,7 @@ var ACTIONS = {
1665
1716
  tool: "spacemolt_salvage",
1666
1717
  action: "sell",
1667
1718
  kind: "mutation",
1668
- summary: "Sell a towed wreck to the salvage yard for credits",
1719
+ summary: "Sell a towed wreck to an NPC salvage yard for credits",
1669
1720
  detailsType: "SellWreckResponse",
1670
1721
  params: []
1671
1722
  },
@@ -2540,7 +2591,7 @@ var ACTIONS = {
2540
2591
  summary: "Attack another player, pirate, empire NPC, creature, station, or intact prize",
2541
2592
  detailsType: "AttackResponse",
2542
2593
  params: [
2543
- { name: "id", type: "string", required: true, description: "ID of the target: a player, pirate, empire NPC, wildlife creature, intact-prize actor, or station. Prize actor IDs come from get_nearby. Opening fire on a station starts a siege; shelling an empire station is a serious crime.", positional: 0 }
2594
+ { name: "id", type: "string", required: true, description: "ID of the target: a player, pirate, empire NPC, wildlife creature, intact-prize actor, or station. Prize actor IDs come from get_nearby. Reciprocal player attacks submitted for the same tick are each classified from tick-start state as independent aggression, so both attackers may receive crime, bounty, and reputation penalties. Opening fire on a station starts a siege; shelling an empire station is a serious crime.", positional: 0 }
2544
2595
  ]
2545
2596
  },
2546
2597
  "spacemolt/buy": {
@@ -3211,7 +3262,9 @@ function buildCommands(dispatch) {
3211
3262
  accept: bindBare("spacemolt_arena", "accept"),
3212
3263
  cancel: bindBare("spacemolt_arena", "cancel"),
3213
3264
  challenge: bind("spacemolt_arena", "challenge"),
3265
+ challenges: bindBare("spacemolt_arena", "challenges"),
3214
3266
  decline: bindBare("spacemolt_arena", "decline"),
3267
+ fight: bind("spacemolt_arena", "fight"),
3215
3268
  status: bindBare("spacemolt_arena", "status")
3216
3269
  },
3217
3270
  spacemolt_auth: {
@@ -3609,6 +3662,16 @@ function retryAfterMsFromClose(err) {
3609
3662
  }
3610
3663
 
3611
3664
  // src/events/emitter.ts
3665
+ function notifyListeners(listeners, label, ...args) {
3666
+ for (const listener of listeners) {
3667
+ try {
3668
+ listener(...args);
3669
+ } catch (err) {
3670
+ console.warn(`[spacemolt] ${label} listener threw: ${err}`);
3671
+ }
3672
+ }
3673
+ }
3674
+
3612
3675
  class EventStream {
3613
3676
  onClose;
3614
3677
  buffer = [];
@@ -3903,14 +3966,17 @@ class StateCache {
3903
3966
  function extractPendingAck(structured) {
3904
3967
  if (!structured)
3905
3968
  return;
3906
- if (structured.pending === true) {
3907
- return { command: String(structured.command ?? ""), message: String(structured.message ?? "") };
3908
- }
3909
3969
  const details = structured.details;
3910
- if (isRecord(details) && details.pending === true) {
3911
- return { command: String(details.command ?? ""), message: String(details.message ?? "") };
3912
- }
3913
- return;
3970
+ const source = structured.pending === true ? structured : isRecord(details) && details.pending === true ? details : undefined;
3971
+ if (!source)
3972
+ return;
3973
+ return {
3974
+ pending: true,
3975
+ command: String(source.command ?? ""),
3976
+ message: String(source.message ?? ""),
3977
+ ...typeof source.auto_docked === "boolean" && { auto_docked: source.auto_docked },
3978
+ ...typeof source.auto_undocked === "boolean" && { auto_undocked: source.auto_undocked }
3979
+ };
3914
3980
  }
3915
3981
 
3916
3982
  class Correlator {
@@ -3944,14 +4010,14 @@ class Correlator {
3944
4010
  return this.rejectInvalidResponse(requestId, pending, frame.type);
3945
4011
  const payload = frame.payload;
3946
4012
  if (pending.kind === "query") {
3947
- this.settle(requestId);
4013
+ this.pending.delete(requestId);
3948
4014
  pending.resolve({ result: payload.result, structuredContent: payload.structuredContent });
3949
4015
  return true;
3950
4016
  }
3951
4017
  const structured = payload.structuredContent;
3952
4018
  const ack = extractPendingAck(structured);
3953
4019
  if (!ack) {
3954
- this.settle(requestId);
4020
+ this.pending.delete(requestId);
3955
4021
  pending.resolve({
3956
4022
  command: String(structured?.command ?? ""),
3957
4023
  tick: 0,
@@ -3969,7 +4035,7 @@ class Correlator {
3969
4035
  if (!isActionResultFrame(frame))
3970
4036
  return this.rejectInvalidResponse(requestId, pending, frame.type);
3971
4037
  const payload = frame.payload;
3972
- this.settle(requestId);
4038
+ this.pending.delete(requestId);
3973
4039
  pending.resolve({
3974
4040
  command: payload.command,
3975
4041
  tick: payload.tick,
@@ -3982,14 +4048,14 @@ class Correlator {
3982
4048
  case "action_error": {
3983
4049
  if (!isActionErrorFrame(frame))
3984
4050
  return this.rejectInvalidResponse(requestId, pending, frame.type);
3985
- this.settle(requestId);
4051
+ this.pending.delete(requestId);
3986
4052
  pending.reject(errorFromActionFrame(frame));
3987
4053
  return true;
3988
4054
  }
3989
4055
  case "error": {
3990
4056
  if (!isErrorFrame(frame))
3991
4057
  return this.rejectInvalidResponse(requestId, pending, frame.type);
3992
- this.settle(requestId);
4058
+ this.pending.delete(requestId);
3993
4059
  pending.reject(errorFromFrame(frame));
3994
4060
  return true;
3995
4061
  }
@@ -4002,46 +4068,15 @@ class Correlator {
4002
4068
  pending.reject(err);
4003
4069
  this.pending.clear();
4004
4070
  }
4005
- settle(requestId) {
4006
- this.pending.delete(requestId);
4007
- }
4008
4071
  rejectInvalidResponse(requestId, pending, frameType) {
4009
- this.settle(requestId);
4072
+ this.pending.delete(requestId);
4010
4073
  pending.reject(new SpacemoltError("invalid_response", `Malformed ${frameType} frame for request ${requestId}`));
4011
4074
  return true;
4012
4075
  }
4013
4076
  }
4014
4077
 
4015
4078
  // src/transport/socket.ts
4016
- class NativeWebSocketAdapter {
4017
- socket;
4018
- constructor(url) {
4019
- this.socket = new WebSocket(url);
4020
- }
4021
- send(data) {
4022
- this.socket.send(data);
4023
- }
4024
- close(code, reason) {
4025
- this.socket.close(code, reason);
4026
- }
4027
- addEventListener(...args) {
4028
- switch (args[0]) {
4029
- case "open":
4030
- this.socket.addEventListener("open", () => args[1]());
4031
- break;
4032
- case "message":
4033
- this.socket.addEventListener("message", (event) => args[1]({ data: event.data }));
4034
- break;
4035
- case "close":
4036
- this.socket.addEventListener("close", (event) => args[1]({ code: event.code, reason: event.reason }));
4037
- break;
4038
- case "error":
4039
- this.socket.addEventListener("error", (event) => args[1](event));
4040
- break;
4041
- }
4042
- }
4043
- }
4044
- var defaultFactory = (url) => new NativeWebSocketAdapter(url);
4079
+ var defaultFactory = (url) => new WebSocket(url);
4045
4080
 
4046
4081
  class Socket {
4047
4082
  opts;
@@ -4416,17 +4451,8 @@ class Account {
4416
4451
  this.stateListeners.delete(listener);
4417
4452
  };
4418
4453
  }
4419
- notifyListeners(listeners, label, ...args) {
4420
- for (const listener of listeners) {
4421
- try {
4422
- listener(...args);
4423
- } catch (err) {
4424
- console.warn(`[spacemolt] ${label} listener threw: ${err}`);
4425
- }
4426
- }
4427
- }
4428
4454
  emitStateChange(changed) {
4429
- this.notifyListeners(this.stateListeners, "onStateChange", changed);
4455
+ notifyListeners(this.stateListeners, "onStateChange", changed);
4430
4456
  }
4431
4457
  async logout() {
4432
4458
  await this.query("spacemolt_auth", "logout");
@@ -4850,7 +4876,7 @@ class Account {
4850
4876
  if (this.shouldReconnect(err)) {
4851
4877
  this.reconnectLoop(err);
4852
4878
  } else if (!this.userClosing) {
4853
- this.notifyListeners(this.disconnectedListeners, "onDisconnected", err);
4879
+ notifyListeners(this.disconnectedListeners, "onDisconnected", err);
4854
4880
  }
4855
4881
  }
4856
4882
  shouldReconnect(err) {
@@ -4869,7 +4895,7 @@ class Account {
4869
4895
  const cfg = this.reconnectConfig;
4870
4896
  const retryAfterMs = retryAfterMsFromClose(err);
4871
4897
  for (let attempt = 1;attempt <= cfg.maxRetries; attempt++) {
4872
- this.notifyListeners(this.reconnectingListeners, "onReconnecting", attempt);
4898
+ notifyListeners(this.reconnectingListeners, "onReconnecting", attempt);
4873
4899
  const backoff = attempt === 1 && retryAfterMs !== undefined ? retryAfterMs : Math.min(cfg.baseDelayMs * 2 ** (attempt - 1), cfg.maxDelayMs);
4874
4900
  await delay(backoff);
4875
4901
  if (this.userClosing) {
@@ -4879,12 +4905,12 @@ class Account {
4879
4905
  try {
4880
4906
  await this.reconnectOnce();
4881
4907
  this.reconnecting = false;
4882
- this.notifyListeners(this.reconnectedListeners, "onReconnected");
4908
+ notifyListeners(this.reconnectedListeners, "onReconnected");
4883
4909
  return;
4884
4910
  } catch {}
4885
4911
  }
4886
4912
  this.reconnecting = false;
4887
- this.notifyListeners(this.disconnectedListeners, "onDisconnected", err);
4913
+ notifyListeners(this.disconnectedListeners, "onDisconnected", err);
4888
4914
  }
4889
4915
  async reconnectOnce() {
4890
4916
  if (!this.credentialsProvider)
@@ -5168,16 +5194,13 @@ class SpacemoltClient {
5168
5194
  return () => this.accountDisconnectedListeners.delete(listener);
5169
5195
  }
5170
5196
  notifyAccountConnected(account) {
5171
- for (const listener of this.accountConnectedListeners)
5172
- listener(account);
5197
+ notifyListeners(this.accountConnectedListeners, "onAccountConnected", account);
5173
5198
  }
5174
5199
  notifyAccountReconnected(account) {
5175
- for (const listener of this.accountReconnectedListeners)
5176
- listener(account);
5200
+ notifyListeners(this.accountReconnectedListeners, "onAccountReconnected", account);
5177
5201
  }
5178
5202
  notifyAccountDisconnected(id, err) {
5179
- for (const listener of this.accountDisconnectedListeners)
5180
- listener(id, err);
5203
+ notifyListeners(this.accountDisconnectedListeners, "onAccountDisconnected", id, err);
5181
5204
  }
5182
5205
  get httpBaseUrl() {
5183
5206
  return this.opts.httpBaseUrl ?? (this.opts.url ? httpBaseFromWs(this.opts.url) : DEFAULT_HTTP_BASE);
@@ -7,7 +7,7 @@
7
7
  * spec instead (see src/generated/). Endpoint: `/ws/v2`.
8
8
  */
9
9
  import type { NotificationPayloads, TypedNotificationType } from './generated/notifications.gen.js';
10
- import type { LoggedInPayload, RegisteredPayload, V2GameState, WelcomePayload } from './generated/openapi/types.gen.js';
10
+ import type { LoggedInPayload, NotificationActionError, NotificationActionResult, PendingActionResponse, RegisteredPayload, V2GameState, WelcomePayload } from './generated/openapi/types.gen.js';
11
11
  /** Inbound frame: client -> server. `payload` is omitted when an action takes none. */
12
12
  export interface InboundFrame {
13
13
  tool: string;
@@ -27,36 +27,25 @@ export interface ResultFrame {
27
27
  structuredContent?: Record<string, unknown>;
28
28
  };
29
29
  }
30
- /** Mutation-ack: a `result` frame whose structuredContent flags `pending: true`. */
31
- export interface PendingAck {
32
- pending: true;
33
- command: string;
34
- message: string;
35
- }
36
30
  /** Outcome push for a queued mutation; echoes the original request_id. */
37
31
  export interface ActionResultFrame {
38
32
  type: 'action_result';
39
33
  request_id?: string;
40
- payload: {
41
- command: string;
42
- tick: number;
34
+ /**
35
+ * The server's published `Notification_action_result`, except for `result`:
36
+ * the spec leaves it untyped because the v1 and v2 shapes differ, and on
37
+ * `/ws/v2` it is always the delta. Narrow it here rather than lose the type.
38
+ */
39
+ payload: Omit<NotificationActionResult, 'result'> & {
43
40
  /** A V2GameState delta — only the sections that changed. See StateDelta. */
44
41
  result: StateDelta;
45
- auto_docked?: boolean;
46
- auto_undocked?: boolean;
47
42
  };
48
43
  }
49
44
  /** Failure outcome for a queued mutation. */
50
45
  export interface ActionErrorFrame {
51
46
  type: 'action_error';
52
47
  request_id?: string;
53
- payload: {
54
- command: string;
55
- tick: number;
56
- code: string;
57
- message: string;
58
- details?: Record<string, unknown>;
59
- };
48
+ payload: NotificationActionError;
60
49
  }
61
50
  /** Generic error frame emitted by the framing layer or a handler. */
62
51
  export interface ErrorFrame {
@@ -163,11 +152,13 @@ export interface QueryResult<T = Record<string, unknown>> {
163
152
  /** Raw JSON for programmatic consumption, typed to the command's response. */
164
153
  structuredContent?: T;
165
154
  }
166
- /** The immediate `pending: true` acknowledgement for a queued mutation. */
167
- export interface MutationAck {
168
- command: string;
169
- message: string;
170
- }
155
+ /**
156
+ * The immediate `pending: true` acknowledgement for a queued mutation. The
157
+ * server's published `PendingActionResponse`, so the `auto_docked`/
158
+ * `auto_undocked` flags it carries reach the caller at ack time rather than
159
+ * only on the final outcome.
160
+ */
161
+ export type MutationAck = PendingActionResponse;
171
162
  /**
172
163
  * Resolved value of a two-phase mutation, delivered when the action executes.
173
164
  * `TDetails` is the action-specific shape of `delta.details` (e.g. `jump` ->
@@ -50,6 +50,5 @@ export declare class Correlator {
50
50
  handle(frame: RawFrame): boolean;
51
51
  /** Reject every in-flight request — used when the socket closes. */
52
52
  rejectAll(err: ConnectionClosedError | SpacemoltError): void;
53
- private settle;
54
53
  private rejectInvalidResponse;
55
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacemolt/lib",
3
- "version": "13.2.0",
3
+ "version": "14.0.0",
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",