ugly-game 0.5.7 → 0.5.8
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/dist/net/roomCore.d.ts +8 -2
- package/dist/net/roomCore.js +44 -3
- package/package.json +1 -1
package/dist/net/roomCore.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ interface DOStorage {
|
|
|
6
6
|
get<T>(key: string): Promise<T | undefined>;
|
|
7
7
|
put<T>(key: string, value: T): Promise<void>;
|
|
8
8
|
delete(key: string): Promise<void>;
|
|
9
|
+
list<T>(options?: {
|
|
10
|
+
prefix?: string;
|
|
11
|
+
}): Promise<Map<string, T>>;
|
|
9
12
|
}
|
|
10
13
|
interface DOState {
|
|
11
14
|
storage: DOStorage;
|
|
@@ -22,8 +25,11 @@ export declare class RoomCore {
|
|
|
22
25
|
private seq;
|
|
23
26
|
constructor(state: DOState, opts?: RoomOptions);
|
|
24
27
|
fetch(request: Request): Promise<unknown>;
|
|
25
|
-
/** The app DO forwards its webSocketMessage here. */
|
|
26
|
-
webSocketMessage(ws: WS, message: string | ArrayBuffer): void
|
|
28
|
+
/** The app DO forwards its webSocketMessage here. Async so relayed edits can be durably accumulated. */
|
|
29
|
+
webSocketMessage(ws: WS, message: string | ArrayBuffer): Promise<void>;
|
|
30
|
+
/** LWW-accumulate a relayed keyed edit into DO storage, so a later joiner reconstructs the world. Keeps the
|
|
31
|
+
* higher (stamp, by) — the exact rule the free-roam client's LwwStore uses, so server + clients converge. */
|
|
32
|
+
private accumulateEdit;
|
|
27
33
|
webSocketClose(ws: WS): void;
|
|
28
34
|
webSocketError(ws: WS): void;
|
|
29
35
|
private json;
|
package/dist/net/roomCore.js
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
// · `repin` → lone client re-pins the shared-clock epoch
|
|
18
18
|
// Snapshot bytes are stored chunked (PUT/GET) for join/resync + save. WebSocket
|
|
19
19
|
// HIBERNATION (state.acceptWebSocket) means no duration billed while a room idles.
|
|
20
|
+
//
|
|
21
|
+
// PERSISTENT KEYED WORLD (free-roam / all-mine-sky): a `relay` whose payload carries an `e = {key, val, stamp,
|
|
22
|
+
// by}` is ALSO accumulated server-side, last-write-wins by (stamp, by), under `e:<key>`. GET `edits` returns the
|
|
23
|
+
// accumulated set so a joiner reconstructs the world — even into an empty room. Persisting from the WS handler
|
|
24
|
+
// (not a concurrent HTTP PUT) is REQUIRED: an HTTP storage write to a DO that holds a live hibernatable socket
|
|
25
|
+
// silently no-ops, whereas a write inside webSocketMessage commits. Relays without an `e.key` (e.g. cogsworth's
|
|
26
|
+
// lockstep commands) are untouched — accumulation is opt-in by payload shape, so RoomCore stays game-agnostic.
|
|
20
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
28
|
const CHUNK = 96 * 1024; // DO value cap is ~128KB; chunk snapshots under it
|
|
22
29
|
const KEYS = { epoch: 'epoch', chunks: 'chunks', bytes: 'bytes', tick: 'tick' };
|
|
@@ -45,6 +52,18 @@ export class RoomCore {
|
|
|
45
52
|
pair[1].send(JSON.stringify({ t: 'hello', peers: this.state.getWebSockets().length, epoch, now: Date.now() }));
|
|
46
53
|
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
47
54
|
}
|
|
55
|
+
// ── accumulated keyed edits (free-roam join sync): the LWW deltas persisted from the relay stream ──
|
|
56
|
+
if (path === 'edits') {
|
|
57
|
+
const m = await this.state.storage.list({ prefix: 'e:' });
|
|
58
|
+
return new Response(JSON.stringify([...m.values()]), {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: {
|
|
61
|
+
'content-type': 'application/json',
|
|
62
|
+
'cache-control': 'no-store',
|
|
63
|
+
'x-ugly-no-spa-fallback': '1',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
48
67
|
// ── snapshot SAVE (join/resync + persistence): body = opaque bytes, stored chunked ──
|
|
49
68
|
if (request.method === 'PUT' || request.method === 'POST') {
|
|
50
69
|
const body = new Uint8Array(await request.arrayBuffer());
|
|
@@ -87,8 +106,8 @@ export class RoomCore {
|
|
|
87
106
|
},
|
|
88
107
|
});
|
|
89
108
|
}
|
|
90
|
-
/** The app DO forwards its webSocketMessage here. */
|
|
91
|
-
webSocketMessage(ws, message) {
|
|
109
|
+
/** The app DO forwards its webSocketMessage here. Async so relayed edits can be durably accumulated. */
|
|
110
|
+
async webSocketMessage(ws, message) {
|
|
92
111
|
let m = null;
|
|
93
112
|
try {
|
|
94
113
|
m = JSON.parse(typeof message === 'string' ? message : new TextDecoder().decode(message));
|
|
@@ -104,7 +123,7 @@ export class RoomCore {
|
|
|
104
123
|
}
|
|
105
124
|
if (m.t === 'repin' && typeof m.tick === 'number' && this.state.getWebSockets().length === 1) {
|
|
106
125
|
const epoch = Date.now() - m.tick * (this.opts.tickMs ?? 0);
|
|
107
|
-
|
|
126
|
+
await this.state.storage.put(KEYS.epoch, epoch);
|
|
108
127
|
ws.send(JSON.stringify({ t: 'epoch', epoch, now: Date.now() }));
|
|
109
128
|
return;
|
|
110
129
|
}
|
|
@@ -120,6 +139,28 @@ export class RoomCore {
|
|
|
120
139
|
const stamped = JSON.stringify({ ...m, seq: ++this.seq });
|
|
121
140
|
for (const peer of this.state.getWebSockets())
|
|
122
141
|
peer.send(stamped);
|
|
142
|
+
// free-roam persistence: a relay carrying a keyed edit is accumulated LWW so it outlives the session (fanout
|
|
143
|
+
// first — this must never delay the relay). Opt-in by payload shape → lockstep relays (no e.key) are untouched.
|
|
144
|
+
if (m.t === 'relay')
|
|
145
|
+
await this.accumulateEdit(m.e);
|
|
146
|
+
}
|
|
147
|
+
/** LWW-accumulate a relayed keyed edit into DO storage, so a later joiner reconstructs the world. Keeps the
|
|
148
|
+
* higher (stamp, by) — the exact rule the free-roam client's LwwStore uses, so server + clients converge. */
|
|
149
|
+
async accumulateEdit(e) {
|
|
150
|
+
if (!e || typeof e !== 'object')
|
|
151
|
+
return;
|
|
152
|
+
const ed = e;
|
|
153
|
+
if (typeof ed.key !== 'number' && typeof ed.key !== 'string')
|
|
154
|
+
return;
|
|
155
|
+
const k = `e:${ed.key}`;
|
|
156
|
+
const es = typeof ed.stamp === 'number' ? ed.stamp : 0;
|
|
157
|
+
const cur = await this.state.storage.get(k);
|
|
158
|
+
if (cur) {
|
|
159
|
+
const cs = typeof cur.stamp === 'number' ? cur.stamp : 0;
|
|
160
|
+
if (cs > es || (cs === es && String(cur.by) >= String(ed.by)))
|
|
161
|
+
return; // an existing write outranks it
|
|
162
|
+
}
|
|
163
|
+
await this.state.storage.put(k, e);
|
|
123
164
|
}
|
|
124
165
|
webSocketClose(ws) {
|
|
125
166
|
try {
|