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

@ -11,10 +11,15 @@ class ProtoSchema {
this.keys = {}; // name -> key
this.errors = {};
this.errorNumbers = {};
this.feeds = {};
this.feedNumbers = {};
this.rows = { T: [], F: [], E: [], Q: [] };
}
// A sync frame's payload without the schema's rows: the device's sections.
static withoutRows(d) {
const out = {};
for (const key of Object.keys(d)) if (!ProtoSchema.ROWS.includes(key)) out[key] = d[key];
return out;
}
// The schema's rows in a sync frame's payload: T types, F fields, E error
// codes, Q feeds. They may span several frames.
add(d) {
@ -30,11 +35,8 @@ class ProtoSchema {
this.errors[number] = name;
this.errorNumbers[name] = number;
}
for (const [number, name] of d.Q || []) {
this.feeds[number] = name;
this.feedNumbers[name] = number;
}
for (const k of ['T', 'F', 'E', 'Q']) if (d[k]) this.rows[k].push(...d[k]);
for (const [number, name] of d.Q || []) this.feedNumbers[name] = number;
for (const k of ProtoSchema.ROWS) if (d[k]) this.rows[k].push(...d[k]);
}
get empty() { return Object.keys(this.fields).length === 0; }
// What a field left out of a frame is: its default.
@ -93,4 +95,7 @@ class ProtoSchema {
return s.empty ? null : s;
}
}
// The keys of the schema's rows in a sync frame, fixed as a client needs them
// before it has the schema.
ProtoSchema.ROWS = ['T', 'F', 'E', 'Q'];
if (typeof module !== 'undefined' && module.exports) module.exports = { ProtoSchema };