fix: protocol 2 core review findings

From the review of #99:
- A sync could stall when a client took a slot the moment another left:
  loop() used up the new client's sync on the old one. The slots move
  to ProtoSlots, a pure module with native tests, and loop() takes a
  slot's wants only for the client it read (takeWants).
- Deleting a group sends its shades, whose inGroup changed. Deleting a
  shade unlinks it from its groups and sends them, so a new shade given
  its id doesn't join them.
- next and sync with an id get an ok reply, and a message in pieces
  gets one bad request, not one per piece.
- Feeds check for loop() before they write the event buffer, and log
  an event too large, as events do.
- editChanges() (ModelEditor, native tests) says what clients get after
  an edit; the edit result carries the shade a group gained or lost.
  order() takes a sort_list_t.
- The frame writers move to ProtoFrame. Renames: pf to proto_field_t,
  the X-macro letters to TYPE/FIELD/ERR/FEED, proto_request_t to
  proto_message_t, REQ_* to WANTS_*, fmt to eventJson. The event
  bodies share sendItem(), the connection type name lives in
  ESPNetwork, and the loop() task check in SocketEmitter.
- e2e: the heap floor check skips when the low since boot is already
  under it; tests for replies to next and sync and a long message.
- SECURITY_PLAN.md splits step 1 into 1a-1d.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 13:17:15 +02:00
parent d869bb53d5
commit dc2f1478b6
29 changed files with 1087 additions and 670 deletions

View file

@ -12,21 +12,21 @@
// The largest frame a client may send; a longer one is refused unread.
#define PROTO_MAX_CLIENT_FRAME 4096
struct proto_request_t {
struct proto_message_t {
uint8_t type = 0;
uint32_t id = 0; // 0 when the frame had none
};
// Reads one frame into doc and out. none when it is a message a client may
// send; otherwise the error to reply with. out.id is set whenever the frame
// had one, so the reply can name it. The payload is doc["d"].
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_request_t &out);
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_message_t &out);
// An op (16-127) as opposed to a control message.
inline bool protoIsOp(uint8_t type) { return type >= 16 && type <= 127; }
// A field of a payload, or its default when it was left out.
int32_t protoInt(JsonObjectConst d, pf field);
bool protoBool(JsonObjectConst d, pf field);
int32_t protoInt(JsonObjectConst d, proto_field_t field);
bool protoBool(JsonObjectConst d, proto_field_t field);
// nullptr when it was left out, as a string's default is "".
const char *protoString(JsonObjectConst d, pf field);
bool protoHas(JsonObjectConst d, pf field);
const char *protoString(JsonObjectConst d, proto_field_t field);
bool protoHas(JsonObjectConst d, proto_field_t field);
#endif