screeps-connectivity 0.13.0 → 0.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 661a53f: Don't show the "Connection lost" modal after an intentional disconnect. A guest hitting Login (or any user logging out) tore the session down synchronously, but the socket's async `onclose` still fired `server:disconnected` with `willReconnect: false`, which re-raised a fatal session error over the login screen. The disconnected event now carries an `intentional` flag so the client can distinguish a user-initiated close from a genuinely lost connection.
8
+ - 9bc39f8: History viewer fixes and a couple of supporting connectivity additions:
9
+
10
+ - Render terrain when reloading directly into history mode. Terrain loading was gated behind the room-subscription effect, which is skipped in history mode, so a fresh reload into a `#tick=` URL showed no terrain. Terrain/decoration loading now runs independently of history mode.
11
+ - Open the history view at the start of the previous, fully-written chunk instead of the current tick (whose chunk isn't flushed yet), avoiding an immediate 404 + fallback round-trip.
12
+ - Suppress the red failure toast when a history chunk is missing (404). `roomHistory` requests are now `silent`, and the viewer shows an in-room "No data available for this tick" hint instead, prompting the user to pick another tick from the timeline.
13
+ - During playback, skip to the start of the next chunk when the current chunk is missing, instead of re-fetching the same non-existent file on every tick.
14
+ - Expose `serverData.historyKeepTicks` (non-official field from the xxscreeps history mod) on the version types, used to size the history timeline's replayable range (falls back to a default window when absent).
15
+ - HTTP errors thrown by `HttpClient` now carry a `status` property so callers can distinguish a 404 from other failures.
16
+ - Dev-only: proxy `/room-history` to `VITE_PROXY_TARGET` in the Vite dev server.
17
+
18
+ - 1e85697: Accept the xxscreeps `{ error: "actually, it was fine" }` sentinel (returned with status 200 by the `create-construction` route to signal success) as a successful response instead of throwing.
19
+
3
20
  ## 0.13.0
4
21
 
5
22
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screeps-connectivity",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,6 +11,12 @@ import { createRegisterEndpoints, type RegisterEndpoints } from './endpoints/reg
11
11
  import type { HttpClientEvents } from '../types/events.js'
12
12
  import type { Subscription } from '../subscription/index.js'
13
13
 
14
+ /** xxscreeps' /api/game/create-construction route returns this in an { error }
15
+ * field with status 200 to signal success. Official Screeps inserts the site
16
+ * directly and returns an id, but xxscreeps returns this fake error and relies
17
+ * on the room socket to show the site next tick. It is not a real error. */
18
+ const XXSCREEPS_FALSE_ERROR = 'actually, it was fine'
19
+
14
20
  export interface RateLimitInfo {
15
21
  limit: number
16
22
  remaining: number
@@ -133,7 +139,8 @@ export class HttpClient extends EventTarget {
133
139
  if (!res.ok && res.status !== 304) {
134
140
  let body = ''
135
141
  try { body = await res.text() } catch { /* ignore */ }
136
- const error = new Error(`HTTP ${res.status}: ${body}`)
142
+ const error = new Error(`HTTP ${res.status}: ${body}`) as Error & { status?: number }
143
+ error.status = res.status
137
144
  this.emit('http:error', { method, path, status: res.status, error, silent: opts.silent })
138
145
  throw error
139
146
  }
@@ -142,6 +149,13 @@ export class HttpClient extends EventTarget {
142
149
 
143
150
  const data = await res.json() as Record<string, unknown>
144
151
 
152
+ // xxscreeps' create-construction route returns { error: 'actually, it was
153
+ // fine' } with status 200 to signal success (see XXSCREEPS_FALSE_ERROR).
154
+ // Treat this sentinel as success, not an error.
155
+ if (data['error'] === XXSCREEPS_FALSE_ERROR) {
156
+ delete data['error']
157
+ }
158
+
145
159
  if (typeof data['error'] === 'string') {
146
160
  const error = new Error(`Screeps API error: ${data['error']}`)
147
161
  this.emit('http:error', { method, path, status: res.status, error, silent: opts.silent })
@@ -98,9 +98,11 @@ export function createGameEndpoints(http: HttpClient, decorationsMock?: ApiRoomD
98
98
  addObjectIntent: (id, room, name, intent, shard) => http.request('POST', '/api/game/add-object-intent', withShard({ _id: id, room, name, intent }, shard)),
99
99
  addGlobalIntent: (name, intent) => http.request('POST', '/api/game/add-global-intent', { name, intent }),
100
100
  roomHistory: (room, time, shard) =>
101
+ // silent: a missing chunk 404s while history is still being written; the caller
102
+ // handles that gracefully, so don't surface a global "request failed" toast.
101
103
  shard
102
- ? http.request('GET', `/room-history/${encodeURIComponent(shard)}/${encodeURIComponent(room)}/${time}.json`)
103
- : http.request('GET', '/room-history', { room, time }),
104
+ ? http.request('GET', `/room-history/${encodeURIComponent(shard)}/${encodeURIComponent(room)}/${time}.json`, undefined, { silent: true })
105
+ : http.request('GET', '/room-history', { room, time }, { silent: true }),
104
106
  setNotifyWhenAttacked: (id, enabled) => http.request('POST', '/api/game/set-notify-when-attacked', { _id: id, enabled }),
105
107
  createInvader: (room, x, y, size, type, boosted) => http.request('POST', '/api/game/create-invader', { room, x, y, size, type, ...(boosted != null ? { boosted } : {}) }),
106
108
  removeInvader: (id) => http.request('POST', '/api/game/remove-invader', { _id: id }),
@@ -79,7 +79,7 @@ export class SocketClient {
79
79
  this.authed = false
80
80
  this.authSub?.dispose()
81
81
  this.authSub = null
82
- this.emit('disconnected', { willReconnect: this.reconnecting })
82
+ this.emit('disconnected', { willReconnect: this.reconnecting, intentional: this._intentionalClose })
83
83
  void this.scheduleReconnect()
84
84
  }
85
85
  this.ws.onerror = (err) => {
@@ -29,8 +29,8 @@ export class ServerStore extends TypedStore<ServerStoreEvents> {
29
29
  this.emit('server:connected', {})
30
30
  }))
31
31
  this.socketSubs.push(socket.on('disconnected', (data) => {
32
- const d = data as { willReconnect: boolean }
33
- this.emit('server:disconnected', { willReconnect: d.willReconnect })
32
+ const d = data as { willReconnect: boolean; intentional?: boolean }
33
+ this.emit('server:disconnected', { willReconnect: d.willReconnect, intentional: d.intentional ?? false })
34
34
  }))
35
35
  this.socketSubs.push(socket.on('socket:error', (data) => {
36
36
  this.emit('server:error', { error: data instanceof Error ? data : new Error(String(data)) })
package/src/types/api.ts CHANGED
@@ -102,6 +102,13 @@ export interface ApiVersionResponse {
102
102
  users: number
103
103
  serverData: {
104
104
  historyChunkSize: number
105
+ /**
106
+ * Retention window for room history, in ticks. Non-official field added by the
107
+ * xxscreeps history mod. When present, the earliest replayable tick is roughly
108
+ * `currentTick - historyKeepTicks`; a value of `0` means history is kept forever
109
+ * (unbounded). Absent on servers without the mod.
110
+ */
111
+ historyKeepTicks?: number
105
112
  features: Array<{ name: string }>
106
113
  shards: string[]
107
114
  customObjectTypes?: unknown
@@ -47,7 +47,7 @@ export interface UserStoreEvents {
47
47
 
48
48
  export interface ServerStoreEvents {
49
49
  'server:connected': Record<string, never>
50
- 'server:disconnected': { willReconnect: boolean }
50
+ 'server:disconnected': { willReconnect: boolean; intentional: boolean }
51
51
  'server:error': { error: Error }
52
52
  'server:version': ServerVersion
53
53
  'server:shards': ShardInfo[]
package/src/types/game.ts CHANGED
@@ -145,6 +145,14 @@ export interface ServerVersion {
145
145
  users: number
146
146
  serverData: {
147
147
  historyChunkSize: number
148
+ /**
149
+ * Retention window for room history, in ticks. Non-official field added by the
150
+ * xxscreeps history mod. When present, the earliest replayable tick is roughly
151
+ * `currentTick - historyKeepTicks`; a value of `0` means history is kept forever
152
+ * (unbounded). Absent on servers without the mod — callers should fall back to a
153
+ * sensible default in that case.
154
+ */
155
+ historyKeepTicks?: number
148
156
  features: ServerFeature[]
149
157
  shards: Array<string | null>
150
158
  welcomeText?: string
@@ -205,6 +205,16 @@ describe('HttpClient', () => {
205
205
  await expect(http.request('GET', '/api/game/create-flag')).rejects.toThrow('Screeps API error: invalid params')
206
206
  })
207
207
 
208
+ it('treats the xxscreeps "actually, it was fine" sentinel as success', async () => {
209
+ fetchMock.mockResolvedValue(mockResponse({ error: 'actually, it was fine' }))
210
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
211
+ const errorHandler = vi.fn()
212
+ http.on('http:error', errorHandler)
213
+ const res = await http.request<{ error?: string }>('POST', '/api/game/create-construction')
214
+ expect(res.error).toBeUndefined()
215
+ expect(errorHandler).not.toHaveBeenCalled()
216
+ })
217
+
208
218
  it('emits http:error on 200 with error in response body', async () => {
209
219
  fetchMock.mockResolvedValue(mockResponse({ ok: 0, error: 'flags limit exceeded' }))
210
220
  const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
@@ -141,4 +141,15 @@ describe('SocketClient', () => {
141
141
  client.disconnect()
142
142
  expect(willReconnect).toBe(false)
143
143
  })
144
+
145
+ it('disconnected event is flagged intentional when disconnect() is called', async () => {
146
+ const client = makeClient()
147
+ const _ws = await connectClient(client)
148
+ let intentional: boolean | undefined
149
+ client.on('disconnected', (data) => {
150
+ intentional = (data as { intentional: boolean }).intentional
151
+ })
152
+ client.disconnect()
153
+ expect(intentional).toBe(true)
154
+ })
144
155
  })