Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | 1x 1x 21x 16x 16x 12x 12x 10x 9x 18x 18x 15x 15x 13x 12x 34x 34x 2x | /**
* WebSocket protocol between the EtherCalc client and server.
*
* Transport: one WS per user per room at `wss://<host>/_ws/:room?user=<user>&auth=<hmac>`.
* Wire format: JSON, one message per frame, shape `{ type: <discriminator>, ...payload }`.
*
* Parity reference: legacy `src/main.ls:484` `@on data:` switch statement.
* The legacy socket.io 0.9 protocol is NOT represented here — it's translated
* at the edge by the `/socket.io/*` compatibility shim.
*/
// ─── Client → Server ───────────────────────────────────────────────────────
export interface ChatClientMessage {
type: 'chat';
room: string;
user: string;
msg: string;
}
export interface AskEcellsMessage {
type: 'ask.ecells';
room: string;
}
export interface MyEcellMessage {
type: 'my.ecell';
room: string;
user: string;
ecell: string;
}
export interface ExecuteClientMessage {
type: 'execute';
room: string;
user: string;
auth?: string;
cmdstr: string;
}
export interface AskLogMessage {
type: 'ask.log';
room: string;
user: string;
}
export interface AskRecalcMessage {
type: 'ask.recalc';
room: string;
}
export interface StopHuddleMessage {
type: 'stopHuddle';
room: string;
auth?: string;
}
export interface EcellClientMessage {
type: 'ecell';
room: string;
user: string;
ecell: string;
original?: string;
auth?: string;
/**
* Private-channel targeting. When set, the server rebroadcasts to all
* peers but only the peer whose username matches `to` acts on it —
* everyone else drops it in the client dispatcher. Used by the
* ask.ecell → ecell reply flow (legacy `player.ls:122`).
*/
to?: string;
}
/**
* `ask.ecell` (singular) — "tell me where you are" cursor-poll broadcast.
*
* Legacy (`src/player-broadcast.ls:21`): every time the local client's
* `DoPositionCalculations` runs (scroll, resize, refocus), it sends
* `ask.ecell`. The legacy server's catch-all `@on data` broadcasts it to
* every peer in the room. Peers receive it and reply with their own
* `ecell` coordinate targeted at the asker via `to: <user>`.
*
* Our server had no catch-all and no explicit `ask.ecell` handler, so the
* frame was silently dropped and remote cursors went stale whenever a
* peer scrolled. Found during the 2026-04-20 browser smoke sweep.
*/
export interface AskEcellClientMessage {
type: 'ask.ecell';
room: string;
user: string;
}
export type ClientMessage =
| ChatClientMessage
| AskEcellsMessage
| MyEcellMessage
| ExecuteClientMessage
| AskLogMessage
| AskRecalcMessage
| StopHuddleMessage
| EcellClientMessage
| AskEcellClientMessage;
export const CLIENT_MESSAGE_TYPES = [
'chat',
'ask.ecells',
'my.ecell',
'execute',
'ask.log',
'ask.recalc',
'stopHuddle',
'ecell',
'ask.ecell',
] as const satisfies readonly ClientMessage['type'][];
// ─── Server → Client ───────────────────────────────────────────────────────
export interface LogServerMessage {
type: 'log';
room: string;
log: readonly string[];
chat: readonly string[];
snapshot: string;
}
export interface RecalcServerMessage {
type: 'recalc';
room: string;
log: readonly string[];
snapshot: string;
force?: boolean;
}
export interface SnapshotServerMessage {
type: 'snapshot';
snapshot: string;
}
export interface EcellsServerMessage {
type: 'ecells';
room: string;
ecells: Record<string, string>;
}
export interface ExecuteServerMessage {
type: 'execute';
room: string;
user: string;
auth?: string;
cmdstr: string;
include_self?: boolean;
}
export interface ChatServerMessage {
type: 'chat';
room: string;
user: string;
msg: string;
}
export interface ConfirmEmailSentMessage {
type: 'confirmemailsent';
message: string;
}
export interface IgnoreMessage {
type: 'ignore';
}
export interface StopHuddleServerMessage {
type: 'stopHuddle';
room: string;
auth?: string;
}
export interface EcellServerMessage {
type: 'ecell';
room: string;
user: string;
ecell: string;
original?: string;
auth?: string;
/** See `EcellClientMessage.to` — preserved end-to-end. */
to?: string;
}
export interface MyEcellServerMessage {
type: 'my.ecell';
room: string;
user: string;
ecell: string;
}
/**
* `ask.ecell` (singular) rebroadcast to peers. Shape identical to the
* client-sent frame — same `user` field that peers use to target their
* reply via `ecell` with `to: user`. See `AskEcellClientMessage`.
*/
export interface AskEcellServerMessage {
type: 'ask.ecell';
room: string;
user: string;
}
export type ServerMessage =
| LogServerMessage
| RecalcServerMessage
| SnapshotServerMessage
| EcellsServerMessage
| ExecuteServerMessage
| ChatServerMessage
| ConfirmEmailSentMessage
| IgnoreMessage
| StopHuddleServerMessage
| EcellServerMessage
| MyEcellServerMessage
| AskEcellServerMessage;
export const SERVER_MESSAGE_TYPES = [
'log',
'recalc',
'snapshot',
'ecells',
'execute',
'chat',
'confirmemailsent',
'ignore',
'stopHuddle',
'ecell',
'my.ecell',
'ask.ecell',
] as const satisfies readonly ServerMessage['type'][];
// ─── Codec ────────────────────────────────────────────────────────────────
export function encodeMessage(msg: ClientMessage | ServerMessage): string {
return JSON.stringify(msg);
}
export function parseClientMessage(raw: string): ClientMessage | null {
const parsed = safeJsonParse(raw);
if (!parsed || typeof parsed !== 'object') return null;
const type = (parsed as { type?: unknown }).type;
if (typeof type !== 'string') return null;
if (!(CLIENT_MESSAGE_TYPES as readonly string[]).includes(type)) return null;
return parsed as ClientMessage;
}
export function parseServerMessage(raw: string): ServerMessage | null {
const parsed = safeJsonParse(raw);
if (!parsed || typeof parsed !== 'object') return null;
const type = (parsed as { type?: unknown }).type;
if (typeof type !== 'string') return null;
if (!(SERVER_MESSAGE_TYPES as readonly string[]).includes(type)) return null;
return parsed as ServerMessage;
}
function safeJsonParse(raw: string): unknown {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
|