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 | 5x 22x 25x 23x 23x 10x 10x 10x 10x 12x 21x 21x 21x 21x 7x 8x 4x 7x 7x 7x 7x 7x 9x 9x 9x 10x 1x | /**
* Pure message-dispatch helpers for the native WebSocket layer.
*
* The `RoomDO.webSocketMessage` handler needs to make several small, pure
* decisions on every frame: is this a filterable no-op? Is it the legacy
* `submitform` special case? What snapshot / auth / chat structure does
* the reply take? Factoring these into one module keeps `room.ts` focused
* on storage I/O and lets the Node test runner enforce 100% branch coverage
* without needing a workerd runtime.
*
* References:
* - CLAUDE.md sec 6.2 (execute / submitform / ask.log flows)
* - CLAUDE.md sec 7 item 12 (text-wiki filter drop)
* - CLAUDE.md sec 7 item 22 (submitform include_self=true invariant)
*/
import type {
AskEcellClientMessage,
AskEcellServerMessage,
AskLogMessage,
ChatClientMessage,
ChatServerMessage,
ClientMessage,
EcellClientMessage,
EcellServerMessage,
EcellsServerMessage,
ExecuteClientMessage,
ExecuteServerMessage,
LogServerMessage,
MyEcellMessage,
MyEcellServerMessage,
StopHuddleMessage,
StopHuddleServerMessage,
} from '@ethercalc/shared/messages';
/** Legacy filter — server drops this command even when auth checks out. */
const TEXT_WIKI_FILTER = 'set sheet defaulttextvalueformat text-wiki';
/**
* Returns true when an `execute` command string should be silently dropped
* without logging or broadcasting. Matches the legacy src/main.ls:502
* guard: the command is a UI affordance on legacy clients that our
* SocialCalc port doesn't need — accepting it would dirty the log.
*/
export function isFilteredExecuteCommand(cmdstr: string): boolean {
return cmdstr === TEXT_WIKI_FILTER;
}
/**
* Test whether an `execute` payload is the `submitform` special case.
*
* Legacy (src/main.ls:518-541): the first line of `cmdstr` — split on a
* carriage return and trimmed — equals the literal string `submitform`.
* The remaining lines carry the row data that must be appended to a
* sibling `<room>_formdata` DO.
*/
export function isSubmitForm(cmdstr: string): boolean {
if (typeof cmdstr !== 'string' || cmdstr.length === 0) return false;
// `split('\r')[0]` on a non-empty string always returns a string.
const firstLine = cmdstr.split('\r')[0] as string;
return firstLine.trim() === 'submitform';
}
/**
* Given a `submitform` `execute` command and the room it was sent against,
* compute the sibling form-data room and return the follow-up commands to
* append there.
*
* Legacy behavior (src/main.ls:522-531):
* 1. Strip the `submitform` header from the cmd string.
* 2. If the current room already ends with `_formdata`, use the same
* room (idempotent on sub-submits from a form editor that itself
* lives in the sibling). Otherwise append `_formdata`.
* 3. The remaining body lines become the commands to replay on the
* sibling — NOT on the source room. The source room sees only the
* header execute broadcast back to all peers with `include_self:
* true`.
*/
export function computeSubmitFormTarget(
room: string,
cmdstr: string,
): { siblingRoom: string; siblingCommands: string } {
const siblingRoom = room.endsWith('_formdata') ? room : `${room}_formdata`;
// Drop everything up to and including the first CR. If the command is
// just the bare word `submitform` with no trailing payload, the sibling
// commands payload is the empty string.
const headerEnd = cmdstr.indexOf('\r');
const siblingCommands = headerEnd === -1 ? '' : cmdstr.slice(headerEnd + 1);
return { siblingRoom, siblingCommands };
}
/**
* Shape a `chat` client message into its broadcast server form. The
* server-emitted `chat` message carries exactly the fields the legacy
* client consumed on the receiving end (`room`, `user`, `msg`).
*/
export function buildChatBroadcast(msg: ChatClientMessage): ChatServerMessage {
return { type: 'chat', room: msg.room, user: msg.user, msg: msg.msg };
}
/**
* Shape an execute-broadcast message. `include_self` is only set when the
* caller explicitly requests it — legacy treats the missing field and
* `false` equivalently, but we preserve an explicit `true` because the
* sec 7 item 22 submitform invariant is tested on presence + truthiness.
*/
export function buildExecuteBroadcast(
msg: ExecuteClientMessage,
includeSelf: boolean,
): ExecuteServerMessage {
const base: ExecuteServerMessage = {
type: 'execute',
room: msg.room,
user: msg.user,
cmdstr: msg.cmdstr,
};
if (msg.auth !== undefined) base.auth = msg.auth;
if (includeSelf) base.include_self = true;
return base;
}
/** Shape an ecells-reply broadcast. */
export function buildEcellsReply(
room: string,
ecells: Record<string, string>,
): EcellsServerMessage {
return { type: 'ecells', room, ecells };
}
/** Shape a my.ecell broadcast. */
export function buildMyEcellBroadcast(msg: MyEcellMessage): MyEcellServerMessage {
return { type: 'my.ecell', room: msg.room, user: msg.user, ecell: msg.ecell };
}
/**
* Shape an ask.ecell rebroadcast. Forwards the asker's `user` to peers so
* they can target their `ecell` reply via `to: user`. The legacy catch-all
* did the same thing with `broadcast @data` — we carry the payload
* explicitly because our parser is closed-union.
*/
export function buildAskEcellBroadcast(
msg: AskEcellClientMessage,
): AskEcellServerMessage {
return { type: 'ask.ecell', room: msg.room, user: msg.user };
}
/** Shape an ecell broadcast. */
export function buildEcellBroadcast(msg: EcellClientMessage): EcellServerMessage {
const out: EcellServerMessage = {
type: 'ecell',
room: msg.room,
user: msg.user,
ecell: msg.ecell,
};
if (msg.original !== undefined) out.original = msg.original;
if (msg.auth !== undefined) out.auth = msg.auth;
// `to` targets a single peer (ask.ecell reply path). Must survive the
// round-trip so the client-side `data.to !== _username` filter works.
if (msg.to !== undefined) out.to = msg.to;
return out;
}
/** Shape a stopHuddle broadcast. */
export function buildStopHuddleBroadcast(
msg: StopHuddleMessage,
): StopHuddleServerMessage {
const out: StopHuddleServerMessage = { type: 'stopHuddle', room: msg.room };
if (msg.auth !== undefined) out.auth = msg.auth;
return out;
}
/** Shape the reply to ask.log. */
export function buildLogReply(
msg: AskLogMessage,
log: readonly string[],
chat: readonly string[],
snapshot: string,
): LogServerMessage {
return { type: 'log', room: msg.room, log, chat, snapshot };
}
/**
* Discriminated-union guard over ClientMessage. Keeps the switch
* exhaustive in room.ts via TS's `never` branch narrowing; this helper
* exists so the Node test runner can assert we enumerate every legal
* type.
*/
export function clientMessageType(msg: ClientMessage): ClientMessage['type'] {
return msg.type;
}
|