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.
Files changed (75) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/eslint.config.js +54 -0
  3. package/package.json +45 -0
  4. package/src/ScreepsClient.ts +172 -0
  5. package/src/badge/colors.ts +83 -0
  6. package/src/badge/generateSvg.ts +70 -0
  7. package/src/badge/index.ts +1 -0
  8. package/src/badge/paths.ts +385 -0
  9. package/src/cache/Cache.ts +71 -0
  10. package/src/cache/Map2Storage.ts +112 -0
  11. package/src/file-storage.ts +2 -0
  12. package/src/http/HttpClient.ts +160 -0
  13. package/src/http/auth/AuthStrategy.ts +5 -0
  14. package/src/http/auth/GuestAuth.ts +13 -0
  15. package/src/http/auth/PasswordAuth.ts +17 -0
  16. package/src/http/auth/SteamTicketAuth.ts +17 -0
  17. package/src/http/auth/TokenAuth.ts +14 -0
  18. package/src/http/decompress.ts +37 -0
  19. package/src/http/endpoints/auth.ts +23 -0
  20. package/src/http/endpoints/experimental.ts +13 -0
  21. package/src/http/endpoints/game.ts +103 -0
  22. package/src/http/endpoints/leaderboard.ts +16 -0
  23. package/src/http/endpoints/power-creeps.ts +24 -0
  24. package/src/http/endpoints/register.ts +19 -0
  25. package/src/http/endpoints/user-messages.ts +20 -0
  26. package/src/http/endpoints/user.ts +95 -0
  27. package/src/http/fetchServerVersion.ts +151 -0
  28. package/src/index.ts +55 -0
  29. package/src/logger.ts +25 -0
  30. package/src/socket/MessageParser.ts +44 -0
  31. package/src/socket/SocketClient.ts +203 -0
  32. package/src/storage/FileStorage.ts +44 -0
  33. package/src/storage/IndexedDBStorage.ts +77 -0
  34. package/src/storage/NullStorage.ts +8 -0
  35. package/src/storage/StorageAdapter.ts +6 -0
  36. package/src/stores/MapStatsStore.ts +115 -0
  37. package/src/stores/MapStore.ts +254 -0
  38. package/src/stores/NavigationStore.ts +61 -0
  39. package/src/stores/RoomStore.ts +264 -0
  40. package/src/stores/ServerStore.ts +128 -0
  41. package/src/stores/TypedStore.ts +31 -0
  42. package/src/stores/UserStore.ts +189 -0
  43. package/src/subscription/index.ts +18 -0
  44. package/src/types/api.ts +252 -0
  45. package/src/types/events.ts +72 -0
  46. package/src/types/game.ts +160 -0
  47. package/tests/.gitkeep +0 -0
  48. package/tests/ScreepsClient.test.ts +229 -0
  49. package/tests/badge/generateSvg.test.ts +174 -0
  50. package/tests/cache/Cache.test.ts +99 -0
  51. package/tests/cache/Map2Storage.test.ts +130 -0
  52. package/tests/http/HttpClient.test.ts +188 -0
  53. package/tests/http/decompress.test.ts +52 -0
  54. package/tests/http/endpoints/auth.test.ts +126 -0
  55. package/tests/http/endpoints/game.test.ts +210 -0
  56. package/tests/http/endpoints/power-creeps.test.ts +81 -0
  57. package/tests/http/endpoints/user-messages.test.ts +68 -0
  58. package/tests/http/endpoints/user.test.ts +139 -0
  59. package/tests/socket/MessageParser.test.ts +55 -0
  60. package/tests/socket/SocketClient.test.ts +144 -0
  61. package/tests/storage/FileStorage.test.ts +64 -0
  62. package/tests/storage/IndexedDBStorage.test.ts +36 -0
  63. package/tests/storage/NullStorage.test.ts +24 -0
  64. package/tests/stores/MapStatsStore.test.ts +234 -0
  65. package/tests/stores/MapStore.test.ts +537 -0
  66. package/tests/stores/NavigationStore.test.ts +166 -0
  67. package/tests/stores/RoomStore.test.ts +130 -0
  68. package/tests/stores/ServerStore.test.ts +48 -0
  69. package/tests/stores/TypedStore.test.ts +54 -0
  70. package/tests/stores/UserStore.test.ts +136 -0
  71. package/tests/subscription/SubscriptionGroup.test.ts +34 -0
  72. package/tests/types/game.test.ts +42 -0
  73. package/tsconfig.json +17 -0
  74. package/tsup.config.ts +9 -0
  75. package/vitest.config.ts +7 -0
@@ -0,0 +1,385 @@
1
+ export interface BadgePathDef {
2
+ calc(param?: number): void
3
+ path1?: string
4
+ path2?: string
5
+ flip?: 'rotate180' | 'rotate90' | 'rotate45'
6
+ }
7
+
8
+ export const BadgePaths: Record<number, BadgePathDef> = {
9
+ 1: {
10
+ calc(param = 0) {
11
+ let vert = 0
12
+ let hor = 0
13
+ if (param > 0) {
14
+ vert = param * 30 / 100
15
+ }
16
+ if (param < 0) {
17
+ hor = -param * 30 / 100
18
+ }
19
+ this.path1 = `M 50 ${100 - vert} L ${hor} 50 H ${100 - hor} Z`
20
+ this.path2 = `M ${hor} 50 H ${100 - hor} L 50 ${vert} Z`
21
+ },
22
+ },
23
+
24
+ 2: {
25
+ calc(param = 0) {
26
+ let x = 0
27
+ let y = 0
28
+ if (param > 0) {
29
+ x = param * 30 / 100
30
+ }
31
+ if (param < 0) {
32
+ y = -param * 30 / 100
33
+ }
34
+ this.path1 = `M ${x} ${y} L 50 50 L ${100 - x} ${y} V -1 H -1 Z`
35
+ this.path2 = `M ${x} ${100 - y} L 50 50 L ${100 - x} ${100 - y} V 101 H -1 Z`
36
+ },
37
+ },
38
+
39
+ 3: {
40
+ calc(param = 0) {
41
+ const angle = Math.PI / 4 + Math.PI / 4 * (param + 100) / 200
42
+ const angle1 = -Math.PI / 2
43
+ const angle2 = Math.PI / 2 + Math.PI / 3
44
+ const angle3 = Math.PI / 2 - Math.PI / 3
45
+
46
+ this.path1 = `M 50 50 L ${50 + 100 * Math.cos(angle1 - angle / 2)} ${50 + 100 * Math.sin(angle1 - angle / 2)} L ${50 + 100 * Math.cos(angle1 + angle / 2)} ${50 + 100 * Math.sin(angle1 + angle / 2)} Z`
47
+ this.path2 = `M 50 50 L ${50 + 100 * Math.cos(angle2 - angle / 2)} ${50 + 100 * Math.sin(angle2 - angle / 2)} L ${50 + 100 * Math.cos(angle2 + angle / 2)} ${50 + 100 * Math.sin(angle2 + angle / 2)} Z M 50 50 L ${50 + 100 * Math.cos(angle3 - angle / 2)} ${50 + 100 * Math.sin(angle3 - angle / 2)} L ${50 + 100 * Math.cos(angle3 + angle / 2)} ${50 + 100 * Math.sin(angle3 + angle / 2)}`
48
+ },
49
+ flip: 'rotate180',
50
+ },
51
+
52
+ 4: {
53
+ calc(param = 0) {
54
+ param += 100
55
+ const y1 = 50 - param * 30 / 200
56
+ const y2 = 50 + param * 30 / 200
57
+
58
+ this.path1 = `M 0 ${y2} H 100 V 100 H 0 Z`
59
+ this.path2 = param > 0 ? `M 0 ${y1} H 100 V ${y2} H 0 Z` : ''
60
+ },
61
+ flip: 'rotate90',
62
+ },
63
+
64
+ 5: {
65
+ calc(param = 0) {
66
+ param += 100
67
+ const x1 = 50 - param * 10 / 200 - 10
68
+ const x2 = 50 + param * 10 / 200 + 10
69
+
70
+ this.path1 = `M ${x1} 0 H ${x2} V 100 H ${x1} Z`
71
+ this.path2 = `M 0 ${x1} H 100 V ${x2} H 0 Z`
72
+ },
73
+ flip: 'rotate45',
74
+ },
75
+
76
+ 6: {
77
+ calc(param = 0) {
78
+ const width = 5 + (param + 100) * 8 / 200
79
+ const x1 = 50
80
+ const x2 = 20
81
+ const x3 = 80
82
+
83
+ this.path1 = `M ${x1 - width} 0 H ${x1 + width} V 100 H ${x1 - width}`
84
+ this.path2 = `M ${x2 - width} 0 H ${x2 + width} V 100 H ${x2 - width} Z M ${x3 - width} 0 H ${x3 + width} V 100 H ${x3 - width} Z`
85
+ },
86
+ flip: 'rotate90',
87
+ },
88
+
89
+ 7: {
90
+ calc(param = 0) {
91
+ const w = 20 + param * 10 / 100
92
+
93
+ this.path1 = `M 0 50 Q 25 30 50 50 T 100 50 V 100 H 0 Z`
94
+ this.path2 = `M 0 ${50 - w} Q 25 ${30 - w} 50 ${50 - w} T 100 ${50 - w} V ${50 + w} Q 75 ${70 + w} 50 ${50 + w} T 0 ${50 + w} Z`
95
+ },
96
+ flip: 'rotate90',
97
+ },
98
+
99
+ 8: {
100
+ calc(param = 0) {
101
+ const y = param * 20 / 100
102
+
103
+ this.path1 = `M 0 50 H 100 V 100 H 0 Z`
104
+ this.path2 = `M 0 50 Q 50 ${y} 100 50 Q 50 ${100 - y} 0 50 Z`
105
+ },
106
+ flip: 'rotate90',
107
+ },
108
+
109
+ 9: {
110
+ calc(param = 0) {
111
+ let y1 = 0
112
+ let y2 = 50
113
+ const h = 70
114
+ if (param > 0) y1 += param / 100 * 20
115
+ if (param < 0) y2 += param / 100 * 30
116
+
117
+ this.path1 = `M 50 ${y1} L 100 ${y1 + h} V 101 H 0 V ${y1 + h} Z`
118
+ this.path2 = `M 50 ${y1 + y2} L 100 ${y1 + y2 + h} V 101 H 0 V ${y1 + y2 + h} Z`
119
+ },
120
+ flip: 'rotate180',
121
+ },
122
+
123
+ 10: {
124
+ calc(param = 0) {
125
+ let r = 30
126
+ let d = 7
127
+
128
+ if (param > 0) r += param * 50 / 100
129
+ if (param < 0) d -= param * 20 / 100
130
+
131
+ this.path1 = `M ${50 + d + r} ${50 - r} A ${r} ${r} 0 0 0 ${50 + d + r} ${50 + r} H 101 V ${50 - r} Z`
132
+ this.path2 = `M ${50 - d - r} ${50 - r} A ${r} ${r} 0 0 1 ${50 - d - r} ${50 + r} H -1 V ${50 - r} Z`
133
+ },
134
+ flip: 'rotate90',
135
+ },
136
+
137
+ 11: {
138
+ calc(param = 0) {
139
+ let a1 = 30
140
+ let a2 = 30
141
+ const x = 50 - 50 * Math.cos(Math.PI / 4)
142
+ const y = 50 - 50 * Math.sin(Math.PI / 4)
143
+
144
+ if (param > 0) {
145
+ a1 += param * 25 / 100
146
+ a2 += param * 25 / 100
147
+ }
148
+ if (param < 0) {
149
+ a2 -= param * 50 / 100
150
+ }
151
+
152
+ this.path1 = `M ${x} ${y} Q ${a1} 50 ${x} ${100 - y} H 0 V ${y} Z M ${100 - x} ${y} Q ${100 - a1} 50 ${100 - x} ${100 - y} H 100 V ${y} Z`
153
+ this.path2 = `M ${x} ${y} Q 50 ${a2} ${100 - x} ${y} V 0 H ${x} Z M ${x} ${100 - y} Q 50 ${100 - a2} ${100 - x} ${100 - y} V 100 H ${x} Z`
154
+ },
155
+ flip: 'rotate90',
156
+ },
157
+
158
+ 12: {
159
+ calc(param = 0) {
160
+ let a1 = 30
161
+ let a2 = 35
162
+
163
+ if (param > 0) a1 += param * 30 / 100
164
+ if (param < 0) a2 += param * 15 / 100
165
+
166
+ this.path1 = `M 0 ${a1} H 100 V 100 H 0 Z`
167
+ this.path2 = `M 0 ${a1} H ${a2} V 100 H 0 Z M 100 ${a1} H ${100 - a2} V 100 H 100 Z`
168
+ },
169
+ flip: 'rotate180',
170
+ },
171
+
172
+ 13: {
173
+ calc(param = 0) {
174
+ let r = 30
175
+ let d = 0
176
+
177
+ if (param > 0) r += param * 50 / 100
178
+ if (param < 0) d -= param * 20 / 100
179
+
180
+ this.path1 = `M 0 0 H 50 V 100 H 0 Z`
181
+ this.path2 = `M ${50 - r} ${50 - d - r} A ${r} ${r} 0 0 0 ${50 + r} ${50 - r - d} V 0 H ${50 - r} Z`
182
+ },
183
+ flip: 'rotate180',
184
+ },
185
+
186
+ 14: {
187
+ calc(param = 0) {
188
+ let a = Math.PI / 4
189
+ const d = 0
190
+
191
+ a += param * Math.PI / 4 / 100
192
+
193
+ this.path1 = `M 50 0 Q 50 ${50 + d} ${50 + 50 * Math.cos(a)} ${50 + 50 * Math.sin(a)} H 100 V 0 H 50 Z`
194
+ this.path2 = `M 50 0 Q 50 ${50 + d} ${50 - 50 * Math.cos(a)} ${50 + 50 * Math.sin(a)} H 0 V 0 H 50 Z`
195
+ },
196
+ flip: 'rotate180',
197
+ },
198
+
199
+ 15: {
200
+ calc(param = 0) {
201
+ const w = 13 + param * 6 / 100
202
+ const r1 = 80
203
+ const r2 = 45
204
+ const d = 10
205
+
206
+ this.path1 = `M ${50 - r1 - w} ${100 + d} A ${r1 + w} ${r1 + w} 0 0 1 ${50 + r1 + w} ${100 + d} H ${50 + r1 - w} A ${r1 - w} ${r1 - w} 0 1 0 ${50 - r1 + w} ${100 + d}`
207
+ this.path2 = `M ${50 - r2 - w} ${100 + d} A ${r2 + w} ${r2 + w} 0 0 1 ${50 + r2 + w} ${100 + d} H ${50 + r2 - w} A ${r2 - w} ${r2 - w} 0 1 0 ${50 - r2 + w} ${100 + d}`
208
+ },
209
+ flip: 'rotate180',
210
+ },
211
+
212
+ 16: {
213
+ calc(param = 0) {
214
+ let a = 30 * Math.PI / 180
215
+ let d = 25
216
+
217
+ if (param > 0) {
218
+ a += 30 * Math.PI / 180 * param / 100
219
+ }
220
+ if (param < 0) {
221
+ d += param * 25 / 100
222
+ }
223
+
224
+ this.path1 = ''
225
+ for (let i = 0; i < 3; i++) {
226
+ const angle1 = i * Math.PI * 2 / 3 + a / 2 - Math.PI / 2
227
+ const angle2 = i * Math.PI * 2 / 3 - a / 2 - Math.PI / 2
228
+
229
+ this.path1 += `M ${50 + 100 * Math.cos(angle1)} ${50 + 100 * Math.sin(angle1)} L ${50 + 100 * Math.cos(angle2)} ${50 + 100 * Math.sin(angle2)} L ${50 + d * Math.cos(angle2)} ${50 + d * Math.sin(angle2)} A ${d} ${d} 0 0 1 ${50 + d * Math.cos(angle1)} ${50 + d * Math.sin(angle1)} Z`
230
+ }
231
+
232
+ this.path2 = ''
233
+ for (let i = 0; i < 3; i++) {
234
+ const angle1 = i * Math.PI * 2 / 3 + a / 2 + Math.PI / 2
235
+ const angle2 = i * Math.PI * 2 / 3 - a / 2 + Math.PI / 2
236
+
237
+ this.path2 += `M ${50 + 100 * Math.cos(angle1)} ${50 + 100 * Math.sin(angle1)} L ${50 + 100 * Math.cos(angle2)} ${50 + 100 * Math.sin(angle2)} L ${50 + d * Math.cos(angle2)} ${50 + d * Math.sin(angle2)} A ${d} ${d} 0 0 1 ${50 + d * Math.cos(angle1)} ${50 + d * Math.sin(angle1)} Z`
238
+ }
239
+ },
240
+ },
241
+
242
+ 17: {
243
+ calc(param = 0) {
244
+ let w = 35
245
+ let h = 45
246
+
247
+ if (param > 0) {
248
+ w += param * 20 / 100
249
+ }
250
+ if (param < 0) {
251
+ h -= param * 30 / 100
252
+ }
253
+
254
+ this.path1 = `M 50 45 L ${50 - w} ${h + 45} H ${50 + w} Z`
255
+ this.path2 = `M 50 0 L ${50 - w} ${h} H ${50 + w} Z`
256
+ },
257
+ },
258
+
259
+ 18: {
260
+ calc(param = 0) {
261
+ let a = 90 * Math.PI / 180
262
+ let d = 10
263
+
264
+ if (param > 0) {
265
+ a -= 60 / 180 * Math.PI * param / 100
266
+ }
267
+ if (param < 0) {
268
+ d -= param * 15 / 100
269
+ }
270
+
271
+ this.path1 = ''
272
+ this.path2 = ''
273
+ for (let i = 0; i < 3; i++) {
274
+ const angle1 = Math.PI * 2 / 3 * i + a / 2 - Math.PI / 2
275
+ const angle2 = Math.PI * 2 / 3 * i - a / 2 - Math.PI / 2
276
+ const path = `M ${50 + 100 * Math.cos(angle1)} ${50 + 100 * Math.sin(angle1)} L ${50 + 100 * Math.cos(angle2)} ${50 + 100 * Math.sin(angle2)} L ${50 + d * Math.cos((angle1 + angle2) / 2)} ${50 + d * Math.sin((angle1 + angle2) / 2)} Z`
277
+
278
+ if (!i) {
279
+ this.path1 += path
280
+ } else {
281
+ this.path2 += path
282
+ }
283
+ }
284
+ },
285
+ flip: 'rotate180',
286
+ },
287
+
288
+ 19: {
289
+ calc(param = 0) {
290
+ let w2 = 20
291
+ let w1 = 60
292
+
293
+ w1 += param * 20 / 100
294
+ w2 += param * 20 / 100
295
+
296
+ this.path1 = `M 50 -10 L ${50 - w1} 100 H ${50 + w1} Z`
297
+ this.path2 = ''
298
+ if (w2 > 0) {
299
+ this.path2 = `M 50 0 L ${50 - w2} 100 H ${50 + w2} Z`
300
+ }
301
+ },
302
+ flip: 'rotate180',
303
+ },
304
+
305
+ 20: {
306
+ calc(param = 0) {
307
+ let w = 10
308
+ let h = 20
309
+
310
+ if (param > 0) w += param * 20 / 100
311
+ if (param < 0) h += param * 40 / 100
312
+
313
+ this.path1 = `M 0 ${50 - h} H ${50 - w} V 100 H 0 Z`
314
+ this.path2 = `M ${50 + w} 0 V ${50 + h} H 100 V 0 Z`
315
+ },
316
+ flip: 'rotate90',
317
+ },
318
+
319
+ 21: {
320
+ calc(param = 0) {
321
+ let w = 40
322
+ let h = 50
323
+
324
+ if (param > 0) w -= param * 20 / 100
325
+ if (param < 0) h += param * 20 / 100
326
+
327
+ this.path1 = `M 50 ${h} Q ${50 + w} 0 50 0 T 50 ${h} Z M 50 ${100 - h} Q ${50 + w} 100 50 100 T 50 ${100 - h} Z`
328
+ this.path2 = `M ${h} 50 Q 0 ${50 + w} 0 50 T ${h} 50 Z M ${100 - h} 50 Q 100 ${50 + w} 100 50 T ${100 - h} 50 Z`
329
+ },
330
+ flip: 'rotate45',
331
+ },
332
+
333
+ 22: {
334
+ calc(param = 0) {
335
+ let w = 20
336
+
337
+ w += param * 10 / 100
338
+
339
+ this.path1 = `M ${50 - w} ${50 - w} H ${50 + w} V ${50 + w} H ${50 - w} Z`
340
+ this.path2 = ''
341
+
342
+ for (let i = -4; i < 4; i++) {
343
+ for (let j = -4; j < 4; j++) {
344
+ const a = (i + j) % 2
345
+ this.path2 += `M ${50 - w - w * 2 * i} ${50 - w - w * 2 * (j + a)} h ${-w * 2} v ${w * 2} h ${w * 2} Z`
346
+ }
347
+ }
348
+ },
349
+ flip: 'rotate45',
350
+ },
351
+
352
+ 23: {
353
+ calc(param = 0) {
354
+ let w = 17
355
+ let h = 25
356
+
357
+ if (param > 0) w += param * 35 / 100
358
+ if (param < 0) h -= param * 23 / 100
359
+
360
+ this.path1 = ''
361
+ for (let i = -4; i <= 4; i++) {
362
+ this.path1 += `M ${50 - w * i * 2} ${50 - h} l ${-w} ${-h} l ${-w} ${h} l ${w} ${h} Z`
363
+ }
364
+ this.path2 = ''
365
+ for (let i = -4; i <= 4; i++) {
366
+ this.path2 += `M ${50 - w * i * 2} ${50 + h} l ${-w} ${-h} l ${-w} ${h} l ${w} ${h} Z`
367
+ }
368
+ },
369
+ flip: 'rotate90',
370
+ },
371
+
372
+ 24: {
373
+ calc(param = 0) {
374
+ let w = 50
375
+ let h = 45
376
+
377
+ if (param > 0) w += param * 60 / 100
378
+ if (param < 0) h += param * 30 / 100
379
+
380
+ this.path1 = `M 0 ${h} L 50 70 L 100 ${h} V 100 H 0 Z`
381
+ this.path2 = `M 50 0 L ${50 + w} 100 H 100 V ${h} L 50 70 L 0 ${h} V 100 H ${50 - w} Z`
382
+ },
383
+ flip: 'rotate180',
384
+ },
385
+ }
@@ -0,0 +1,71 @@
1
+ import type { StorageAdapter } from '../storage/StorageAdapter.js'
2
+
3
+ interface MemoryEntry {
4
+ data: unknown
5
+ expires?: number
6
+ }
7
+
8
+ export class Cache {
9
+ private readonly memory = new Map<string, MemoryEntry>()
10
+ private readonly storage: StorageAdapter | null
11
+ private readonly namespace: string
12
+
13
+ constructor(namespace: string, storage: StorageAdapter | null) {
14
+ this.namespace = namespace
15
+ this.storage = storage
16
+ }
17
+
18
+ private memKey(key: string): string {
19
+ return `${this.namespace}/${key}`
20
+ }
21
+
22
+ get<T>(key: string): T | undefined {
23
+ const entry = this.memory.get(this.memKey(key))
24
+ if (!entry) return undefined
25
+ if (entry.expires !== undefined && Date.now() > entry.expires) {
26
+ this.memory.delete(this.memKey(key))
27
+ return undefined
28
+ }
29
+ return entry.data as T
30
+ }
31
+
32
+ set<T>(key: string, data: T, ttlMs?: number): void {
33
+ this.memory.set(this.memKey(key), {
34
+ data,
35
+ expires: ttlMs !== undefined ? Date.now() + ttlMs : undefined,
36
+ })
37
+ }
38
+
39
+ delete(key: string): void {
40
+ this.memory.delete(this.memKey(key))
41
+ }
42
+
43
+ async getPersistent(key: string): Promise<Uint8Array | null> {
44
+ if (!this.storage) return null
45
+ return this.storage.get(`${this.namespace}/${key}`)
46
+ }
47
+
48
+ async setPersistent(key: string, data: Uint8Array): Promise<void> {
49
+ if (!this.storage) return
50
+ await this.storage.set(`${this.namespace}/${key}`, data)
51
+ }
52
+
53
+ async deletePersistent(key: string): Promise<void> {
54
+ if (!this.storage) return
55
+ await this.storage.delete(`${this.namespace}/${key}`)
56
+ }
57
+
58
+ clearMemory(): void {
59
+ this.memory.clear()
60
+ }
61
+
62
+ async clearPersistent(): Promise<void> {
63
+ if (!this.storage) return
64
+ await this.storage.clear()
65
+ }
66
+
67
+ async clearAll(): Promise<void> {
68
+ this.clearMemory()
69
+ await this.clearPersistent()
70
+ }
71
+ }
@@ -0,0 +1,112 @@
1
+ import type { StorageAdapter } from '../storage/StorageAdapter.js'
2
+ import type { RoomMap2Data } from '../types/game.js'
3
+
4
+ interface CachedEntry {
5
+ data: RoomMap2Data
6
+ lastSeen: number // wall-clock ms; meaningful across sessions
7
+ lastAccess: number // monotonic session sequence; used for in-memory LRU order
8
+ }
9
+
10
+ interface PersistedEntry {
11
+ data: RoomMap2Data
12
+ lastSeen: number
13
+ lastAccess: number
14
+ }
15
+
16
+ export interface Map2StorageOptions {
17
+ adapter: StorageAdapter | null
18
+ namespace: string
19
+ maxEntries: number
20
+ }
21
+
22
+ export class Map2Storage {
23
+ private readonly memory = new Map<string, CachedEntry>()
24
+ private readonly adapter: StorageAdapter | null
25
+ private readonly maxEntries: number
26
+ readonly namespace: string
27
+ private accessSeq = 0
28
+
29
+ constructor(opts: Map2StorageOptions) {
30
+ this.adapter = opts.adapter
31
+ this.maxEntries = opts.maxEntries
32
+ this.namespace = opts.namespace
33
+ }
34
+
35
+ private key(room: string, shard: string | null): string {
36
+ return `${shard ?? '_'}/${room}`
37
+ }
38
+
39
+ private adapterKey(k: string): string {
40
+ return `map2/${k}`
41
+ }
42
+
43
+ private tick(): number { return ++this.accessSeq }
44
+
45
+ private serialize(entry: CachedEntry): Uint8Array {
46
+ const payload: PersistedEntry = { data: entry.data, lastSeen: entry.lastSeen, lastAccess: entry.lastAccess }
47
+ return new TextEncoder().encode(JSON.stringify(payload))
48
+ }
49
+
50
+ private deserialize(bytes: Uint8Array): PersistedEntry | null {
51
+ try {
52
+ return JSON.parse(new TextDecoder().decode(bytes)) as PersistedEntry
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ /** Synchronous read from memory only. Updates lastAccess on hit. */
59
+ getMemory(room: string, shard: string | null): RoomMap2Data | null {
60
+ const entry = this.memory.get(this.key(room, shard))
61
+ if (!entry) return null
62
+ entry.lastAccess = this.tick()
63
+ return entry.data
64
+ }
65
+
66
+ /**
67
+ * Async read: memory first, then IndexedDB fallback.
68
+ * Hydrates memory on IndexedDB hit.
69
+ */
70
+ async get(room: string, shard: string | null): Promise<RoomMap2Data | null> {
71
+ const memData = this.getMemory(room, shard)
72
+ if (memData) return memData
73
+ if (!this.adapter) return null
74
+
75
+ const k = this.key(room, shard)
76
+ const bytes = await this.adapter.get(this.adapterKey(k))
77
+ if (!bytes) return null
78
+
79
+ const persisted = this.deserialize(bytes)
80
+ if (!persisted) return null
81
+
82
+ this.memory.set(k, { data: persisted.data, lastSeen: persisted.lastSeen, lastAccess: this.tick() })
83
+ return persisted.data
84
+ }
85
+
86
+ /**
87
+ * Write to memory synchronously, then persist to IndexedDB.
88
+ * Callers that don't need to await persistence: void this.put(...)
89
+ */
90
+ async put(room: string, shard: string | null, data: RoomMap2Data): Promise<void> {
91
+ const k = this.key(room, shard)
92
+ const entry: CachedEntry = { data, lastSeen: Date.now(), lastAccess: this.tick() }
93
+ // Memory update is synchronous — happens before any await
94
+ this.memory.set(k, entry)
95
+ // Evict LRU entries (awaits adapter deletes so the full put() resolves after eviction)
96
+ await this.pruneIfNeeded()
97
+ if (this.adapter) {
98
+ await this.adapter.set(this.adapterKey(k), this.serialize(entry))
99
+ }
100
+ }
101
+
102
+ private async pruneIfNeeded(): Promise<void> {
103
+ if (this.memory.size <= this.maxEntries) return
104
+ const sorted = [...this.memory.entries()]
105
+ .sort((a, b) => a[1].lastAccess - b[1].lastAccess)
106
+ const toEvict = sorted.slice(0, this.memory.size - this.maxEntries)
107
+ for (const [k] of toEvict) {
108
+ this.memory.delete(k)
109
+ if (this.adapter) await this.adapter.delete(this.adapterKey(k))
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,2 @@
1
+ export { FileStorage } from './storage/FileStorage.js'
2
+ export type { StorageAdapter } from './storage/StorageAdapter.js'