@tailuge/messaging 1.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/README.md +81 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lobby.d.ts +87 -0
- package/dist/lobby.js +243 -0
- package/dist/lobby.js.map +1 -0
- package/dist/messagingclient.d.ts +39 -0
- package/dist/messagingclient.js +109 -0
- package/dist/messagingclient.js.map +1 -0
- package/dist/nchanclient.d.ts +24 -0
- package/dist/nchanclient.js +109 -0
- package/dist/nchanclient.js.map +1 -0
- package/dist/table.d.ts +51 -0
- package/dist/table.js +126 -0
- package/dist/table.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/uid.d.ts +1 -0
- package/dist/utils/uid.js +4 -0
- package/dist/utils/uid.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Billiards Messaging Client
|
|
2
|
+
|
|
3
|
+
A stateful messaging library for Nchan-powered real-time applications. This library handles presence, heartbeats, matchmaking (challenges), and game table communication.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Install Dependencies
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### 2. Run Nchan Server (Docker)
|
|
13
|
+
The library requires an Nchan server for transport. You can start the local development server using:
|
|
14
|
+
```bash
|
|
15
|
+
npm run docker:nchan
|
|
16
|
+
```
|
|
17
|
+
To stop the server:
|
|
18
|
+
```bash
|
|
19
|
+
npm run docker:stop
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 3. Run the Example
|
|
23
|
+
```bash
|
|
24
|
+
npm run example
|
|
25
|
+
```
|
|
26
|
+
Then open [http://localhost:3000](http://localhost:3000) in multiple tabs to test presence and challenges.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Testing
|
|
31
|
+
|
|
32
|
+
The project includes three levels of testing:
|
|
33
|
+
|
|
34
|
+
### 1. Unit & Integration Tests (Jest)
|
|
35
|
+
Comprehensive tests for `MessagingClient`, `Lobby`, and `Table` logic. These use `testcontainers` to automatically spin up an Nchan instance.
|
|
36
|
+
```bash
|
|
37
|
+
npm run test
|
|
38
|
+
```
|
|
39
|
+
To debug leaks or hanging processes:
|
|
40
|
+
```bash
|
|
41
|
+
npx jest --config test/jest.config.js --detectOpenHandles --forceExit
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Browser Connection Tests (Playwright)
|
|
45
|
+
Verifies that the client correctly connects and communicates within a real browser environment.
|
|
46
|
+
```bash
|
|
47
|
+
npm run test:debug
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Nchan Configuration Tests (Shell)
|
|
51
|
+
A suite of `curl`-based tests to verify that the Nchan server endpoints and metadata enrichment are working correctly.
|
|
52
|
+
```bash
|
|
53
|
+
# Ensure local docker is running first
|
|
54
|
+
npm run docker:nchan
|
|
55
|
+
./docker/testnchan.sh
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Building
|
|
61
|
+
|
|
62
|
+
### Library & Example
|
|
63
|
+
The example is bundled using `esbuild`:
|
|
64
|
+
```bash
|
|
65
|
+
npm run build:example
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Docker Image
|
|
69
|
+
To rebuild the Nchan server image:
|
|
70
|
+
```bash
|
|
71
|
+
npm run docker:build
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
- **Linting**: `npm run lint`
|
|
79
|
+
- **Formatting**: `npm run format`
|
|
80
|
+
- **Specification**: See [MESSAGING_SPEC.md](./MESSAGING_SPEC.md) for the API contract and data models.
|
|
81
|
+
- **Architectural Overview**: See [AGENTS.md](./AGENTS.md) for the design patterns used.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC"}
|
package/dist/lobby.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { NchanClient } from "./nchanclient";
|
|
2
|
+
import { PresenceMessage, ChallengeMessage } from "./types";
|
|
3
|
+
import { Table } from "./table";
|
|
4
|
+
export interface LobbyOptions {
|
|
5
|
+
heartbeatInterval?: number;
|
|
6
|
+
pruneInterval?: number;
|
|
7
|
+
staleTtl?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Manages the global lobby state, including real-time presence tracking and challenge flows.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Lobby {
|
|
13
|
+
private nchan;
|
|
14
|
+
currentUser: PresenceMessage;
|
|
15
|
+
private users;
|
|
16
|
+
private listeners;
|
|
17
|
+
private challengeListeners;
|
|
18
|
+
private subscription;
|
|
19
|
+
private isJoined;
|
|
20
|
+
private heartbeatTimer?;
|
|
21
|
+
private pruneTimer?;
|
|
22
|
+
private readonly heartbeatInterval;
|
|
23
|
+
private readonly pruneInterval;
|
|
24
|
+
private readonly staleTtl;
|
|
25
|
+
constructor(nchan: NchanClient, currentUser: PresenceMessage, options?: LobbyOptions);
|
|
26
|
+
/**
|
|
27
|
+
* Initializes the lobby by subscribing to presence events and broadcasting "join".
|
|
28
|
+
*/
|
|
29
|
+
join(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Pauses the heartbeat timer (e.g. when tab is hidden).
|
|
32
|
+
*/
|
|
33
|
+
pauseHeartbeat(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Resumes the heartbeat timer (e.g. when tab becomes visible).
|
|
36
|
+
*/
|
|
37
|
+
resumeHeartbeat(): void;
|
|
38
|
+
private startHeartbeat;
|
|
39
|
+
private stopHeartbeat;
|
|
40
|
+
private startPruning;
|
|
41
|
+
private stopPruning;
|
|
42
|
+
/**
|
|
43
|
+
* Emits the current list of online users whenever it changes.
|
|
44
|
+
*/
|
|
45
|
+
onUsersChange(callback: (users: PresenceMessage[]) => void): void;
|
|
46
|
+
/**
|
|
47
|
+
* Stop listening to user changes.
|
|
48
|
+
*/
|
|
49
|
+
offUsersChange(callback: (users: PresenceMessage[]) => void): void;
|
|
50
|
+
/**
|
|
51
|
+
* Allows updating the current user's status (e.g. name or playing state).
|
|
52
|
+
*/
|
|
53
|
+
updatePresence(update: Partial<PresenceMessage>): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Challenge another user to a game.
|
|
56
|
+
* Returns the ID of the table created for the challenge.
|
|
57
|
+
*/
|
|
58
|
+
challenge(userId: string, ruleType: string): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Accept an incoming challenge.
|
|
61
|
+
* Returns the Table instance for the accepted game.
|
|
62
|
+
*/
|
|
63
|
+
acceptChallenge(userId: string, ruleType: string, tableId: string): Promise<Table>;
|
|
64
|
+
/**
|
|
65
|
+
* Decline an incoming challenge.
|
|
66
|
+
*/
|
|
67
|
+
declineChallenge(userId: string, ruleType: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Cancel an outgoing challenge.
|
|
70
|
+
*/
|
|
71
|
+
cancelChallenge(userId: string, ruleType: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to incoming challenges directed at the current user.
|
|
74
|
+
*/
|
|
75
|
+
onChallenge(callback: (challenge: ChallengeMessage) => void): void;
|
|
76
|
+
/**
|
|
77
|
+
* Gracefully leaves the lobby.
|
|
78
|
+
*/
|
|
79
|
+
leave(options?: {
|
|
80
|
+
isTeardown?: boolean;
|
|
81
|
+
}): Promise<void>;
|
|
82
|
+
private handleIncomingMessage;
|
|
83
|
+
private handlePresenceUpdate;
|
|
84
|
+
private handleChallenge;
|
|
85
|
+
private notifyListeners;
|
|
86
|
+
private getUsersList;
|
|
87
|
+
}
|
package/dist/lobby.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { parseMessage } from "./types";
|
|
2
|
+
import { Table } from "./table";
|
|
3
|
+
import { getUID } from "./utils/uid";
|
|
4
|
+
/**
|
|
5
|
+
* Manages the global lobby state, including real-time presence tracking and challenge flows.
|
|
6
|
+
*/
|
|
7
|
+
export class Lobby {
|
|
8
|
+
nchan;
|
|
9
|
+
currentUser;
|
|
10
|
+
users = new Map();
|
|
11
|
+
listeners = [];
|
|
12
|
+
challengeListeners = [];
|
|
13
|
+
subscription = null;
|
|
14
|
+
isJoined = false;
|
|
15
|
+
heartbeatTimer;
|
|
16
|
+
pruneTimer;
|
|
17
|
+
heartbeatInterval;
|
|
18
|
+
pruneInterval;
|
|
19
|
+
staleTtl;
|
|
20
|
+
constructor(nchan, currentUser, options = {}) {
|
|
21
|
+
this.nchan = nchan;
|
|
22
|
+
this.currentUser = currentUser;
|
|
23
|
+
this.heartbeatInterval = options.heartbeatInterval || 30000;
|
|
24
|
+
this.pruneInterval = options.pruneInterval || 10000;
|
|
25
|
+
this.staleTtl = options.staleTtl || 90000;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Initializes the lobby by subscribing to presence events and broadcasting "join".
|
|
29
|
+
*/
|
|
30
|
+
async join() {
|
|
31
|
+
if (this.isJoined)
|
|
32
|
+
return;
|
|
33
|
+
this.subscription = this.nchan.subscribePresence((data) => {
|
|
34
|
+
this.handleIncomingMessage(data);
|
|
35
|
+
});
|
|
36
|
+
await this.subscription.ready;
|
|
37
|
+
// Broadcast our own presence
|
|
38
|
+
await this.nchan.publishPresence(this.currentUser);
|
|
39
|
+
this.startHeartbeat();
|
|
40
|
+
this.startPruning();
|
|
41
|
+
this.isJoined = true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pauses the heartbeat timer (e.g. when tab is hidden).
|
|
45
|
+
*/
|
|
46
|
+
pauseHeartbeat() {
|
|
47
|
+
this.stopHeartbeat();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resumes the heartbeat timer (e.g. when tab becomes visible).
|
|
51
|
+
*/
|
|
52
|
+
resumeHeartbeat() {
|
|
53
|
+
this.startHeartbeat();
|
|
54
|
+
}
|
|
55
|
+
startHeartbeat() {
|
|
56
|
+
this.stopHeartbeat();
|
|
57
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
58
|
+
try {
|
|
59
|
+
await this.nchan.publishPresence({
|
|
60
|
+
...this.currentUser,
|
|
61
|
+
type: "heartbeat",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (_e) {
|
|
65
|
+
console.error("Failed to send heartbeat:", _e);
|
|
66
|
+
}
|
|
67
|
+
}, this.heartbeatInterval);
|
|
68
|
+
}
|
|
69
|
+
stopHeartbeat() {
|
|
70
|
+
if (this.heartbeatTimer) {
|
|
71
|
+
clearInterval(this.heartbeatTimer);
|
|
72
|
+
this.heartbeatTimer = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
startPruning() {
|
|
76
|
+
this.stopPruning();
|
|
77
|
+
this.pruneTimer = setInterval(() => {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const [userId, user] of this.users.entries()) {
|
|
81
|
+
if (userId === this.currentUser.userId)
|
|
82
|
+
continue;
|
|
83
|
+
// Use lastSeen (ms) or fall back to current time if just joined
|
|
84
|
+
const lastSeen = user.lastSeen || now;
|
|
85
|
+
if (now - lastSeen > this.staleTtl) {
|
|
86
|
+
this.users.delete(userId);
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (changed) {
|
|
91
|
+
this.notifyListeners();
|
|
92
|
+
}
|
|
93
|
+
}, this.pruneInterval);
|
|
94
|
+
}
|
|
95
|
+
stopPruning() {
|
|
96
|
+
if (this.pruneTimer) {
|
|
97
|
+
clearInterval(this.pruneTimer);
|
|
98
|
+
this.pruneTimer = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Emits the current list of online users whenever it changes.
|
|
103
|
+
*/
|
|
104
|
+
onUsersChange(callback) {
|
|
105
|
+
this.listeners.push(callback);
|
|
106
|
+
// Immediate emit of current state to the new listener
|
|
107
|
+
callback(this.getUsersList());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Stop listening to user changes.
|
|
111
|
+
*/
|
|
112
|
+
offUsersChange(callback) {
|
|
113
|
+
this.listeners = this.listeners.filter((l) => l !== callback);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Allows updating the current user's status (e.g. name or playing state).
|
|
117
|
+
*/
|
|
118
|
+
async updatePresence(update) {
|
|
119
|
+
this.currentUser = { ...this.currentUser, ...update };
|
|
120
|
+
await this.nchan.publishPresence(this.currentUser);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Challenge another user to a game.
|
|
124
|
+
* Returns the ID of the table created for the challenge.
|
|
125
|
+
*/
|
|
126
|
+
async challenge(userId, ruleType) {
|
|
127
|
+
const tableId = getUID();
|
|
128
|
+
await this.nchan.publishChallenge({
|
|
129
|
+
type: "offer",
|
|
130
|
+
challengerId: this.currentUser.userId,
|
|
131
|
+
challengerName: this.currentUser.userName,
|
|
132
|
+
recipientId: userId,
|
|
133
|
+
ruleType,
|
|
134
|
+
tableId,
|
|
135
|
+
});
|
|
136
|
+
return tableId;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Accept an incoming challenge.
|
|
140
|
+
* Returns the Table instance for the accepted game.
|
|
141
|
+
*/
|
|
142
|
+
async acceptChallenge(userId, ruleType, tableId) {
|
|
143
|
+
await this.nchan.publishChallenge({
|
|
144
|
+
type: "accept",
|
|
145
|
+
challengerId: this.currentUser.userId,
|
|
146
|
+
challengerName: this.currentUser.userName,
|
|
147
|
+
recipientId: userId,
|
|
148
|
+
ruleType,
|
|
149
|
+
tableId,
|
|
150
|
+
});
|
|
151
|
+
// Automatically update our presence to show we've joined the table
|
|
152
|
+
await this.updatePresence({ tableId });
|
|
153
|
+
const table = new Table(this.nchan, tableId, this.currentUser.userId, this);
|
|
154
|
+
await table.join();
|
|
155
|
+
return table;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Decline an incoming challenge.
|
|
159
|
+
*/
|
|
160
|
+
async declineChallenge(userId, ruleType) {
|
|
161
|
+
await this.nchan.publishChallenge({
|
|
162
|
+
type: "decline",
|
|
163
|
+
challengerId: this.currentUser.userId,
|
|
164
|
+
challengerName: this.currentUser.userName,
|
|
165
|
+
recipientId: userId,
|
|
166
|
+
ruleType,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Cancel an outgoing challenge.
|
|
171
|
+
*/
|
|
172
|
+
async cancelChallenge(userId, ruleType) {
|
|
173
|
+
await this.nchan.publishChallenge({
|
|
174
|
+
type: "cancel",
|
|
175
|
+
challengerId: this.currentUser.userId,
|
|
176
|
+
challengerName: this.currentUser.userName,
|
|
177
|
+
recipientId: userId,
|
|
178
|
+
ruleType,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Subscribe to incoming challenges directed at the current user.
|
|
183
|
+
*/
|
|
184
|
+
onChallenge(callback) {
|
|
185
|
+
this.challengeListeners.push(callback);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Gracefully leaves the lobby.
|
|
189
|
+
*/
|
|
190
|
+
async leave(options = {}) {
|
|
191
|
+
this.stopHeartbeat();
|
|
192
|
+
this.stopPruning();
|
|
193
|
+
this.subscription?.stop();
|
|
194
|
+
try {
|
|
195
|
+
await this.nchan.publishPresence({
|
|
196
|
+
...this.currentUser,
|
|
197
|
+
type: "leave",
|
|
198
|
+
}, { keepalive: options.isTeardown });
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
console.error("Error leaving lobby:", e);
|
|
202
|
+
}
|
|
203
|
+
this.users.clear();
|
|
204
|
+
this.notifyListeners();
|
|
205
|
+
this.isJoined = false;
|
|
206
|
+
}
|
|
207
|
+
handleIncomingMessage(data) {
|
|
208
|
+
const rawMsg = parseMessage(data);
|
|
209
|
+
if (!rawMsg)
|
|
210
|
+
return;
|
|
211
|
+
if (rawMsg.messageType === "presence") {
|
|
212
|
+
this.handlePresenceUpdate(rawMsg);
|
|
213
|
+
}
|
|
214
|
+
else if (rawMsg.messageType === "challenge") {
|
|
215
|
+
this.handleChallenge(rawMsg);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
handlePresenceUpdate(msg) {
|
|
219
|
+
if (msg.type === "leave") {
|
|
220
|
+
this.users.delete(msg.userId);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Use local time for real-time pruning to avoid clock skew
|
|
224
|
+
msg.lastSeen = Date.now();
|
|
225
|
+
this.users.set(msg.userId, msg);
|
|
226
|
+
}
|
|
227
|
+
this.notifyListeners();
|
|
228
|
+
}
|
|
229
|
+
handleChallenge(msg) {
|
|
230
|
+
// Filter messages directed at us (or broadcasted ones)
|
|
231
|
+
if (msg.recipientId === this.currentUser.userId) {
|
|
232
|
+
this.challengeListeners.forEach((cb) => cb(msg));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
notifyListeners() {
|
|
236
|
+
const list = this.getUsersList();
|
|
237
|
+
this.listeners.forEach((cb) => cb(list));
|
|
238
|
+
}
|
|
239
|
+
getUsersList() {
|
|
240
|
+
return Array.from(this.users.values());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=lobby.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lobby.js","sourceRoot":"","sources":["../src/lobby.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Lobby, LobbyOptions } from "./lobby";
|
|
2
|
+
import { Table } from "./table";
|
|
3
|
+
import { PresenceMessage } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* The main messaging client library entry point.
|
|
6
|
+
* Encapsulates transport logic and provides access to lobby and table functionality.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MessagingClient {
|
|
9
|
+
private nchan;
|
|
10
|
+
private activeLobbies;
|
|
11
|
+
private activeTables;
|
|
12
|
+
private lastLobbyConfig?;
|
|
13
|
+
private isStopping;
|
|
14
|
+
constructor(options: {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Initializes the client and ensures connection readiness.
|
|
19
|
+
* In browser environments, attaches lifecycle event listeners.
|
|
20
|
+
*/
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Stops all active connections and cleans up.
|
|
24
|
+
*/
|
|
25
|
+
stop(options?: {
|
|
26
|
+
isTeardown?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Enters the global lobby for presence broadcasting and tracking.
|
|
30
|
+
*/
|
|
31
|
+
joinLobby(user: PresenceMessage, options?: LobbyOptions): Promise<Lobby>;
|
|
32
|
+
/**
|
|
33
|
+
* Joins a specific table for communication.
|
|
34
|
+
*/
|
|
35
|
+
joinTable<T = any>(tableId: string, userId: string): Promise<Table<T>>;
|
|
36
|
+
private handlePageHide;
|
|
37
|
+
private handlePageShow;
|
|
38
|
+
private handleVisibilityChange;
|
|
39
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { NchanClient } from "./nchanclient";
|
|
2
|
+
import { Lobby } from "./lobby";
|
|
3
|
+
import { Table } from "./table";
|
|
4
|
+
/**
|
|
5
|
+
* The main messaging client library entry point.
|
|
6
|
+
* Encapsulates transport logic and provides access to lobby and table functionality.
|
|
7
|
+
*/
|
|
8
|
+
export class MessagingClient {
|
|
9
|
+
nchan;
|
|
10
|
+
activeLobbies = [];
|
|
11
|
+
activeTables = [];
|
|
12
|
+
lastLobbyConfig;
|
|
13
|
+
isStopping = false;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.nchan = new NchanClient(options.baseUrl);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initializes the client and ensures connection readiness.
|
|
19
|
+
* In browser environments, attaches lifecycle event listeners.
|
|
20
|
+
*/
|
|
21
|
+
async start() {
|
|
22
|
+
if (typeof window !== "undefined") {
|
|
23
|
+
window.addEventListener("pagehide", this.handlePageHide);
|
|
24
|
+
window.addEventListener("pageshow", this.handlePageShow);
|
|
25
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stops all active connections and cleans up.
|
|
30
|
+
*/
|
|
31
|
+
async stop(options = {}) {
|
|
32
|
+
if (this.isStopping)
|
|
33
|
+
return;
|
|
34
|
+
this.isStopping = true;
|
|
35
|
+
try {
|
|
36
|
+
if (typeof window !== "undefined") {
|
|
37
|
+
window.removeEventListener("pagehide", this.handlePageHide);
|
|
38
|
+
window.removeEventListener("pageshow", this.handlePageShow);
|
|
39
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
40
|
+
}
|
|
41
|
+
const lobbies = [...this.activeLobbies];
|
|
42
|
+
this.activeLobbies = [];
|
|
43
|
+
await Promise.all(lobbies.map((lobby) => lobby.leave(options)));
|
|
44
|
+
const tables = [...this.activeTables];
|
|
45
|
+
this.activeTables = [];
|
|
46
|
+
// Use for loop to await each table leave
|
|
47
|
+
for (const table of tables) {
|
|
48
|
+
await table.leave(options);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
this.isStopping = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Enters the global lobby for presence broadcasting and tracking.
|
|
57
|
+
*/
|
|
58
|
+
async joinLobby(user, options) {
|
|
59
|
+
// Prevent duplicate joins if already in a lobby for this user
|
|
60
|
+
const existing = this.activeLobbies.find((l) => l.currentUser.userId === user.userId);
|
|
61
|
+
if (existing)
|
|
62
|
+
return existing;
|
|
63
|
+
this.lastLobbyConfig = { user, options };
|
|
64
|
+
const lobby = new Lobby(this.nchan, user, options);
|
|
65
|
+
await lobby.join();
|
|
66
|
+
this.activeLobbies.push(lobby);
|
|
67
|
+
return lobby;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Joins a specific table for communication.
|
|
71
|
+
*/
|
|
72
|
+
async joinTable(tableId, userId) {
|
|
73
|
+
let table = this.activeTables.find((t) => t.tableId === tableId);
|
|
74
|
+
if (!table) {
|
|
75
|
+
const lobby = this.activeLobbies.find((l) => l.currentUser.userId === userId);
|
|
76
|
+
if (!lobby) {
|
|
77
|
+
throw new Error(`Cannot join table: No active lobby found for user ${userId}`);
|
|
78
|
+
}
|
|
79
|
+
table = new Table(this.nchan, tableId, userId, lobby);
|
|
80
|
+
await table.join();
|
|
81
|
+
this.activeTables.push(table);
|
|
82
|
+
await lobby.updatePresence({ tableId });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await table.join();
|
|
86
|
+
}
|
|
87
|
+
return table;
|
|
88
|
+
}
|
|
89
|
+
handlePageHide = () => {
|
|
90
|
+
// Stop all connections on page hide (prevent ghosting)
|
|
91
|
+
// Non-blocking call because pagehide might terminate the process
|
|
92
|
+
this.stop({ isTeardown: true });
|
|
93
|
+
};
|
|
94
|
+
handlePageShow = (event) => {
|
|
95
|
+
// If returning via bfcache, restore connections
|
|
96
|
+
if (event.persisted && this.lastLobbyConfig) {
|
|
97
|
+
this.joinLobby(this.lastLobbyConfig.user, this.lastLobbyConfig.options);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
handleVisibilityChange = () => {
|
|
101
|
+
if (document.hidden) {
|
|
102
|
+
this.activeLobbies.forEach((l) => l.pauseHeartbeat());
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.activeLobbies.forEach((l) => l.resumeHeartbeat());
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=messagingclient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messagingclient.js","sourceRoot":"","sources":["../src/messagingclient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PresenceMessage, ChallengeMessage, TableMessage } from "./types";
|
|
2
|
+
export type Subscription = {
|
|
3
|
+
stop: () => void;
|
|
4
|
+
ready: Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export declare class NchanClient {
|
|
7
|
+
private server;
|
|
8
|
+
constructor(server: string);
|
|
9
|
+
private getWsUrl;
|
|
10
|
+
private getHttpUrl;
|
|
11
|
+
private publish;
|
|
12
|
+
publishPresence(message: Omit<PresenceMessage, "messageType">, options?: {
|
|
13
|
+
keepalive?: boolean;
|
|
14
|
+
}): Promise<Response>;
|
|
15
|
+
publishChallenge(message: Omit<ChallengeMessage, "messageType">, options?: {
|
|
16
|
+
keepalive?: boolean;
|
|
17
|
+
}): Promise<Response>;
|
|
18
|
+
publishTable<T>(tableId: string, message: Omit<TableMessage<T>, "senderId">, senderId: string, options?: {
|
|
19
|
+
keepalive?: boolean;
|
|
20
|
+
}): Promise<Response>;
|
|
21
|
+
subscribePresence(onMessage: (data: string) => void): Subscription;
|
|
22
|
+
subscribeTable(tableId: string, onMessage: (data: string) => void): Subscription;
|
|
23
|
+
private subscribe;
|
|
24
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export class NchanClient {
|
|
2
|
+
server;
|
|
3
|
+
constructor(server) {
|
|
4
|
+
// Ensure server string doesn't end with a slash and starts with protocol if missing
|
|
5
|
+
this.server = server.replace(/\/$/, "");
|
|
6
|
+
if (!this.server.startsWith("http")) {
|
|
7
|
+
this.server = `http://${this.server}`;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
getWsUrl(path) {
|
|
11
|
+
return this.server.replace(/^http/, "ws") + path;
|
|
12
|
+
}
|
|
13
|
+
getHttpUrl(path) {
|
|
14
|
+
return this.server + path;
|
|
15
|
+
}
|
|
16
|
+
async publish(path, message, options = {}) {
|
|
17
|
+
const url = this.getHttpUrl(path);
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify(message),
|
|
22
|
+
keepalive: options.keepalive,
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`Publish failed: ${response.status}`);
|
|
26
|
+
}
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
29
|
+
// Publishing
|
|
30
|
+
async publishPresence(message, options) {
|
|
31
|
+
return this.publish("/publish/presence/lobby", {
|
|
32
|
+
...message,
|
|
33
|
+
messageType: "presence",
|
|
34
|
+
}, options);
|
|
35
|
+
}
|
|
36
|
+
async publishChallenge(message, options) {
|
|
37
|
+
return this.publish("/publish/presence/lobby", {
|
|
38
|
+
...message,
|
|
39
|
+
messageType: "challenge",
|
|
40
|
+
}, options);
|
|
41
|
+
}
|
|
42
|
+
async publishTable(tableId, message, senderId, options) {
|
|
43
|
+
return this.publish(`/publish/table/${tableId}`, {
|
|
44
|
+
...message,
|
|
45
|
+
senderId,
|
|
46
|
+
}, options);
|
|
47
|
+
}
|
|
48
|
+
// Subscribing
|
|
49
|
+
subscribePresence(onMessage) {
|
|
50
|
+
return this.subscribe("/subscribe/presence/lobby", onMessage);
|
|
51
|
+
}
|
|
52
|
+
subscribeTable(tableId, onMessage) {
|
|
53
|
+
return this.subscribe(`/subscribe/table/${tableId}`, onMessage);
|
|
54
|
+
}
|
|
55
|
+
subscribe(path, onMessage) {
|
|
56
|
+
const url = this.getWsUrl(path);
|
|
57
|
+
let ws = null;
|
|
58
|
+
let stopped = false;
|
|
59
|
+
let reconnectAttempts = 0;
|
|
60
|
+
const maxReconnectDelay = 30000;
|
|
61
|
+
let reconnectTimer = null;
|
|
62
|
+
let resolveReady;
|
|
63
|
+
const ready = new Promise((r) => {
|
|
64
|
+
resolveReady = r;
|
|
65
|
+
});
|
|
66
|
+
const connect = () => {
|
|
67
|
+
if (stopped)
|
|
68
|
+
return;
|
|
69
|
+
if (ws && ws.readyState <= WebSocket.OPEN) {
|
|
70
|
+
resolveReady();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
ws = new globalThis.WebSocket(url);
|
|
74
|
+
ws.onmessage = (event) => {
|
|
75
|
+
onMessage(event.data);
|
|
76
|
+
};
|
|
77
|
+
ws.onopen = () => {
|
|
78
|
+
reconnectAttempts = 0;
|
|
79
|
+
resolveReady();
|
|
80
|
+
};
|
|
81
|
+
ws.onclose = () => {
|
|
82
|
+
if (!stopped) {
|
|
83
|
+
const delay = Math.min(Math.pow(2, reconnectAttempts) * 1000, maxReconnectDelay);
|
|
84
|
+
reconnectAttempts++;
|
|
85
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
ws.onerror = () => {
|
|
89
|
+
ws?.close();
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
connect();
|
|
93
|
+
return {
|
|
94
|
+
ready,
|
|
95
|
+
stop: () => {
|
|
96
|
+
stopped = true;
|
|
97
|
+
if (reconnectTimer) {
|
|
98
|
+
clearTimeout(reconnectTimer);
|
|
99
|
+
reconnectTimer = null;
|
|
100
|
+
}
|
|
101
|
+
if (ws) {
|
|
102
|
+
ws.close();
|
|
103
|
+
ws = null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=nchanclient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nchanclient.js","sourceRoot":"","sources":["../src/nchanclient.ts"],"names":[],"mappings":"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"}
|
package/dist/table.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NchanClient } from "./nchanclient";
|
|
2
|
+
import { TableMessage, PresenceMessage } from "./types";
|
|
3
|
+
import { Lobby } from "./lobby";
|
|
4
|
+
/**
|
|
5
|
+
* Represents a specific communication channel for a 2-player/spectator scenario at a table.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Table<T = any> {
|
|
8
|
+
private nchan;
|
|
9
|
+
readonly tableId: string;
|
|
10
|
+
private userId;
|
|
11
|
+
private lobby?;
|
|
12
|
+
private subscription;
|
|
13
|
+
private isJoined;
|
|
14
|
+
private messageListeners;
|
|
15
|
+
private spectatorListeners;
|
|
16
|
+
private opponentLeftListeners;
|
|
17
|
+
private lobbyUnsubscribe?;
|
|
18
|
+
opponentLeft: boolean;
|
|
19
|
+
private opponentSeen;
|
|
20
|
+
constructor(nchan: NchanClient, tableId: string, userId: string, lobby?: Lobby | undefined);
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the table by subscribing to its specific channel.
|
|
23
|
+
*/
|
|
24
|
+
join(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Broadcast an event to all participants at the table.
|
|
27
|
+
*/
|
|
28
|
+
publish(type: string, data: T): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to events published by other participants.
|
|
31
|
+
*/
|
|
32
|
+
onMessage(callback: (event: TableMessage<T>) => void): void;
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to opponent departure (explicit leave or timeout).
|
|
35
|
+
*/
|
|
36
|
+
onOpponentLeft(callback: () => void): void;
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to changes in the spectator list.
|
|
39
|
+
* Note: In a real implementation, this would track presence messages on the table channel.
|
|
40
|
+
*/
|
|
41
|
+
onSpectatorChange(callback: (spectators: PresenceMessage[]) => void): void;
|
|
42
|
+
/**
|
|
43
|
+
* Leave the table and stop all subscriptions.
|
|
44
|
+
*/
|
|
45
|
+
leave(options?: {
|
|
46
|
+
isTeardown?: boolean;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
private handleIncomingMessage;
|
|
49
|
+
private handleLobbyUsersChange;
|
|
50
|
+
private notifyOpponentLeft;
|
|
51
|
+
}
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { parseMessage } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Represents a specific communication channel for a 2-player/spectator scenario at a table.
|
|
4
|
+
*/
|
|
5
|
+
export class Table {
|
|
6
|
+
nchan;
|
|
7
|
+
tableId;
|
|
8
|
+
userId;
|
|
9
|
+
lobby;
|
|
10
|
+
subscription = null;
|
|
11
|
+
isJoined = false;
|
|
12
|
+
messageListeners = [];
|
|
13
|
+
spectatorListeners = [];
|
|
14
|
+
opponentLeftListeners = [];
|
|
15
|
+
lobbyUnsubscribe;
|
|
16
|
+
opponentLeft = false;
|
|
17
|
+
opponentSeen = false;
|
|
18
|
+
constructor(nchan, tableId, userId, lobby) {
|
|
19
|
+
this.nchan = nchan;
|
|
20
|
+
this.tableId = tableId;
|
|
21
|
+
this.userId = userId;
|
|
22
|
+
this.lobby = lobby;
|
|
23
|
+
if (this.lobby) {
|
|
24
|
+
const handler = (users) => this.handleLobbyUsersChange(users);
|
|
25
|
+
this.lobby.onUsersChange(handler);
|
|
26
|
+
this.lobbyUnsubscribe = () => {
|
|
27
|
+
this.lobby?.offUsersChange(handler);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Initializes the table by subscribing to its specific channel.
|
|
33
|
+
*/
|
|
34
|
+
async join() {
|
|
35
|
+
if (this.isJoined)
|
|
36
|
+
return;
|
|
37
|
+
this.subscription = this.nchan.subscribeTable(this.tableId, (data) => {
|
|
38
|
+
this.handleIncomingMessage(data);
|
|
39
|
+
});
|
|
40
|
+
await this.subscription.ready;
|
|
41
|
+
this.isJoined = true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Broadcast an event to all participants at the table.
|
|
45
|
+
*/
|
|
46
|
+
async publish(type, data) {
|
|
47
|
+
await this.nchan.publishTable(this.tableId, { type, data }, this.userId);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Subscribe to events published by other participants.
|
|
51
|
+
*/
|
|
52
|
+
onMessage(callback) {
|
|
53
|
+
this.messageListeners.push(callback);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to opponent departure (explicit leave or timeout).
|
|
57
|
+
*/
|
|
58
|
+
onOpponentLeft(callback) {
|
|
59
|
+
this.opponentLeftListeners.push(callback);
|
|
60
|
+
if (this.opponentLeft) {
|
|
61
|
+
callback();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Subscribe to changes in the spectator list.
|
|
66
|
+
* Note: In a real implementation, this would track presence messages on the table channel.
|
|
67
|
+
*/
|
|
68
|
+
onSpectatorChange(callback) {
|
|
69
|
+
this.spectatorListeners.push(callback);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Leave the table and stop all subscriptions.
|
|
73
|
+
*/
|
|
74
|
+
async leave(options = {}) {
|
|
75
|
+
try {
|
|
76
|
+
// Explicitly notify the opponent we are leaving
|
|
77
|
+
await this.nchan.publishTable(this.tableId, { type: "SYSTEM_DISCONNECT", data: {} }, this.userId, { keepalive: options.isTeardown });
|
|
78
|
+
if (!options.isTeardown) {
|
|
79
|
+
// Small delay to ensure the message is dispatched before closing the socket
|
|
80
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error("Error leaving table:", e);
|
|
85
|
+
}
|
|
86
|
+
// Clear lobby presence if we have one
|
|
87
|
+
if (this.lobby) {
|
|
88
|
+
await this.lobby.updatePresence({ tableId: undefined });
|
|
89
|
+
}
|
|
90
|
+
this.subscription?.stop();
|
|
91
|
+
this.messageListeners = [];
|
|
92
|
+
this.spectatorListeners = [];
|
|
93
|
+
this.opponentLeftListeners = [];
|
|
94
|
+
this.lobbyUnsubscribe?.();
|
|
95
|
+
this.isJoined = false;
|
|
96
|
+
}
|
|
97
|
+
handleIncomingMessage(data) {
|
|
98
|
+
const msg = parseMessage(data);
|
|
99
|
+
if (!msg || !msg.type)
|
|
100
|
+
return;
|
|
101
|
+
// Handle system messages internally
|
|
102
|
+
if (msg.type === "SYSTEM_DISCONNECT" && msg.senderId !== this.userId) {
|
|
103
|
+
this.notifyOpponentLeft();
|
|
104
|
+
}
|
|
105
|
+
// Notify message listeners
|
|
106
|
+
this.messageListeners.forEach((cb) => cb(msg));
|
|
107
|
+
}
|
|
108
|
+
handleLobbyUsersChange(users) {
|
|
109
|
+
const playersAtThisTable = users.filter((u) => u.tableId === this.tableId);
|
|
110
|
+
const opponent = playersAtThisTable.find((u) => u.userId !== this.userId);
|
|
111
|
+
if (opponent) {
|
|
112
|
+
this.opponentSeen = true;
|
|
113
|
+
}
|
|
114
|
+
// Watchdog trigger: Opponent was here, but now is gone.
|
|
115
|
+
if (this.opponentSeen && !opponent) {
|
|
116
|
+
this.notifyOpponentLeft();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
notifyOpponentLeft() {
|
|
120
|
+
if (this.opponentLeft)
|
|
121
|
+
return; // Only notify once
|
|
122
|
+
this.opponentLeft = true;
|
|
123
|
+
this.opponentLeftListeners.forEach((cb) => cb());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.js","sourceRoot":"","sources":["../src/table.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-enriched metadata added to all messages by Nchan.
|
|
3
|
+
* This is the absolute source of truth for timing and origin.
|
|
4
|
+
*/
|
|
5
|
+
export interface _Meta {
|
|
6
|
+
ts: string;
|
|
7
|
+
locale: string;
|
|
8
|
+
ua: string;
|
|
9
|
+
ip: string;
|
|
10
|
+
origin: string;
|
|
11
|
+
host: string;
|
|
12
|
+
path: string;
|
|
13
|
+
method: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Seek object for table seeking in the lobby
|
|
17
|
+
*/
|
|
18
|
+
export interface Seek {
|
|
19
|
+
tableId: string;
|
|
20
|
+
ruleType?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Presence-related messages (user join/leave/heartbeat)
|
|
24
|
+
*/
|
|
25
|
+
export interface PresenceMessage {
|
|
26
|
+
messageType: "presence";
|
|
27
|
+
type: "join" | "heartbeat" | "leave";
|
|
28
|
+
userId: string;
|
|
29
|
+
userName: string;
|
|
30
|
+
ruleType?: string;
|
|
31
|
+
opponentId?: string | null;
|
|
32
|
+
seek?: Seek;
|
|
33
|
+
lastSeen?: number;
|
|
34
|
+
_meta?: _Meta;
|
|
35
|
+
tableId?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Peer-to-peer challenge request
|
|
39
|
+
*/
|
|
40
|
+
export interface ChallengeMessage {
|
|
41
|
+
messageType: "challenge";
|
|
42
|
+
type: "offer" | "accept" | "decline" | "cancel";
|
|
43
|
+
challengerId: string;
|
|
44
|
+
challengerName: string;
|
|
45
|
+
recipientId: string;
|
|
46
|
+
ruleType: string;
|
|
47
|
+
tableId?: string;
|
|
48
|
+
_meta?: _Meta;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Generic structure for table/game events
|
|
52
|
+
*/
|
|
53
|
+
export interface TableMessage<T = any> {
|
|
54
|
+
type: string;
|
|
55
|
+
senderId: string;
|
|
56
|
+
data: T;
|
|
57
|
+
_meta?: _Meta;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Lobby-level information about an active game table
|
|
61
|
+
*/
|
|
62
|
+
export interface TableInfo {
|
|
63
|
+
tableId: string;
|
|
64
|
+
ruleType: string;
|
|
65
|
+
players: {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
}[];
|
|
69
|
+
spectatorCount: number;
|
|
70
|
+
status: "waiting" | "playing" | "finished";
|
|
71
|
+
createdAt: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Union type for messages received via the lobby channel
|
|
75
|
+
*/
|
|
76
|
+
export type LobbyIncomingMessage = PresenceMessage | ChallengeMessage;
|
|
77
|
+
/**
|
|
78
|
+
* Type guards
|
|
79
|
+
*/
|
|
80
|
+
export declare function isPresenceMessage(msg: any): msg is PresenceMessage;
|
|
81
|
+
export declare function isChallengeMessage(msg: any): msg is ChallengeMessage;
|
|
82
|
+
/**
|
|
83
|
+
* Helper to parse incoming Nchan JSON strings
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseMessage<T>(data: string): T | null;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guards
|
|
3
|
+
*/
|
|
4
|
+
export function isPresenceMessage(msg) {
|
|
5
|
+
return msg?.messageType === "presence";
|
|
6
|
+
}
|
|
7
|
+
export function isChallengeMessage(msg) {
|
|
8
|
+
return msg?.messageType === "challenge";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Helper to parse incoming Nchan JSON strings
|
|
12
|
+
*/
|
|
13
|
+
export function parseMessage(data) {
|
|
14
|
+
if (!data || data.trim() === "")
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(data);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
console.error("Failed to parse Nchan message:", e);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAiFA;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAQ;IACxC,OAAO,GAAG,EAAE,WAAW,KAAK,UAAU,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAQ;IACzC,OAAO,GAAG,EAAE,WAAW,KAAK,WAAW,CAAC;AAC1C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAI,IAAY;IAC1C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;IAC/B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,CAAC,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getUID(): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uid.js","sourceRoot":"","sources":["../../src/utils/uid.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,MAAM;IACpB,OAAO,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;AACrF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tailuge/messaging",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A stateful messaging library for Nchan-powered real-time applications.",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "jest --config test/jest.config.cjs",
|
|
19
|
+
"coverage": "jest --config test/jest.config.cjs --coverage",
|
|
20
|
+
"playwright": "npx playwright test playwright/debug-connection.spec.ts",
|
|
21
|
+
"test:debug": "npm run playwright",
|
|
22
|
+
"lint": "tsc --noEmit && npx oxlint src test",
|
|
23
|
+
"prettify": "npx oxfmt src test",
|
|
24
|
+
"build": "tsc --declaration",
|
|
25
|
+
"release": "npm version minor --no-git-tag-version && npm run build",
|
|
26
|
+
"build:all": "bash scripts/docker-nchan.sh build",
|
|
27
|
+
"build:example": "npx esbuild example/src/client.ts --bundle --outfile=example/dist/client.js",
|
|
28
|
+
"example": "npm run build:example && npx http-server example -p 3000",
|
|
29
|
+
"docker:start": "bash scripts/docker-nchan.sh",
|
|
30
|
+
"docker:stop": "bash scripts/docker-nchan.sh stop",
|
|
31
|
+
"docker:build": "npm run build:all",
|
|
32
|
+
"deps": "npx npm-check"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"messaging",
|
|
36
|
+
"nchan",
|
|
37
|
+
"real-time",
|
|
38
|
+
"presence",
|
|
39
|
+
"lobby"
|
|
40
|
+
],
|
|
41
|
+
"author": "tailuge",
|
|
42
|
+
"license": "GPL-3.0",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@playwright/test": "1.58.2",
|
|
45
|
+
"@types/jest": "30.0.0",
|
|
46
|
+
"@types/ws": "^8.18.1",
|
|
47
|
+
"esbuild": "^0.27.3",
|
|
48
|
+
"http-server": "^14.1.1",
|
|
49
|
+
"jest": "30.3.0",
|
|
50
|
+
"oxlint": "^1.52.0",
|
|
51
|
+
"testcontainers": "^11.12.0",
|
|
52
|
+
"ts-jest": "^29.4.6",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"ws": "^8.19.0"
|
|
55
|
+
},
|
|
56
|
+
"browserslist": [
|
|
57
|
+
"defaults",
|
|
58
|
+
"ios >= 12",
|
|
59
|
+
"safari >= 12"
|
|
60
|
+
]
|
|
61
|
+
}
|