screeps-connectivity 0.2.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/CHANGELOG.md +40 -0
- package/eslint.config.js +54 -0
- package/package.json +45 -0
- package/src/ScreepsClient.ts +172 -0
- package/src/badge/colors.ts +83 -0
- package/src/badge/generateSvg.ts +70 -0
- package/src/badge/index.ts +1 -0
- package/src/badge/paths.ts +385 -0
- package/src/cache/Cache.ts +71 -0
- package/src/cache/Map2Storage.ts +112 -0
- package/src/file-storage.ts +2 -0
- package/src/http/HttpClient.ts +160 -0
- package/src/http/auth/AuthStrategy.ts +5 -0
- package/src/http/auth/GuestAuth.ts +13 -0
- package/src/http/auth/PasswordAuth.ts +17 -0
- package/src/http/auth/SteamTicketAuth.ts +17 -0
- package/src/http/auth/TokenAuth.ts +14 -0
- package/src/http/decompress.ts +37 -0
- package/src/http/endpoints/auth.ts +23 -0
- package/src/http/endpoints/experimental.ts +13 -0
- package/src/http/endpoints/game.ts +103 -0
- package/src/http/endpoints/leaderboard.ts +16 -0
- package/src/http/endpoints/power-creeps.ts +24 -0
- package/src/http/endpoints/register.ts +19 -0
- package/src/http/endpoints/user-messages.ts +20 -0
- package/src/http/endpoints/user.ts +95 -0
- package/src/http/fetchServerVersion.ts +151 -0
- package/src/index.ts +55 -0
- package/src/logger.ts +25 -0
- package/src/socket/MessageParser.ts +44 -0
- package/src/socket/SocketClient.ts +203 -0
- package/src/storage/FileStorage.ts +44 -0
- package/src/storage/IndexedDBStorage.ts +77 -0
- package/src/storage/NullStorage.ts +8 -0
- package/src/storage/StorageAdapter.ts +6 -0
- package/src/stores/MapStatsStore.ts +115 -0
- package/src/stores/MapStore.ts +254 -0
- package/src/stores/NavigationStore.ts +61 -0
- package/src/stores/RoomStore.ts +264 -0
- package/src/stores/ServerStore.ts +128 -0
- package/src/stores/TypedStore.ts +31 -0
- package/src/stores/UserStore.ts +189 -0
- package/src/subscription/index.ts +18 -0
- package/src/types/api.ts +252 -0
- package/src/types/events.ts +72 -0
- package/src/types/game.ts +160 -0
- package/tests/.gitkeep +0 -0
- package/tests/ScreepsClient.test.ts +229 -0
- package/tests/badge/generateSvg.test.ts +174 -0
- package/tests/cache/Cache.test.ts +99 -0
- package/tests/cache/Map2Storage.test.ts +130 -0
- package/tests/http/HttpClient.test.ts +188 -0
- package/tests/http/decompress.test.ts +52 -0
- package/tests/http/endpoints/auth.test.ts +126 -0
- package/tests/http/endpoints/game.test.ts +210 -0
- package/tests/http/endpoints/power-creeps.test.ts +81 -0
- package/tests/http/endpoints/user-messages.test.ts +68 -0
- package/tests/http/endpoints/user.test.ts +139 -0
- package/tests/socket/MessageParser.test.ts +55 -0
- package/tests/socket/SocketClient.test.ts +144 -0
- package/tests/storage/FileStorage.test.ts +64 -0
- package/tests/storage/IndexedDBStorage.test.ts +36 -0
- package/tests/storage/NullStorage.test.ts +24 -0
- package/tests/stores/MapStatsStore.test.ts +234 -0
- package/tests/stores/MapStore.test.ts +537 -0
- package/tests/stores/NavigationStore.test.ts +166 -0
- package/tests/stores/RoomStore.test.ts +130 -0
- package/tests/stores/ServerStore.test.ts +48 -0
- package/tests/stores/TypedStore.test.ts +54 -0
- package/tests/stores/UserStore.test.ts +136 -0
- package/tests/subscription/SubscriptionGroup.test.ts +34 -0
- package/tests/types/game.test.ts +42 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { RoomStore } from '../../src/stores/RoomStore.js'
|
|
3
|
+
import { Cache } from '../../src/cache/Cache.js'
|
|
4
|
+
import { TerrainType } from '../../src/types/game.js'
|
|
5
|
+
|
|
6
|
+
function makeStore() {
|
|
7
|
+
const http = {
|
|
8
|
+
game: {
|
|
9
|
+
roomTerrain: vi.fn().mockResolvedValue({
|
|
10
|
+
ok: 1,
|
|
11
|
+
terrain: [{ _id: 'id', room: 'W7N7', terrain: '0'.repeat(2500), type: 'terrain' }],
|
|
12
|
+
}),
|
|
13
|
+
roomObjects: vi.fn().mockResolvedValue({ ok: 1, objects: [], users: {} }),
|
|
14
|
+
},
|
|
15
|
+
} as unknown as import('../../src/http/HttpClient.js').HttpClient
|
|
16
|
+
|
|
17
|
+
const socket = {
|
|
18
|
+
subscribe: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
|
19
|
+
on: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
|
20
|
+
} as unknown as import('../../src/socket/SocketClient.js').SocketClient
|
|
21
|
+
|
|
22
|
+
const cache = new Cache('test', null)
|
|
23
|
+
const store = new RoomStore(http, socket, cache)
|
|
24
|
+
return { store, http, socket, cache }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('RoomStore', () => {
|
|
28
|
+
it('fetches terrain from API on first call', async () => {
|
|
29
|
+
const { store, http } = makeStore()
|
|
30
|
+
const terrain = await store.terrain('W7N7', 'shard0')
|
|
31
|
+
expect(http.game.roomTerrain).toHaveBeenCalledWith('W7N7', 'shard0')
|
|
32
|
+
expect(terrain.get(0, 0)).toBe(TerrainType.Plain)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns cached terrain on second call', async () => {
|
|
36
|
+
const { store, http } = makeStore()
|
|
37
|
+
await store.terrain('W7N7', 'shard0')
|
|
38
|
+
await store.terrain('W7N7', 'shard0')
|
|
39
|
+
expect(http.game.roomTerrain).toHaveBeenCalledOnce()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('objects() returns null before any subscription updates', () => {
|
|
43
|
+
const { store } = makeStore()
|
|
44
|
+
expect(store.objects('W7N7', 'shard0')).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('subscribe() calls socket.subscribe with the correct channel', () => {
|
|
48
|
+
const { store, socket } = makeStore()
|
|
49
|
+
store.subscribe('W7N7', 'shard0')
|
|
50
|
+
expect(socket.subscribe).toHaveBeenCalledWith('room:shard0/W7N7')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('subscribe() omits shard prefix when shard is null (private server)', () => {
|
|
54
|
+
const { store, socket } = makeStore()
|
|
55
|
+
store.subscribe('E9N3', null)
|
|
56
|
+
expect(socket.subscribe).toHaveBeenCalledWith('room:E9N3')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('subscribe() returns a Subscription with dispose()', () => {
|
|
60
|
+
const { store } = makeStore()
|
|
61
|
+
const sub = store.subscribe('W7N7', 'shard0')
|
|
62
|
+
expect(typeof sub.dispose).toBe('function')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('merges room object diff on WS updates', async () => {
|
|
66
|
+
const { store, socket } = makeStore()
|
|
67
|
+
let messageHandler: (data: unknown) => void = () => {}
|
|
68
|
+
;(socket.on as ReturnType<typeof vi.fn>).mockImplementation((_ch: string, cb: (data: unknown) => void) => {
|
|
69
|
+
messageHandler = cb
|
|
70
|
+
return { dispose: vi.fn() }
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
store.subscribe('W7N7', 'shard0')
|
|
74
|
+
|
|
75
|
+
// First message: full state
|
|
76
|
+
messageHandler({
|
|
77
|
+
objects: { id1: { _id: 'id1', type: 'creep', room: 'W7N7', x: 10, y: 10 } },
|
|
78
|
+
gameTime: 1000,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(store.objects('W7N7', 'shard0')).toMatchObject({
|
|
82
|
+
id1: { _id: 'id1', type: 'creep' },
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Second message: diff
|
|
86
|
+
messageHandler({
|
|
87
|
+
objects: { id1: { x: 11, y: 11 } },
|
|
88
|
+
gameTime: 1001,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(store.objects('W7N7', 'shard0')?.['id1']).toMatchObject({ x: 11, y: 11, type: 'creep' })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('emits room:update event on WS message', async () => {
|
|
95
|
+
const { store, socket } = makeStore()
|
|
96
|
+
let messageHandler: (data: unknown) => void = () => {}
|
|
97
|
+
;(socket.on as ReturnType<typeof vi.fn>).mockImplementation((_ch: string, cb: (data: unknown) => void) => {
|
|
98
|
+
messageHandler = cb
|
|
99
|
+
return { dispose: vi.fn() }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const handler = vi.fn()
|
|
103
|
+
store.on('room:update', handler)
|
|
104
|
+
store.subscribe('W7N7', 'shard0')
|
|
105
|
+
messageHandler({ objects: {}, gameTime: 2000 })
|
|
106
|
+
|
|
107
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ room: 'W7N7', shard: 'shard0', gameTime: 2000 }))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('fetchObjects loads objects via HTTP and emits room:update', async () => {
|
|
111
|
+
const { store, http } = makeStore()
|
|
112
|
+
const mockObjects = [
|
|
113
|
+
{ _id: 'o1', type: 'controller', room: 'E9N3', x: 25, y: 25 },
|
|
114
|
+
{ _id: 'o2', type: 'source', room: 'E9N3', x: 10, y: 10 },
|
|
115
|
+
]
|
|
116
|
+
;(http.game as unknown as { roomObjects: ReturnType<typeof vi.fn> }).roomObjects = vi.fn().mockResolvedValue({ ok: 1, objects: mockObjects, users: {} })
|
|
117
|
+
|
|
118
|
+
const eventSpy = vi.fn()
|
|
119
|
+
store.on('room:update', eventSpy)
|
|
120
|
+
|
|
121
|
+
await store.fetchObjects('E9N3', 'shard0')
|
|
122
|
+
|
|
123
|
+
expect(http.game.roomObjects).toHaveBeenCalledWith('E9N3', 'shard0')
|
|
124
|
+
expect(eventSpy).toHaveBeenCalledOnce()
|
|
125
|
+
const update = eventSpy.mock.calls[0][0] as { objects: Record<string, unknown> }
|
|
126
|
+
expect(update.objects).toHaveProperty('o1')
|
|
127
|
+
expect(update.objects).toHaveProperty('o2')
|
|
128
|
+
expect(update.objects.o1).toEqual(expect.objectContaining({ type: 'controller' }))
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { ServerStore } from '../../src/stores/ServerStore.js'
|
|
3
|
+
import { Cache } from '../../src/cache/Cache.js'
|
|
4
|
+
import type { ApiVersionResponse } from '../../src/types/api.js'
|
|
5
|
+
|
|
6
|
+
const mockVersion: ApiVersionResponse = { ok: 1, package: 5, protocol: 13, users: 100, serverData: { historyChunkSize: 20, features: [], shards: ['shard0'] } }
|
|
7
|
+
|
|
8
|
+
function makeStore() {
|
|
9
|
+
const http = {
|
|
10
|
+
request: vi.fn().mockResolvedValue({ ...mockVersion }),
|
|
11
|
+
} as unknown as import('../../src/http/HttpClient.js').HttpClient
|
|
12
|
+
|
|
13
|
+
const socket = {
|
|
14
|
+
on: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
|
15
|
+
} as unknown as import('../../src/socket/SocketClient.js').SocketClient
|
|
16
|
+
|
|
17
|
+
return { store: new ServerStore(http, socket, new Cache('test', null)), http, socket }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('ServerStore', () => {
|
|
21
|
+
it('fetches server version', async () => {
|
|
22
|
+
const { store } = makeStore()
|
|
23
|
+
const v = await store.version()
|
|
24
|
+
expect(v.protocol).toBe(13)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('caches version after first fetch', async () => {
|
|
28
|
+
const { store, http } = makeStore()
|
|
29
|
+
await store.version()
|
|
30
|
+
await store.version()
|
|
31
|
+
expect(http.request).toHaveBeenCalledOnce()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('emits server:connected when socket fires connected event', () => {
|
|
35
|
+
const { socket } = makeStore()
|
|
36
|
+
let connectedCb: (data: unknown) => void = () => {}
|
|
37
|
+
;(socket.on as ReturnType<typeof vi.fn>).mockImplementation((ch: string, cb: (data: unknown) => void) => {
|
|
38
|
+
if (ch === 'connected') connectedCb = cb
|
|
39
|
+
return { dispose: vi.fn() }
|
|
40
|
+
})
|
|
41
|
+
// Re-create store to trigger the socket.on wiring
|
|
42
|
+
const store2 = new ServerStore(socket as unknown as import('../../src/http/HttpClient.js').HttpClient, socket, new Cache('t', null))
|
|
43
|
+
const spy = vi.fn()
|
|
44
|
+
store2.on('server:connected', spy)
|
|
45
|
+
connectedCb({})
|
|
46
|
+
expect(spy).toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { TypedStore } from '../../src/stores/TypedStore.js'
|
|
3
|
+
|
|
4
|
+
interface TestEvents {
|
|
5
|
+
'test:event': { value: number }
|
|
6
|
+
'test:other': { name: string }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('TypedStore', () => {
|
|
10
|
+
it('delivers typed event detail to listener', () => {
|
|
11
|
+
const store = new TypedStore<TestEvents>()
|
|
12
|
+
const handler = vi.fn()
|
|
13
|
+
store.on('test:event', handler)
|
|
14
|
+
store.emit('test:event', { value: 42 })
|
|
15
|
+
expect(handler).toHaveBeenCalledWith({ value: 42 })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('dispose() removes the listener', () => {
|
|
19
|
+
const store = new TypedStore<TestEvents>()
|
|
20
|
+
const handler = vi.fn()
|
|
21
|
+
const sub = store.on('test:event', handler)
|
|
22
|
+
sub.dispose()
|
|
23
|
+
store.emit('test:event', { value: 1 })
|
|
24
|
+
expect(handler).not.toHaveBeenCalled()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('multiple listeners on the same event all fire', () => {
|
|
28
|
+
const store = new TypedStore<TestEvents>()
|
|
29
|
+
const h1 = vi.fn()
|
|
30
|
+
const h2 = vi.fn()
|
|
31
|
+
store.on('test:event', h1)
|
|
32
|
+
store.on('test:event', h2)
|
|
33
|
+
store.emit('test:event', { value: 7 })
|
|
34
|
+
expect(h1).toHaveBeenCalledOnce()
|
|
35
|
+
expect(h2).toHaveBeenCalledOnce()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('listeners on different events do not cross-fire', () => {
|
|
39
|
+
const store = new TypedStore<TestEvents>()
|
|
40
|
+
const h1 = vi.fn()
|
|
41
|
+
const h2 = vi.fn()
|
|
42
|
+
store.on('test:event', h1)
|
|
43
|
+
store.on('test:other', h2)
|
|
44
|
+
store.emit('test:event', { value: 1 })
|
|
45
|
+
expect(h1).toHaveBeenCalledOnce()
|
|
46
|
+
expect(h2).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('on() returns a Subscription compatible with SubscriptionGroup', () => {
|
|
50
|
+
const store = new TypedStore<TestEvents>()
|
|
51
|
+
const sub = store.on('test:event', vi.fn())
|
|
52
|
+
expect(typeof sub.dispose).toBe('function')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { UserStore } from '../../src/stores/UserStore.js'
|
|
3
|
+
import { Cache } from '../../src/cache/Cache.js'
|
|
4
|
+
import type { UserInfo } from '../../src/types/game.js'
|
|
5
|
+
|
|
6
|
+
const mockUser: UserInfo = { _id: 'uid1', username: 'user', email: 'a@b.com', cpu: 20, gcl: 100, credits: 50, badge: { type: 1, color1: '#fff', color2: '#000', color3: '#f00', param: 0, flip: false } }
|
|
7
|
+
|
|
8
|
+
function makeStore() {
|
|
9
|
+
const http = {
|
|
10
|
+
auth: { me: vi.fn().mockResolvedValue({ ...mockUser, ok: 1 }) },
|
|
11
|
+
user: { worldStatus: vi.fn().mockResolvedValue({ ok: 1, status: 'normal' }) },
|
|
12
|
+
} as unknown as import('../../src/http/HttpClient.js').HttpClient
|
|
13
|
+
|
|
14
|
+
const socket = {
|
|
15
|
+
subscribe: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
|
16
|
+
on: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
|
17
|
+
} as unknown as import('../../src/socket/SocketClient.js').SocketClient
|
|
18
|
+
|
|
19
|
+
const cache = new Cache('test', null)
|
|
20
|
+
return { store: new UserStore(http, socket, cache), http, socket }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('UserStore', () => {
|
|
24
|
+
it('fetches user info from API', async () => {
|
|
25
|
+
const { store } = makeStore()
|
|
26
|
+
const user = await store.me()
|
|
27
|
+
expect(user.username).toBe('user')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('fetches world status from API and emits event', async () => {
|
|
31
|
+
const { store, http } = makeStore()
|
|
32
|
+
const events: Array<{ status: string }> = []
|
|
33
|
+
store.on('user:worldStatus', e => events.push(e))
|
|
34
|
+
|
|
35
|
+
const status = await store.worldStatus()
|
|
36
|
+
|
|
37
|
+
expect(status).toBe('normal')
|
|
38
|
+
expect(store.worldStatusValue).toBe('normal')
|
|
39
|
+
expect(http.user.worldStatus).toHaveBeenCalledOnce()
|
|
40
|
+
expect(events).toEqual([{ status: 'normal' }])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('caches world status after first fetch', async () => {
|
|
44
|
+
const { store, http } = makeStore()
|
|
45
|
+
|
|
46
|
+
await store.worldStatus()
|
|
47
|
+
await store.worldStatus()
|
|
48
|
+
|
|
49
|
+
expect(http.user.worldStatus).toHaveBeenCalledOnce()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('refreshWorldStatus() bypasses cached world status', async () => {
|
|
53
|
+
const { store, http } = makeStore()
|
|
54
|
+
|
|
55
|
+
await store.worldStatus()
|
|
56
|
+
;(http.user.worldStatus as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: 1, status: 'lost' })
|
|
57
|
+
const status = await store.refreshWorldStatus()
|
|
58
|
+
|
|
59
|
+
expect(status).toBe('lost')
|
|
60
|
+
expect(http.user.worldStatus).toHaveBeenCalledTimes(2)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('caches user info after first fetch', async () => {
|
|
64
|
+
const { store, http } = makeStore()
|
|
65
|
+
await store.me()
|
|
66
|
+
await store.me()
|
|
67
|
+
expect(http.auth.me).toHaveBeenCalledOnce()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('deduplicates concurrent me() calls', async () => {
|
|
71
|
+
const { store, http } = makeStore()
|
|
72
|
+
let resolveMe: (value: unknown) => void = () => {}
|
|
73
|
+
;(http.auth.me as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => new Promise(r => { resolveMe = r }))
|
|
74
|
+
|
|
75
|
+
const p1 = store.me()
|
|
76
|
+
const p2 = store.me()
|
|
77
|
+
const p3 = store.me()
|
|
78
|
+
|
|
79
|
+
resolveMe({ ...mockUser, ok: 1 })
|
|
80
|
+
|
|
81
|
+
const [u1, u2, u3] = await Promise.all([p1, p2, p3])
|
|
82
|
+
expect(u1).toEqual(u2)
|
|
83
|
+
expect(u2).toEqual(u3)
|
|
84
|
+
expect(http.auth.me).toHaveBeenCalledOnce()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('subscribe cpu starts WS subscription with userId prefix', async () => {
|
|
88
|
+
const { store, socket } = makeStore()
|
|
89
|
+
await store.me() // preload user id
|
|
90
|
+
store.subscribe('cpu')
|
|
91
|
+
await new Promise(r => setTimeout(r, 0)) // let async userId lookup settle
|
|
92
|
+
expect(socket.subscribe).toHaveBeenCalledWith('user:uid1/cpu')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('cpu stats are updated via WS and event fired', async () => {
|
|
96
|
+
const { store, socket } = makeStore()
|
|
97
|
+
await store.me()
|
|
98
|
+
let handler: (data: unknown) => void = () => {}
|
|
99
|
+
;(socket.on as ReturnType<typeof vi.fn>).mockImplementation((_ch: string, cb: (data: unknown) => void) => {
|
|
100
|
+
handler = cb
|
|
101
|
+
return { dispose: vi.fn() }
|
|
102
|
+
})
|
|
103
|
+
const eventSpy = vi.fn()
|
|
104
|
+
store.on('user:cpu', eventSpy)
|
|
105
|
+
store.subscribe('cpu')
|
|
106
|
+
await new Promise(r => setTimeout(r, 0))
|
|
107
|
+
handler({ cpu: 42, memory: 1024 })
|
|
108
|
+
expect(store.cpu).toEqual({ cpu: 42, memory: 1024 })
|
|
109
|
+
expect(eventSpy).toHaveBeenCalledWith({ cpu: 42, memory: 1024 })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('console messages accumulate and emit event', async () => {
|
|
113
|
+
const { store, socket } = makeStore()
|
|
114
|
+
await store.me()
|
|
115
|
+
let handler: (data: unknown) => void = () => {}
|
|
116
|
+
;(socket.on as ReturnType<typeof vi.fn>).mockImplementation((_ch: string, cb: (data: unknown) => void) => {
|
|
117
|
+
handler = cb
|
|
118
|
+
return { dispose: vi.fn() }
|
|
119
|
+
})
|
|
120
|
+
store.subscribe('console')
|
|
121
|
+
await new Promise(r => setTimeout(r, 0))
|
|
122
|
+
handler({ messages: { log: ['line1'], results: [] } })
|
|
123
|
+
expect(store.console).toHaveLength(1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('dispose() stops WS subscription', async () => {
|
|
127
|
+
const { store, socket } = makeStore()
|
|
128
|
+
await store.me()
|
|
129
|
+
const mockDispose = vi.fn()
|
|
130
|
+
;(socket.subscribe as ReturnType<typeof vi.fn>).mockReturnValue({ dispose: mockDispose })
|
|
131
|
+
const sub = store.subscribe('cpu')
|
|
132
|
+
await new Promise(r => setTimeout(r, 0))
|
|
133
|
+
sub.dispose()
|
|
134
|
+
expect(mockDispose).toHaveBeenCalled()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { SubscriptionGroup } from '../../src/subscription/index.js'
|
|
3
|
+
|
|
4
|
+
describe('SubscriptionGroup', () => {
|
|
5
|
+
it('calls dispose on all added subscriptions', () => {
|
|
6
|
+
const group = new SubscriptionGroup()
|
|
7
|
+
const d1 = vi.fn()
|
|
8
|
+
const d2 = vi.fn()
|
|
9
|
+
group.add({ dispose: d1 })
|
|
10
|
+
group.add({ dispose: d2 })
|
|
11
|
+
group.dispose()
|
|
12
|
+
expect(d1).toHaveBeenCalledOnce()
|
|
13
|
+
expect(d2).toHaveBeenCalledOnce()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('clears internal list after dispose so second dispose is a no-op', () => {
|
|
17
|
+
const group = new SubscriptionGroup()
|
|
18
|
+
const d1 = vi.fn()
|
|
19
|
+
group.add({ dispose: d1 })
|
|
20
|
+
group.dispose()
|
|
21
|
+
group.dispose()
|
|
22
|
+
expect(d1).toHaveBeenCalledOnce()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('itself satisfies the Subscription interface', () => {
|
|
26
|
+
const outer = new SubscriptionGroup()
|
|
27
|
+
const inner = new SubscriptionGroup()
|
|
28
|
+
const d = vi.fn()
|
|
29
|
+
inner.add({ dispose: d })
|
|
30
|
+
outer.add(inner)
|
|
31
|
+
outer.dispose()
|
|
32
|
+
expect(d).toHaveBeenCalledOnce()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { RoomTerrain, TerrainType } from '../../src/types/game.js'
|
|
3
|
+
|
|
4
|
+
describe('RoomTerrain', () => {
|
|
5
|
+
it('parses plain, wall, swamp from encoded string', () => {
|
|
6
|
+
const encoded = '012' + '0'.repeat(2497)
|
|
7
|
+
const terrain = RoomTerrain.fromEncodedString(encoded)
|
|
8
|
+
expect(terrain.get(0, 0)).toBe(TerrainType.Plain)
|
|
9
|
+
expect(terrain.get(1, 0)).toBe(TerrainType.Wall)
|
|
10
|
+
expect(terrain.get(2, 0)).toBe(TerrainType.Swamp)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('normalizes value 3 to Wall', () => {
|
|
14
|
+
const encoded = '3' + '0'.repeat(2499)
|
|
15
|
+
const terrain = RoomTerrain.fromEncodedString(encoded)
|
|
16
|
+
expect(terrain.get(0, 0)).toBe(TerrainType.Wall)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('maps (x, y) to index y*50+x', () => {
|
|
20
|
+
// tile at x=0, y=1 is index 50
|
|
21
|
+
const chars = Array(2500).fill('0')
|
|
22
|
+
chars[50] = '1'
|
|
23
|
+
const terrain = RoomTerrain.fromEncodedString(chars.join(''))
|
|
24
|
+
expect(terrain.get(0, 1)).toBe(TerrainType.Wall)
|
|
25
|
+
expect(terrain.get(0, 0)).toBe(TerrainType.Plain)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('exposes raw Uint8Array of length 2500', () => {
|
|
29
|
+
const terrain = RoomTerrain.fromEncodedString('0'.repeat(2500))
|
|
30
|
+
expect(terrain.raw).toBeInstanceOf(Uint8Array)
|
|
31
|
+
expect(terrain.raw.length).toBe(2500)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('round-trips through raw bytes', () => {
|
|
35
|
+
const encoded = '012' + '0'.repeat(2497)
|
|
36
|
+
const terrain = RoomTerrain.fromEncodedString(encoded)
|
|
37
|
+
const restored = new RoomTerrain(terrain.raw)
|
|
38
|
+
expect(restored.get(0, 0)).toBe(TerrainType.Plain)
|
|
39
|
+
expect(restored.get(1, 0)).toBe(TerrainType.Wall)
|
|
40
|
+
expect(restored.get(2, 0)).toBe(TerrainType.Swamp)
|
|
41
|
+
})
|
|
42
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src",
|
|
14
|
+
"verbatimModuleSyntax": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|
package/tsup.config.ts
ADDED