fix: web UI client review findings (step 2a)

- One socket at a time: a socket being replaced is let go without a word,
  and connect() or a new token while a reconnect waits opens no second one.
  A new token reconnects at once, without a refused.
- Events that came before the schema go as soon as it is whole, ahead of
  the sync frames formatted after them, not at syncEnd over newer state.
- A schema's hash counts only once it has come whole, so a sync cut short
  gets it again.
- Feeds are subscribed again after a reconnect; an unknown feed is refused.
- A long op refused as busy without its id goes again.
- A sync without repeaters clears them; order changes name their list; a
  refused resync is called off.
- refused: a listener can close() to stop the retries, which the UI does
  once /loginContext says a login is needed. The client itself can't tell a
  refused token from a device that is down.
- localStorage keeps the schema by default. minify.py removes its work
  folder even when a bundle fails. ADR 0005 records the two bundles.
- Smaller: ProtoSchema.ROWS/withoutRows, ProtoModel.ITEMS, named type
  numbers, unused feeds map and FakeSocket.all gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-25 18:04:07 +01:00
parent 99c6019431
commit 4415beb4f7
8 changed files with 382 additions and 147 deletions

View file

@ -79,7 +79,26 @@ test('removals, orders and repeaters', () => {
assert.equal(m.shades.get(second.shadeId).sortOrder, 0);
m.applyEvent('repeaters', { repeaters: [7, 8] });
assert.deepEqual(m.repeaters, [7, 8]);
assert.deepEqual(changes.map((c) => c[0]), ['shadeRemoved', 'order', 'repeaters']);
// An order says which list it is.
assert.deepEqual(changes.map((c) => c[0]), ['shadeRemoved', 'shadeOrder', 'repeaters']);
});
test('a sync without repeaters clears them, as the device leaves the list out when it has none', () => {
const m = synced();
m.applyEvent('repeaters', { repeaters: [7] });
m.beginSync();
for (const d of deviceSections(m.schema)) m.applySync(Object.assign({}, d, { repeaters: undefined }));
m.endSync();
assert.deepEqual(m.repeaters, []);
});
test('a sync that is called off removes nothing', () => {
const m = synced();
const shades = m.shades.size;
m.beginSync();
m.cancelSync();
m.endSync();
assert.equal(m.shades.size, shades);
});
test('a sync drops what it no longer lists, and keeps what an event added during it', () => {