fix: edit ops review findings

From the review of 7723711:
- The assembler is locked: a client closed for a full queue is closed
  by the task that was sending, loop(), whose disconnect dropped the
  buffer while async_tcp could be copying into it. A whole message is
  taken out of it, so a disconnect can't free it while it is read.
- loop() expires a message stalled for 10 s, freeing its buffer and
  telling its client, instead of holding it until another client came.
  A binary message is refused before it can touch another's buffer.
- Each edit takes only the fields its route reads; one it would ignore
  is refused by name. linkRemote's rolling code goes in under the
  route's name, so applyModelEdit is unchanged again.
- An edit refused for an id that doesn't exist replies not found
  (edit_result_t::missing). A reply that doesn't fit its buffer goes
  as an internal error, so the op still gets one.
- The TYPE table says what becomes of each op (proto_op_t), which the
  socket switches on. The reply address is one proto_reply_to_t. The
  routes and the ops start and queue an edit the same way
  (newModelEdit, submitModelEdit). The full-list texts and the offered
  remote address rule live in ModelEditor.
- e2e: the sort order test skips a list that it would renumber.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 18:10:06 +02:00
parent 77237111ea
commit 8fa087aa89
23 changed files with 486 additions and 257 deletions

View file

@ -26,9 +26,10 @@ enum class model_edit_type_t : uint8_t {
// What an edit did, sent to the clients as requestResult. id is the room,
// shade or group it added or changed, 0 when none. shadeId is the shade a
// group gained or lost. code is the validation error of a shade that wasn't
// saved, 0 otherwise.
// saved, 0 otherwise. notFound: it was refused for an id that doesn't exist.
struct edit_result_t {
bool ok = true;
bool notFound = false;
uint8_t id = 0;
uint8_t shadeId = 0;
int8_t code = 0;
@ -40,7 +41,17 @@ struct edit_result_t {
r.code = code;
return r;
}
static edit_result_t missing(const char *desc) {
edit_result_t r = error(desc);
r.notFound = true;
return r;
}
};
// Why an add, or the read that offers one, is refused when every slot of its
// kind is taken.
#define EDIT_ROOMS_FULL "Maximum number of rooms exceeded."
#define EDIT_SHADES_FULL "Maximum number of shades exceeded."
#define EDIT_GROUPS_FULL "Maximum number of groups exceeded."
// The lists an edit can put in a new order.
enum class sort_list_t : uint8_t { none, rooms, shades, groups };
@ -64,6 +75,9 @@ int8_t nextSortOrder(const int8_t *orders, size_t count);
// candidate, or the next address after it that isn't in used. No two shades or
// groups may share a remote address.
uint32_t freeRemoteAddress(uint32_t candidate, const uint32_t *used, size_t count);
// The remote address a new shade or group with that id is offered: the
// starting address plus its id, or the next free one after it.
uint32_t offeredRemoteAddress(uint32_t startingAddress, uint8_t id, const uint32_t *used, size_t count);
// A list of ids or addresses with 0 for an empty slot, kept packed at the front.
// Adding one that is already there changes nothing and succeeds; false when the