nostr-arena 0.1.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/dist/__tests__/callbacks.test.d.ts +28 -0
  4. package/dist/__tests__/callbacks.test.d.ts.map +1 -0
  5. package/dist/__tests__/callbacks.test.js +140 -0
  6. package/dist/__tests__/callbacks.test.js.map +1 -0
  7. package/dist/__tests__/config.test.d.ts +18 -0
  8. package/dist/__tests__/config.test.d.ts.map +1 -0
  9. package/dist/__tests__/config.test.js +43 -0
  10. package/dist/__tests__/config.test.js.map +1 -0
  11. package/dist/__tests__/error-handling.test.d.ts +14 -0
  12. package/dist/__tests__/error-handling.test.d.ts.map +1 -0
  13. package/dist/__tests__/error-handling.test.js +199 -0
  14. package/dist/__tests__/error-handling.test.js.map +1 -0
  15. package/dist/__tests__/protocol.test.d.ts +7 -0
  16. package/dist/__tests__/protocol.test.d.ts.map +1 -0
  17. package/dist/__tests__/protocol.test.js +257 -0
  18. package/dist/__tests__/protocol.test.js.map +1 -0
  19. package/dist/__tests__/state-flow.test.d.ts +37 -0
  20. package/dist/__tests__/state-flow.test.d.ts.map +1 -0
  21. package/dist/__tests__/state-flow.test.js +160 -0
  22. package/dist/__tests__/state-flow.test.js.map +1 -0
  23. package/dist/__tests__/timing.test.d.ts +20 -0
  24. package/dist/__tests__/timing.test.d.ts.map +1 -0
  25. package/dist/__tests__/timing.test.js +73 -0
  26. package/dist/__tests__/timing.test.js.map +1 -0
  27. package/dist/core/Arena.d.ts +164 -0
  28. package/dist/core/Arena.d.ts.map +1 -0
  29. package/dist/core/Arena.js +634 -0
  30. package/dist/core/Arena.js.map +1 -0
  31. package/dist/core/NostrClient.d.ts +108 -0
  32. package/dist/core/NostrClient.d.ts.map +1 -0
  33. package/dist/core/NostrClient.js +225 -0
  34. package/dist/core/NostrClient.js.map +1 -0
  35. package/dist/core/index.d.ts +8 -0
  36. package/dist/core/index.d.ts.map +1 -0
  37. package/dist/core/index.js +7 -0
  38. package/dist/core/index.js.map +1 -0
  39. package/dist/index.d.ts +35 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +37 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/proxy.d.ts +36 -0
  44. package/dist/proxy.d.ts.map +1 -0
  45. package/dist/proxy.js +98 -0
  46. package/dist/proxy.js.map +1 -0
  47. package/dist/react/index.d.ts +7 -0
  48. package/dist/react/index.d.ts.map +1 -0
  49. package/dist/react/index.js +6 -0
  50. package/dist/react/index.js.map +1 -0
  51. package/dist/react/useArena.d.ts +74 -0
  52. package/dist/react/useArena.d.ts.map +1 -0
  53. package/dist/react/useArena.js +224 -0
  54. package/dist/react/useArena.js.map +1 -0
  55. package/dist/retry.d.ts +54 -0
  56. package/dist/retry.d.ts.map +1 -0
  57. package/dist/retry.js +82 -0
  58. package/dist/retry.js.map +1 -0
  59. package/dist/testing/MockArena.d.ts +84 -0
  60. package/dist/testing/MockArena.d.ts.map +1 -0
  61. package/dist/testing/MockArena.js +156 -0
  62. package/dist/testing/MockArena.js.map +1 -0
  63. package/dist/testing/index.d.ts +6 -0
  64. package/dist/testing/index.d.ts.map +1 -0
  65. package/dist/testing/index.js +6 -0
  66. package/dist/testing/index.js.map +1 -0
  67. package/dist/types.d.ts +183 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/types.js +55 -0
  70. package/dist/types.js.map +1 -0
  71. package/package.json +84 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * State Flow Tests
3
+ *
4
+ * Source: architecture.md - State Flow diagram
5
+ *
6
+ * ```
7
+ * ┌─────────┐ create() ┌─────────┐ opponent ┌─────────┐
8
+ * │ idle │────────────▶│ waiting │────joins───▶│ ready │
9
+ * └─────────┘ └─────────┘ └────┬────┘
10
+ * ▲ │
11
+ * │ game starts
12
+ * │ │
13
+ * │ ┌──────────┐ ┌───▼─────┐
14
+ * │◀────leave()──────│ finished │◀──gameover──│ playing │
15
+ * │ └────┬─────┘ └─────────┘
16
+ * │ │
17
+ * │ rematch
18
+ * │ │
19
+ * │ ┌────▼────┐
20
+ * └──────────────────│ ready │
21
+ * └─────────┘
22
+ * ```
23
+ *
24
+ * Source: README.md - Room States table
25
+ *
26
+ * | Status | Description |
27
+ * | -------- | ------------------------------------- |
28
+ * | idle | No room active |
29
+ * | creating | Creating a new room |
30
+ * | waiting | Waiting for opponent to join |
31
+ * | joining | Joining an existing room |
32
+ * | ready | Both players connected, ready to play |
33
+ * | playing | Game in progress |
34
+ * | finished | Game ended |
35
+ */
36
+ import { describe, it, expect, beforeEach } from 'vitest';
37
+ import { MockArena } from '../testing/MockArena';
38
+ describe('State Flow (architecture.md)', () => {
39
+ let room;
40
+ beforeEach(() => {
41
+ room = new MockArena({ gameId: 'test-game' });
42
+ });
43
+ describe('Initial state: idle', () => {
44
+ /**
45
+ * Source: README.md - "idle: No room active"
46
+ */
47
+ it('should start in idle status', () => {
48
+ expect(room.roomState.status).toBe('idle');
49
+ });
50
+ });
51
+ describe('Transition: idle → waiting (via create())', () => {
52
+ /**
53
+ * Source: architecture.md diagram
54
+ * "idle ──create()──▶ waiting"
55
+ */
56
+ it('create() should transition from idle to waiting', async () => {
57
+ expect(room.roomState.status).toBe('idle');
58
+ await room.create();
59
+ expect(room.roomState.status).toBe('waiting');
60
+ });
61
+ it('after create(), isHost should be true', async () => {
62
+ await room.create();
63
+ expect(room.roomState.isHost).toBe(true);
64
+ });
65
+ });
66
+ describe('Transition: waiting → ready (opponent joins)', () => {
67
+ /**
68
+ * Source: architecture.md diagram
69
+ * "waiting ──opponent joins──▶ ready"
70
+ */
71
+ it('should transition from waiting to ready when opponent joins', async () => {
72
+ await room.create();
73
+ expect(room.roomState.status).toBe('waiting');
74
+ room.simulateOpponentJoin('opponent-pubkey');
75
+ expect(room.roomState.status).toBe('ready');
76
+ });
77
+ });
78
+ describe('Transition: idle → ready (via join())', () => {
79
+ /**
80
+ * Source: architecture.md - Arena API
81
+ * "join(roomId): Promise<void>"
82
+ *
83
+ * Note: The diagram shows idle → joining → ready, but for MockArena
84
+ * join() directly goes to ready (simplified for testing)
85
+ */
86
+ it('join() should transition to ready', async () => {
87
+ expect(room.roomState.status).toBe('idle');
88
+ await room.join('existing-room-id');
89
+ expect(room.roomState.status).toBe('ready');
90
+ });
91
+ it('after join(), isHost should be false', async () => {
92
+ await room.join('existing-room-id');
93
+ expect(room.roomState.isHost).toBe(false);
94
+ });
95
+ });
96
+ describe('Transition: playing → finished (gameover)', () => {
97
+ /**
98
+ * Source: architecture.md diagram
99
+ * "playing ──gameover──▶ finished"
100
+ */
101
+ it('sendGameOver() should transition to finished', async () => {
102
+ await room.create();
103
+ room.simulateOpponentJoin('opponent');
104
+ room.sendGameOver('win', 500);
105
+ expect(room.roomState.status).toBe('finished');
106
+ });
107
+ });
108
+ describe('Transition: any → idle (leave())', () => {
109
+ /**
110
+ * Source: architecture.md diagram
111
+ * "◀────leave()────"
112
+ */
113
+ it('leave() from waiting should go to idle', async () => {
114
+ await room.create();
115
+ expect(room.roomState.status).toBe('waiting');
116
+ room.leave();
117
+ expect(room.roomState.status).toBe('idle');
118
+ });
119
+ it('leave() from ready should go to idle', async () => {
120
+ await room.create();
121
+ room.simulateOpponentJoin('opponent');
122
+ expect(room.roomState.status).toBe('ready');
123
+ room.leave();
124
+ expect(room.roomState.status).toBe('idle');
125
+ });
126
+ it('leave() from finished should go to idle', async () => {
127
+ await room.create();
128
+ room.simulateOpponentJoin('opponent');
129
+ room.sendGameOver('win');
130
+ expect(room.roomState.status).toBe('finished');
131
+ room.leave();
132
+ expect(room.roomState.status).toBe('idle');
133
+ });
134
+ });
135
+ describe('Transition: finished → ready (rematch)', () => {
136
+ /**
137
+ * Source: architecture.md diagram
138
+ * "finished ──rematch──▶ ready"
139
+ */
140
+ it('acceptRematch() should transition from finished to ready', async () => {
141
+ await room.create();
142
+ room.simulateOpponentJoin('opponent');
143
+ room.sendGameOver('win');
144
+ expect(room.roomState.status).toBe('finished');
145
+ room.simulateRematchRequested();
146
+ room.acceptRematch();
147
+ expect(room.roomState.status).toBe('ready');
148
+ });
149
+ it('rematch should generate new seed', async () => {
150
+ await room.create();
151
+ room.simulateOpponentJoin('opponent');
152
+ const oldSeed = room.roomState.seed;
153
+ room.sendGameOver('win');
154
+ room.simulateRematchRequested();
155
+ room.acceptRematch();
156
+ expect(room.roomState.seed).not.toBe(oldSeed);
157
+ });
158
+ });
159
+ });
160
+ //# sourceMappingURL=state-flow.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/state-flow.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAMjD,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,IAAI,IAA8B,CAAC;IAEnC,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,GAAG,IAAI,SAAS,CAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC;;WAEG;QAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;YACrC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACzD;;;WAGG;QAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;YAC/D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;YACrD,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,8CAA8C,EAAE,GAAG,EAAE;QAC5D;;;WAGG;QAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;YAC3E,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAE9C,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;YAC7C,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,uCAAuC,EAAE,GAAG,EAAE;QACrD;;;;;;WAMG;QAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;YACjD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;YACpD,MAAM,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACzD;;;WAGG;QAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;YAC5D,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;YAEtC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAChD;;;WAGG;QAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;YACtD,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAE9C,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;YACpD,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;YACtC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAE5C,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;YACtC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAE/C,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,wCAAwC,EAAE,GAAG,EAAE;QACtD;;;WAGG;QAEH,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;YACxE,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;YACtC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAE/C,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAChC,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;YAChD,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;YACtC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAEpC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAChC,IAAI,CAAC,aAAa,EAAE,CAAC;YAErB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Timing Tests
3
+ *
4
+ * Source: architecture.md - Timing section
5
+ *
6
+ * | Constant | Default | Description |
7
+ * | --------------------- | ----------------- | --------------------- |
8
+ * | `roomExpiry` | 600000ms (10 min) | Room expiration |
9
+ * | `heartbeatInterval` | 3000ms | Heartbeat sending |
10
+ * | `disconnectThreshold` | 10000ms | Disconnect detection |
11
+ * | `stateThrottle` | 100ms | State update throttle |
12
+ * | `joinTimeout` | 30000ms | Join timeout |
13
+ *
14
+ * Source: protocol.md - Heartbeat section
15
+ *
16
+ * "Every 3 seconds"
17
+ * "If no heartbeat received for 10 seconds, opponent is considered disconnected."
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=timing.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/timing.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Timing Tests
3
+ *
4
+ * Source: architecture.md - Timing section
5
+ *
6
+ * | Constant | Default | Description |
7
+ * | --------------------- | ----------------- | --------------------- |
8
+ * | `roomExpiry` | 600000ms (10 min) | Room expiration |
9
+ * | `heartbeatInterval` | 3000ms | Heartbeat sending |
10
+ * | `disconnectThreshold` | 10000ms | Disconnect detection |
11
+ * | `stateThrottle` | 100ms | State update throttle |
12
+ * | `joinTimeout` | 30000ms | Join timeout |
13
+ *
14
+ * Source: protocol.md - Heartbeat section
15
+ *
16
+ * "Every 3 seconds"
17
+ * "If no heartbeat received for 10 seconds, opponent is considered disconnected."
18
+ */
19
+ import { describe, it, expect } from 'vitest';
20
+ import { DEFAULT_CONFIG } from '../types';
21
+ describe('Timing Constants (architecture.md)', () => {
22
+ describe('roomExpiry', () => {
23
+ it('should be 600000ms (10 min)', () => {
24
+ expect(DEFAULT_CONFIG.roomExpiry).toBe(600000);
25
+ });
26
+ it('should equal 10 minutes in milliseconds', () => {
27
+ const tenMinutesMs = 10 * 60 * 1000;
28
+ expect(DEFAULT_CONFIG.roomExpiry).toBe(tenMinutesMs);
29
+ });
30
+ });
31
+ describe('heartbeatInterval', () => {
32
+ it('should be 3000ms', () => {
33
+ expect(DEFAULT_CONFIG.heartbeatInterval).toBe(3000);
34
+ });
35
+ it('should equal 3 seconds in milliseconds', () => {
36
+ const threeSecondsMs = 3 * 1000;
37
+ expect(DEFAULT_CONFIG.heartbeatInterval).toBe(threeSecondsMs);
38
+ });
39
+ });
40
+ describe('disconnectThreshold', () => {
41
+ it('should be 10000ms', () => {
42
+ expect(DEFAULT_CONFIG.disconnectThreshold).toBe(10000);
43
+ });
44
+ it('should equal 10 seconds in milliseconds', () => {
45
+ const tenSecondsMs = 10 * 1000;
46
+ expect(DEFAULT_CONFIG.disconnectThreshold).toBe(tenSecondsMs);
47
+ });
48
+ });
49
+ describe('stateThrottle', () => {
50
+ it('should be 100ms', () => {
51
+ expect(DEFAULT_CONFIG.stateThrottle).toBe(100);
52
+ });
53
+ });
54
+ });
55
+ describe('Timing Relationships (protocol.md)', () => {
56
+ /**
57
+ * Source: protocol.md - Heartbeat section
58
+ *
59
+ * "Every 3 seconds ... If no heartbeat received for 10 seconds"
60
+ *
61
+ * This implies: disconnectThreshold > heartbeatInterval * 3
62
+ * (we need to miss at least 3 heartbeats to detect disconnect)
63
+ */
64
+ it('should detect disconnect after missing ~3 heartbeats', () => {
65
+ const missedHeartbeats = DEFAULT_CONFIG.disconnectThreshold / DEFAULT_CONFIG.heartbeatInterval;
66
+ // 10000 / 3000 = 3.33, meaning ~3 missed heartbeats trigger disconnect
67
+ expect(missedHeartbeats).toBeGreaterThanOrEqual(3);
68
+ });
69
+ it('disconnect threshold should be greater than heartbeat interval', () => {
70
+ expect(DEFAULT_CONFIG.disconnectThreshold).toBeGreaterThan(DEFAULT_CONFIG.heartbeatInterval);
71
+ });
72
+ });
73
+ //# sourceMappingURL=timing.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.test.js","sourceRoot":"","sources":["../../src/__tests__/timing.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;YACrC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;YACpC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;YAC1B,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,CAAC;YAChC,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;YAC3B,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;YACjD,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,CAAC;YAC/B,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;YACzB,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD;;;;;;;OAOG;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,gBAAgB,GAAG,cAAc,CAAC,mBAAmB,GAAG,cAAc,CAAC,iBAAiB,CAAC;QAC/F,uEAAuE;QACvE,MAAM,CAAC,gBAAgB,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,eAAe,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * nostr-battle-room - Arena
3
+ * Main class for managing multiplayer game rooms over Nostr
4
+ */
5
+ import type { ArenaConfig, RoomState, OpponentBase } from '../types';
6
+ /**
7
+ * Internal opponent state (mutable)
8
+ */
9
+ interface InternalOpponentState<TGameState> extends OpponentBase {
10
+ gameState: TGameState | null;
11
+ }
12
+ /**
13
+ * Arena - Manages a multiplayer game room over Nostr
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * interface MyGameState {
18
+ * score: number;
19
+ * position: { x: number; y: number };
20
+ * }
21
+ *
22
+ * const room = new Arena<MyGameState>({
23
+ * gameId: 'my-game',
24
+ * relays: ['wss://relay.damus.io'],
25
+ * });
26
+ *
27
+ * room.on('opponentState', (state) => {
28
+ * console.log('Opponent score:', state.score);
29
+ * });
30
+ *
31
+ * const url = await room.create();
32
+ * console.log('Share this URL:', url);
33
+ * ```
34
+ */
35
+ export declare class Arena<TGameState = Record<string, unknown>> {
36
+ private client;
37
+ private config;
38
+ private callbacks;
39
+ private _roomState;
40
+ private _opponent;
41
+ private unsubscribe;
42
+ private heartbeatInterval;
43
+ private disconnectCheckInterval;
44
+ private lastStateUpdate;
45
+ private storage;
46
+ constructor(config: ArenaConfig);
47
+ /** Current room state */
48
+ get roomState(): Readonly<RoomState>;
49
+ /** Current opponent state (null if no opponent) */
50
+ get opponent(): Readonly<InternalOpponentState<TGameState>> | null;
51
+ /** Whether connected to Nostr relays */
52
+ get isConnected(): boolean;
53
+ /** This player's public key */
54
+ get publicKey(): string;
55
+ /**
56
+ * Get the connection status of each relay
57
+ */
58
+ getRelayStatus(): Map<string, boolean>;
59
+ /**
60
+ * Check if at least one relay is connected
61
+ */
62
+ get hasConnectedRelay(): boolean;
63
+ /**
64
+ * Register event callback for opponentJoin
65
+ */
66
+ onOpponentJoin(callback: (publicKey: string) => void): this;
67
+ /**
68
+ * Register event callback for opponentState
69
+ */
70
+ onOpponentState(callback: (state: TGameState) => void): this;
71
+ /**
72
+ * Register event callback for opponentDisconnect
73
+ */
74
+ onOpponentDisconnect(callback: () => void): this;
75
+ /**
76
+ * Register event callback for opponentGameOver
77
+ */
78
+ onOpponentGameOver(callback: (reason: string, finalScore?: number) => void): this;
79
+ /**
80
+ * Register event callback for rematchRequested
81
+ */
82
+ onRematchRequested(callback: () => void): this;
83
+ /**
84
+ * Register event callback for rematchStart
85
+ */
86
+ onRematchStart(callback: (newSeed: number) => void): this;
87
+ /**
88
+ * Register event callback for error
89
+ */
90
+ onError(callback: (error: Error) => void): this;
91
+ /**
92
+ * Connect to Nostr relays
93
+ */
94
+ connect(): void;
95
+ /**
96
+ * Disconnect from relays and clean up
97
+ */
98
+ disconnect(): void;
99
+ /**
100
+ * Create a new room
101
+ *
102
+ * Times out after joinTimeout (default: 30 seconds)
103
+ * @returns Room URL (e.g., "https://example.com/battle/abc123")
104
+ */
105
+ create(baseUrl?: string): Promise<string>;
106
+ /**
107
+ * Internal create implementation
108
+ */
109
+ private createInternal;
110
+ /**
111
+ * Join an existing room
112
+ *
113
+ * Times out after joinTimeout (default: 30 seconds)
114
+ */
115
+ join(roomId: string): Promise<void>;
116
+ /**
117
+ * Internal join implementation
118
+ */
119
+ private joinInternal;
120
+ /**
121
+ * Leave the current room
122
+ */
123
+ leave(): void;
124
+ /**
125
+ * Attempt to reconnect to a previously joined room
126
+ */
127
+ reconnect(): Promise<boolean>;
128
+ /**
129
+ * Internal reconnection logic
130
+ */
131
+ private reconnectInternal;
132
+ /**
133
+ * Send game state to opponent (throttled)
134
+ */
135
+ sendState(state: TGameState): void;
136
+ /**
137
+ * Send game over event
138
+ */
139
+ sendGameOver(reason: string, finalScore?: number): void;
140
+ /**
141
+ * Request a rematch
142
+ */
143
+ requestRematch(): void;
144
+ /**
145
+ * Accept a rematch request
146
+ */
147
+ acceptRematch(): void;
148
+ private subscribeToRoom;
149
+ private handleRoomEvent;
150
+ private startHeartbeat;
151
+ private stopHeartbeat;
152
+ private startDisconnectCheck;
153
+ private stopDisconnectCheck;
154
+ private publishToRoom;
155
+ private createInitialOpponent;
156
+ private resetForRematch;
157
+ private get storageKey();
158
+ private saveRoom;
159
+ private loadRoom;
160
+ private clearRoom;
161
+ private isExpired;
162
+ }
163
+ export {};
164
+ //# sourceMappingURL=Arena.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Arena.d.ts","sourceRoot":"","sources":["../../src/core/Arena.ts"],"names":[],"mappings":"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"}