@signe/room 2.9.4 → 3.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/CHANGELOG.md +13 -0
- package/dist/chunk-EUXUH3YW.js +15 -0
- package/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/cloudflare/index.d.ts +71 -0
- package/dist/cloudflare/index.js +320 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/index.d.ts +65 -188
- package/dist/index.js +742 -146
- package/dist/index.js.map +1 -1
- package/dist/node/index.d.ts +164 -0
- package/dist/node/index.js +786 -0
- package/dist/node/index.js.map +1 -0
- package/dist/party-dNs-hqkq.d.ts +175 -0
- package/examples/cloudflare/README.md +62 -0
- package/examples/cloudflare/node_modules/.bin/tsc +17 -0
- package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
- package/examples/cloudflare/package.json +24 -0
- package/examples/cloudflare/public/index.html +443 -0
- package/examples/cloudflare/src/index.ts +28 -0
- package/examples/cloudflare/src/room.ts +44 -0
- package/examples/cloudflare/tsconfig.json +10 -0
- package/examples/cloudflare/wrangler.jsonc +25 -0
- package/examples/node/README.md +57 -0
- package/examples/node/node_modules/.bin/tsc +17 -0
- package/examples/node/node_modules/.bin/tsserver +17 -0
- package/examples/node/node_modules/.bin/tsx +17 -0
- package/examples/node/package.json +23 -0
- package/examples/node/public/index.html +443 -0
- package/examples/node/room.ts +44 -0
- package/examples/node/server.sqlite.ts +52 -0
- package/examples/node/server.ts +51 -0
- package/examples/node/tsconfig.json +10 -0
- package/examples/node-game/README.md +66 -0
- package/examples/node-game/package.json +23 -0
- package/examples/node-game/public/index.html +705 -0
- package/examples/node-game/room.ts +145 -0
- package/examples/node-game/server.sqlite.ts +54 -0
- package/examples/node-game/server.ts +53 -0
- package/examples/node-game/tsconfig.json +10 -0
- package/examples/node-shard/README.md +32 -0
- package/examples/node-shard/dev.ts +39 -0
- package/examples/node-shard/package.json +24 -0
- package/examples/node-shard/public/index.html +777 -0
- package/examples/node-shard/room-server.ts +68 -0
- package/examples/node-shard/room.ts +105 -0
- package/examples/node-shard/shared.ts +6 -0
- package/examples/node-shard/tsconfig.json +14 -0
- package/examples/node-shard/world-server.ts +169 -0
- package/package.json +14 -5
- package/readme.md +377 -11
- package/src/cloudflare/index.ts +474 -0
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +626 -90
- package/src/session.guard.ts +6 -2
- package/src/shard.ts +91 -23
- package/src/storage.ts +29 -5
- package/src/testing.ts +4 -3
- package/src/types/party.ts +4 -1
- package/src/world.guard.ts +23 -4
- package/src/world.ts +170 -79
- package/examples/game/.vscode/launch.json +0 -11
- package/examples/game/.vscode/settings.json +0 -11
- package/examples/game/README.md +0 -40
- package/examples/game/app/client.tsx +0 -15
- package/examples/game/app/components/Admin.tsx +0 -1089
- package/examples/game/app/components/Room.tsx +0 -162
- package/examples/game/app/styles.css +0 -31
- package/examples/game/package-lock.json +0 -225
- package/examples/game/package.json +0 -20
- package/examples/game/party/game.room.ts +0 -32
- package/examples/game/party/server.ts +0 -10
- package/examples/game/party/shard.ts +0 -5
- package/examples/game/partykit.json +0 -14
- package/examples/game/public/favicon.ico +0 -0
- package/examples/game/public/index.html +0 -27
- package/examples/game/public/normalize.css +0 -351
- package/examples/game/shared/room.schema.ts +0 -14
- package/examples/game/tsconfig.json +0 -109
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Signe Durable Object Room</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light;
|
|
10
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
background: #f5f7fa;
|
|
21
|
+
color: #17191c;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main {
|
|
25
|
+
width: min(960px, calc(100vw - 32px));
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
padding: 32px 0;
|
|
28
|
+
display: grid;
|
|
29
|
+
gap: 18px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
header {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: end;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
gap: 16px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h1,
|
|
40
|
+
h2 {
|
|
41
|
+
margin: 0;
|
|
42
|
+
line-height: 1.1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h1 {
|
|
46
|
+
font-size: 30px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
h2 {
|
|
50
|
+
font-size: 18px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.muted,
|
|
54
|
+
.status {
|
|
55
|
+
color: #5b6472;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.panel,
|
|
59
|
+
.card {
|
|
60
|
+
border: 1px solid #d9dde3;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
background: #ffffff;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.panel {
|
|
66
|
+
padding: 20px;
|
|
67
|
+
display: grid;
|
|
68
|
+
gap: 16px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.join {
|
|
72
|
+
max-width: 520px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
form {
|
|
76
|
+
display: grid;
|
|
77
|
+
gap: 12px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
label {
|
|
81
|
+
display: grid;
|
|
82
|
+
gap: 6px;
|
|
83
|
+
font-size: 14px;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
input {
|
|
88
|
+
min-height: 42px;
|
|
89
|
+
border: 1px solid #c9d0d9;
|
|
90
|
+
border-radius: 6px;
|
|
91
|
+
padding: 0 12px;
|
|
92
|
+
font: inherit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
button {
|
|
96
|
+
min-height: 40px;
|
|
97
|
+
border: 1px solid #17191c;
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
background: #17191c;
|
|
100
|
+
color: #ffffff;
|
|
101
|
+
padding: 0 14px;
|
|
102
|
+
font: inherit;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
button.secondary {
|
|
107
|
+
background: #ffffff;
|
|
108
|
+
color: #17191c;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.room {
|
|
112
|
+
display: grid;
|
|
113
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
114
|
+
gap: 18px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.count {
|
|
118
|
+
font-size: 72px;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
font-weight: 700;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.actions {
|
|
124
|
+
display: flex;
|
|
125
|
+
gap: 10px;
|
|
126
|
+
flex-wrap: wrap;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.session {
|
|
130
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.users {
|
|
135
|
+
padding: 16px;
|
|
136
|
+
display: grid;
|
|
137
|
+
gap: 12px;
|
|
138
|
+
align-content: start;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.user-list {
|
|
142
|
+
display: grid;
|
|
143
|
+
gap: 8px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.user {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
gap: 10px;
|
|
151
|
+
min-height: 40px;
|
|
152
|
+
padding: 8px 10px;
|
|
153
|
+
border: 1px solid #e1e5ea;
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.badge {
|
|
158
|
+
border-radius: 999px;
|
|
159
|
+
padding: 3px 8px;
|
|
160
|
+
font-size: 12px;
|
|
161
|
+
background: #edf1f5;
|
|
162
|
+
color: #4b5563;
|
|
163
|
+
white-space: nowrap;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.badge.online {
|
|
167
|
+
background: #e7f7ed;
|
|
168
|
+
color: #146c36;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.hidden {
|
|
172
|
+
display: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (max-width: 720px) {
|
|
176
|
+
header,
|
|
177
|
+
.room {
|
|
178
|
+
grid-template-columns: 1fr;
|
|
179
|
+
display: grid;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<main>
|
|
186
|
+
<header>
|
|
187
|
+
<div>
|
|
188
|
+
<h1>Signe Durable Object Room</h1>
|
|
189
|
+
<div class="muted" id="roomLabel">Choose a room</div>
|
|
190
|
+
</div>
|
|
191
|
+
<button id="leaveRoom" class="secondary hidden" type="button">Change room</button>
|
|
192
|
+
</header>
|
|
193
|
+
|
|
194
|
+
<section class="panel join" id="joinPanel">
|
|
195
|
+
<h2>Join a room</h2>
|
|
196
|
+
<form id="joinForm">
|
|
197
|
+
<label>
|
|
198
|
+
Room
|
|
199
|
+
<input id="roomInput" name="room" autocomplete="off" required value="demo" />
|
|
200
|
+
</label>
|
|
201
|
+
<label>
|
|
202
|
+
Name
|
|
203
|
+
<input id="nameInput" name="name" autocomplete="name" required />
|
|
204
|
+
</label>
|
|
205
|
+
<button type="submit">Enter room</button>
|
|
206
|
+
</form>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<section class="room hidden" id="roomPanel">
|
|
210
|
+
<div class="panel">
|
|
211
|
+
<h2>Counter</h2>
|
|
212
|
+
<div class="count" id="count">0</div>
|
|
213
|
+
<div class="actions">
|
|
214
|
+
<button id="increment" type="button">Increment</button>
|
|
215
|
+
<button id="reset" class="secondary" type="button">Reset</button>
|
|
216
|
+
<button id="resetSession" class="secondary" type="button">New session</button>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="status" id="status">Disconnected</div>
|
|
219
|
+
<div class="muted session" id="sessionLabel"></div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<aside class="card users">
|
|
223
|
+
<h2>Users</h2>
|
|
224
|
+
<div class="user-list" id="users"></div>
|
|
225
|
+
</aside>
|
|
226
|
+
</section>
|
|
227
|
+
</main>
|
|
228
|
+
|
|
229
|
+
<script>
|
|
230
|
+
const storagePrefix = "signe-cloudflare";
|
|
231
|
+
const state = {
|
|
232
|
+
socket: null,
|
|
233
|
+
roomId: getRoomIdFromPath(),
|
|
234
|
+
name: localStorage.getItem(`${storagePrefix}-name`) || "",
|
|
235
|
+
sessionId: localStorage.getItem(`${storagePrefix}-session-id`) || "",
|
|
236
|
+
users: {},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const joinPanel = document.querySelector("#joinPanel");
|
|
240
|
+
const roomPanel = document.querySelector("#roomPanel");
|
|
241
|
+
const roomLabel = document.querySelector("#roomLabel");
|
|
242
|
+
const leaveRoom = document.querySelector("#leaveRoom");
|
|
243
|
+
const joinForm = document.querySelector("#joinForm");
|
|
244
|
+
const roomInput = document.querySelector("#roomInput");
|
|
245
|
+
const nameInput = document.querySelector("#nameInput");
|
|
246
|
+
const count = document.querySelector("#count");
|
|
247
|
+
const status = document.querySelector("#status");
|
|
248
|
+
const sessionLabel = document.querySelector("#sessionLabel");
|
|
249
|
+
const users = document.querySelector("#users");
|
|
250
|
+
|
|
251
|
+
nameInput.value = state.name;
|
|
252
|
+
|
|
253
|
+
if (state.roomId) {
|
|
254
|
+
roomInput.value = state.roomId;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
joinForm.addEventListener("submit", (event) => {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
const roomId = normalizeRoomId(roomInput.value);
|
|
260
|
+
const name = nameInput.value.trim() || "Anonymous";
|
|
261
|
+
|
|
262
|
+
localStorage.setItem(`${storagePrefix}-name`, name);
|
|
263
|
+
history.pushState({}, "", `/rooms/${encodeURIComponent(roomId)}`);
|
|
264
|
+
enterRoom(roomId, name);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
leaveRoom.addEventListener("click", () => {
|
|
268
|
+
disconnect();
|
|
269
|
+
history.pushState({}, "", "/");
|
|
270
|
+
showJoin();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
window.addEventListener("popstate", () => {
|
|
274
|
+
const roomId = getRoomIdFromPath();
|
|
275
|
+
if (roomId) {
|
|
276
|
+
enterRoom(roomId, state.name || "Anonymous");
|
|
277
|
+
} else {
|
|
278
|
+
disconnect();
|
|
279
|
+
showJoin();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
document.querySelector("#increment").addEventListener("click", () => {
|
|
284
|
+
state.socket?.send(JSON.stringify({ action: "increment", value: { amount: 1 } }));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
document.querySelector("#reset").addEventListener("click", async () => {
|
|
288
|
+
if (!state.roomId) return;
|
|
289
|
+
await fetch(`/parties/main/${encodeURIComponent(state.roomId)}/reset`, { method: "POST" });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
document.querySelector("#resetSession").addEventListener("click", () => {
|
|
293
|
+
if (!state.roomId) return;
|
|
294
|
+
disconnect();
|
|
295
|
+
localStorage.removeItem(`${storagePrefix}-session-id`);
|
|
296
|
+
state.sessionId = "";
|
|
297
|
+
enterRoom(state.roomId, state.name || "Anonymous");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (state.roomId) {
|
|
301
|
+
enterRoom(state.roomId, state.name || "Anonymous");
|
|
302
|
+
} else {
|
|
303
|
+
showJoin();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function enterRoom(roomId, name) {
|
|
307
|
+
disconnect();
|
|
308
|
+
state.roomId = roomId;
|
|
309
|
+
state.name = name;
|
|
310
|
+
state.users = {};
|
|
311
|
+
count.textContent = "0";
|
|
312
|
+
renderUsers();
|
|
313
|
+
showRoom();
|
|
314
|
+
|
|
315
|
+
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
316
|
+
const params = new URLSearchParams({ name, id: getSessionId() });
|
|
317
|
+
state.socket = new WebSocket(`${protocol}://${location.host}/parties/main/${encodeURIComponent(roomId)}?${params}`);
|
|
318
|
+
|
|
319
|
+
state.socket.addEventListener("open", () => {
|
|
320
|
+
status.textContent = "Connected";
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
state.socket.addEventListener("close", () => {
|
|
324
|
+
status.textContent = "Disconnected";
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
state.socket.addEventListener("message", (event) => {
|
|
328
|
+
const packet = JSON.parse(event.data);
|
|
329
|
+
if (packet.type !== "sync" || !packet.value) return;
|
|
330
|
+
|
|
331
|
+
if (typeof packet.value.count === "number") {
|
|
332
|
+
count.textContent = String(packet.value.count);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (packet.value.users) {
|
|
336
|
+
mergeUsers(state.users, packet.value.users);
|
|
337
|
+
renderUsers();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function disconnect() {
|
|
343
|
+
if (state.socket) {
|
|
344
|
+
state.socket.close();
|
|
345
|
+
state.socket = null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function showJoin() {
|
|
350
|
+
state.roomId = null;
|
|
351
|
+
joinPanel.classList.remove("hidden");
|
|
352
|
+
roomPanel.classList.add("hidden");
|
|
353
|
+
leaveRoom.classList.add("hidden");
|
|
354
|
+
roomLabel.textContent = "Choose a room";
|
|
355
|
+
status.textContent = "Disconnected";
|
|
356
|
+
sessionLabel.textContent = "";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function showRoom() {
|
|
360
|
+
joinPanel.classList.add("hidden");
|
|
361
|
+
roomPanel.classList.remove("hidden");
|
|
362
|
+
leaveRoom.classList.remove("hidden");
|
|
363
|
+
roomLabel.textContent = `/rooms/${state.roomId}`;
|
|
364
|
+
sessionLabel.textContent = `session ${getSessionId().slice(0, 8)}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getSessionId() {
|
|
368
|
+
if (!state.sessionId) {
|
|
369
|
+
state.sessionId = crypto.randomUUID();
|
|
370
|
+
localStorage.setItem(`${storagePrefix}-session-id`, state.sessionId);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return state.sessionId;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function renderUsers() {
|
|
377
|
+
const entries = Object.entries(state.users);
|
|
378
|
+
users.innerHTML = entries.length
|
|
379
|
+
? entries.map(([, user]) => {
|
|
380
|
+
const name = escapeHtml(user.name || "Anonymous");
|
|
381
|
+
const connected = Boolean(user.connected);
|
|
382
|
+
return `
|
|
383
|
+
<div class="user">
|
|
384
|
+
<span>${name}</span>
|
|
385
|
+
<span class="badge ${connected ? "online" : ""}">${connected ? "connected" : "offline"}</span>
|
|
386
|
+
</div>
|
|
387
|
+
`;
|
|
388
|
+
}).join("")
|
|
389
|
+
: `<div class="muted">No users yet</div>`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function mergeUsers(target, patch) {
|
|
393
|
+
for (const [id, value] of Object.entries(patch)) {
|
|
394
|
+
if (value === "$delete") {
|
|
395
|
+
delete target[id];
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
target[id] = mergeValue(target[id], value);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function mergeValue(current, patch) {
|
|
404
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
405
|
+
return patch;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const next = current && typeof current === "object" && !Array.isArray(current)
|
|
409
|
+
? { ...current }
|
|
410
|
+
: {};
|
|
411
|
+
|
|
412
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
413
|
+
if (value === "$delete") {
|
|
414
|
+
delete next[key];
|
|
415
|
+
} else {
|
|
416
|
+
next[key] = mergeValue(next[key], value);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return next;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getRoomIdFromPath() {
|
|
424
|
+
const match = location.pathname.match(/^\/rooms\/([^/]+)$/);
|
|
425
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function normalizeRoomId(value) {
|
|
429
|
+
return value.trim().replace(/[^a-zA-Z0-9_-]/g, "-") || "demo";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function escapeHtml(value) {
|
|
433
|
+
return value.replace(/[&<>"']/g, (char) => ({
|
|
434
|
+
"&": "&",
|
|
435
|
+
"<": "<",
|
|
436
|
+
">": ">",
|
|
437
|
+
"\"": """,
|
|
438
|
+
"'": "'",
|
|
439
|
+
}[char]));
|
|
440
|
+
}
|
|
441
|
+
</script>
|
|
442
|
+
</body>
|
|
443
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createCloudflareRoomWorker, SigneRoomDurableObject } from "@signe/room/cloudflare";
|
|
2
|
+
import { CounterServer } from "./room";
|
|
3
|
+
|
|
4
|
+
export { SigneRoomDurableObject };
|
|
5
|
+
|
|
6
|
+
interface Env extends Record<string, unknown> {
|
|
7
|
+
ROOMS: DurableObjectNamespace;
|
|
8
|
+
ASSETS: {
|
|
9
|
+
fetch(request: Request): Promise<Response>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const roomWorker = createCloudflareRoomWorker(CounterServer, {
|
|
14
|
+
binding: "ROOMS",
|
|
15
|
+
partiesPath: "/parties/main",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
async fetch(request: Request, env: Env, ctx: unknown): Promise<Response> {
|
|
20
|
+
const url = new URL(request.url);
|
|
21
|
+
|
|
22
|
+
if (url.pathname.startsWith("/parties/main/")) {
|
|
23
|
+
return roomWorker.fetch(request, env, ctx);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return env.ASSETS.fetch(request);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Action, Request, Room, Server } from "@signe/room";
|
|
2
|
+
import { signal } from "@signe/reactive";
|
|
3
|
+
import { connected, sync, users } from "@signe/sync";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
class DemoUser {
|
|
7
|
+
@sync() name = signal("Anonymous");
|
|
8
|
+
@connected() connected = signal(false);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@Room({ path: "{roomId}", sessionExpiryTime: 2000 })
|
|
12
|
+
class CounterRoom {
|
|
13
|
+
@sync() count = signal(0);
|
|
14
|
+
@users(DemoUser) users = signal<Record<string, DemoUser>>({});
|
|
15
|
+
|
|
16
|
+
onJoin(user: DemoUser, _conn: unknown, ctx: { request?: Request }) {
|
|
17
|
+
const url = new URL(ctx.request?.url ?? "http://localhost");
|
|
18
|
+
const name = url.searchParams.get("name")?.trim();
|
|
19
|
+
|
|
20
|
+
if (name) {
|
|
21
|
+
user.name.set(name.slice(0, 40));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Action("increment", z.object({ amount: z.number().optional() }))
|
|
26
|
+
increment(_user: DemoUser, value: { amount?: number }) {
|
|
27
|
+
this.count.update((count) => count + (value.amount ?? 1));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Request({ path: "/count" })
|
|
31
|
+
getCount() {
|
|
32
|
+
return { count: this.count() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Request({ path: "/reset", method: "POST" })
|
|
36
|
+
reset() {
|
|
37
|
+
this.count.set(0);
|
|
38
|
+
return { count: this.count() };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class CounterServer extends Server {
|
|
43
|
+
rooms = [CounterRoom];
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "signe-room-cloudflare-example",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"compatibility_date": "2024-12-18",
|
|
6
|
+
"assets": {
|
|
7
|
+
"directory": "./public",
|
|
8
|
+
"binding": "ASSETS",
|
|
9
|
+
"not_found_handling": "single-page-application"
|
|
10
|
+
},
|
|
11
|
+
"durable_objects": {
|
|
12
|
+
"bindings": [
|
|
13
|
+
{
|
|
14
|
+
"name": "ROOMS",
|
|
15
|
+
"class_name": "SigneRoomDurableObject"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"migrations": [
|
|
20
|
+
{
|
|
21
|
+
"tag": "v1",
|
|
22
|
+
"new_sqlite_classes": ["SigneRoomDurableObject"]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# `@signe/room` Node Adapter Example
|
|
2
|
+
|
|
3
|
+
This example runs a `@signe/room` server in a plain Node.js HTTP server with
|
|
4
|
+
WebSocket upgrades handled by `ws`.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm install
|
|
8
|
+
pnpm --filter @signe/room-node-example dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Open http://localhost:3000, choose a room id and a display name, then enter the
|
|
12
|
+
room. The browser URL changes to `/rooms/:roomId`; refreshing that URL serves
|
|
13
|
+
the same app and reconnects to the matching room.
|
|
14
|
+
|
|
15
|
+
- App URL: `http://localhost:3000/rooms/demo`
|
|
16
|
+
- HTTP: `GET /parties/main/demo/count`
|
|
17
|
+
- HTTP: `POST /parties/main/demo/reset`
|
|
18
|
+
- WebSocket: `ws://localhost:3000/parties/main/demo?name=Sam&id=browser-session-id`
|
|
19
|
+
|
|
20
|
+
The room uses `@users()` and `@connected()` from `@signe/sync`, so the right
|
|
21
|
+
panel shows every known user and whether they are currently connected.
|
|
22
|
+
|
|
23
|
+
The browser example stores a session id in `localStorage` and sends it as the
|
|
24
|
+
WebSocket `id` query parameter. That id is the private session id used by the
|
|
25
|
+
room server, so refreshing or reconnecting brings the same user back online. If
|
|
26
|
+
you click "New session" or clear local storage, the next connection gets a new
|
|
27
|
+
session and the previous user remains visible as offline until normal session
|
|
28
|
+
cleanup removes it. Multiple tabs with the same stored session id stay attached
|
|
29
|
+
to the same user and receive room broadcasts independently. Server handlers
|
|
30
|
+
receive a unique `conn.id` for each WebSocket and the shared private session id
|
|
31
|
+
as `conn.sessionId`.
|
|
32
|
+
|
|
33
|
+
## SQLite storage
|
|
34
|
+
|
|
35
|
+
The default example uses `createMemoryNodeRoomStorage()`. To run the same room
|
|
36
|
+
with the package's SQLite-backed `room.storage`, use:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm --filter @signe/room-node-example dev:sqlite
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The SQLite example uses `createSqliteNodeRoomStorage()` from `@signe/room/node`
|
|
43
|
+
and Node's built-in `node:sqlite` module. It stores room state in
|
|
44
|
+
`packages/room/examples/node/rooms.sqlite`.
|
|
45
|
+
Because the users collection is persisted, reopening the SQLite example can show
|
|
46
|
+
previous users as offline until they reconnect with the same session id.
|
|
47
|
+
|
|
48
|
+
The storage provider is passed to `createNodeRoomTransport`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const transport = createNodeRoomTransport(CounterServer, {
|
|
52
|
+
partiesPath: "/parties/main",
|
|
53
|
+
storage: createSqliteNodeRoomStorage({
|
|
54
|
+
databasePath: "./rooms.sqlite",
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
|
6
|
+
esac
|
|
7
|
+
|
|
8
|
+
if [ -z "$NODE_PATH" ]; then
|
|
9
|
+
export NODE_PATH="/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/bin/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/node_modules"
|
|
10
|
+
else
|
|
11
|
+
export NODE_PATH="/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/bin/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
12
|
+
fi
|
|
13
|
+
if [ -x "$basedir/node" ]; then
|
|
14
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec node "$basedir/../typescript/bin/tsc" "$@"
|
|
17
|
+
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
|
6
|
+
esac
|
|
7
|
+
|
|
8
|
+
if [ -z "$NODE_PATH" ]; then
|
|
9
|
+
export NODE_PATH="/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/bin/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/node_modules"
|
|
10
|
+
else
|
|
11
|
+
export NODE_PATH="/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/bin/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules/typescript/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/typescript@5.4.5/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
12
|
+
fi
|
|
13
|
+
if [ -x "$basedir/node" ]; then
|
|
14
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
|
17
|
+
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
|
6
|
+
esac
|
|
7
|
+
|
|
8
|
+
if [ -z "$NODE_PATH" ]; then
|
|
9
|
+
export NODE_PATH="/home/runner/work/signe/signe/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/dist/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/tsx@4.21.0/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/node_modules"
|
|
10
|
+
else
|
|
11
|
+
export NODE_PATH="/home/runner/work/signe/signe/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/dist/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/tsx@4.21.0/node_modules:/home/runner/work/signe/signe/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
12
|
+
fi
|
|
13
|
+
if [ -x "$basedir/node" ]; then
|
|
14
|
+
exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec node "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
17
|
+
fi
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@signe/room-node-example",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx server.ts",
|
|
8
|
+
"dev:sqlite": "node --experimental-sqlite --import tsx server.sqlite.ts",
|
|
9
|
+
"build": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@signe/reactive": "workspace:*",
|
|
13
|
+
"@signe/room": "workspace:*",
|
|
14
|
+
"@signe/sync": "workspace:*",
|
|
15
|
+
"ws": "^8.17.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.13.9",
|
|
19
|
+
"@types/ws": "^8.5.12",
|
|
20
|
+
"tsx": "^4.19.2",
|
|
21
|
+
"typescript": "^5.4.5"
|
|
22
|
+
}
|
|
23
|
+
}
|