Repository navigation
Conversation
|
Thanks Adam. This follows the shape agreed in #347 closely, and the isolation holds up:
A few things to change before merge, most important first. 1. Backend CI fails, from a timing race in a test. 2. A reinstalled plugin's counter can repeat an old value, and the ETag can't tell. New counters start from the boot What goes wrong on a display:
So the first write after a reset is lost on every display, which is the case this PR exists for. Suggested fix: one server-wide sequence for plugin counters, so a value is never reused within a boot: let pluginSeq = base;
// in bumpPlugin, instead of starting from base:
revisions[domain] = ++pluginSeq;(or put the keys in the ETag). It's also worth pruning in 3. Drop the Google Tasks route entry. 4. Reject 5. Plugin guide wording.
Smaller: the branch is two commits behind main (#362, #364). A rebase will re-run CI against them. 🤖 Review written with Claude Code |
…herforth#347) Per-plugin storage counters on the jherforth#339 path: a write moves only its own plugin's counter, pushed over the event stream within a second and caught up by the 15s GET /api/changes check, so plugins need polling only as the older-server fallback. The event carries the plugin id only, never a key; the frame reloads what it uses. Counters are created on a plugin's first storage write and dropped when its data is purged, so the set grows and shrinks with installed plugins; the ETag is built from the five fixed kinds plus the plugin kinds in sorted order. A frame receives only its own plugin's events: the dashboard subscribes per plugin id and forwards nothing else into the frame. Covered write paths: PUT, DELETE and POST increment through the request hook, server-side reactions, and uninstall with purge; reactions never fire on a storage change itself, which would self-trigger. The guide documents the no-key payload and the per-plugin scoping. Tests: per-path and isolation coverage in server/tests (unit + real server, incl. the stream carrying no key) and client tracker tests (a frame's subscription hears only its own plugin, baseline rules, prune on uninstall, reinstall reading as changed).
- Reaction stream test: wait out the coalescing window before opening the stream, and count only this test's own plugin's events, so an earlier test's burst can't be mistaken for this test's event. - Plugin counters now start from a server-wide sequence, never reused within a boot, so uninstall-with-purge plus reinstall can't repeat the old counter or ETag; the client also prunes vanished plugin keys on every 200, so a reinstall reads as changed even at an equal value. - Drop the dead /api/users/:id/google-tasks/sync route entry: HomeGlow has no such route. - Installing a manifest with a reaction on plugin.storage.changed now fails loudly instead of silently never running. - Guide: declaring the event requires a server with this support (no older-server fallback), and a frame hears its own writes too, so a write-on-load plus reload-on-event widget would loop.
11b5d9e to
a5f3879
Compare
|
Thanks for the review — all five points are addressed in the latest push, and the branch is rebased onto main (#362, #364):
Server tests (householdChanges, pluginStorageChanges, pluginPlatform, householdChangesRoutes: 58 pass) and client householdChanges tests (14 pass) are green locally; CI will re-run against the rebased branch. Generated with Muse AI 🤖 |
|
Thanks Adam. I re-checked a5f3879 on the rebased branch. Points 1, 3, 4 and 5 are done and look right:
The client pruning on every 200 is also good. One thing left on point 2: the counter can still repeat. My snippet was ambiguous here, sorry. The sequence is only used when a counter is created, and later writes still do The window is narrower now, thanks to the client prune. It only bites when the purge and the reinstall's first write both happen between two of a display's checks (15 s, or longer while the tab is hidden). In that case the display sends the old ETag and gets 304, and the stream event is dropped as "already known". The fix is to take the next sequence value on every plugin bump: function bumpPlugin(pluginId) {
if (typeof pluginId !== 'string' || pluginId.length === 0) return;
const domain = pluginDomain(pluginId);
revisions[domain] = ++pluginSeq;
pending.add(domain);
scheduleFlush();
}Then no plugin counter value is ever reused within a boot. Every write and every purge changes the set of values, so the ETag can't repeat either, even without keys in it. The new server test passes only because it creates With that change I'm happy with this. 🤖 Review written with Claude Code |
A create-only sequence still lets a plugin's own count climb back onto a value it had before a purge (two writes, purge, reinstall, one write repeats the old counter and ETag), so a display holding the old ETag reads a 304 and drops the stream event as already known between checks. Every bump now takes ++pluginSeq, so no plugin counter value is ever reused within a boot. The reinstall test no longer leans on an intervening write to another plugin: two writes, purge, reinstall, one write must give a fresh counter and ETag. The moves-only-that-counter test expectation shifts accordingly (other takes 103, since lists took 101 and 102). Generated with Muse AI
|
Thanks — good catch, and thanks for spelling out the remaining case. Fixed in the latest push:
Server tests (householdChanges, pluginStorageChanges, pluginPlatform, householdChangesRoutes: 58 pass) and client householdChanges tests (14 pass) are green locally; CI will re-run against the new push. Generated with Muse AI 🤖 |
|
Read through the latest push, not run. The fixes Jay asked for look right. Three things left, the first one being the one I'd settle before merge:
Smaller: declaring this event without |
1. Restart seeding: seedPlugins() at boot seeds a counter for every
plugin with stored data (distinct plugin_storage rows), so frames
read as moved after a restart instead of silently pruning a
vanished key and showing stale data until the next write.
2. Self-write detection: storage writes return their revision
(PUT/DELETE { success, revision }, increment adds revision),
bumped in-route so the response carries the exact revision the
event will carry; the tracker and wrapper pass the revision
through to the frame, which compares it against its own writes'
revisions. The event still carries no key and no value.
3. Visible-mode (phones) deafness: in visible mode the stream now
establishes the baseline instead of dropping events until a
first GET succeeds; interval-mode behavior is unchanged.
4. Install rejection: declaring plugin.storage.changed without
storage: true now fails at install, like reactions do.
5. Docs: backend-api.md describes the dynamic plugin:<pluginId>
counters alongside the fixed household kinds; the plugin guide
covers the restart promise, revision comparison, phone
behavior, and the install failure.
Tests: server unit + real-server revision/restart tests, client
tracker visible-mode tests, platform install-rejection test; full
client (891) and server (713) suites green.
|
Thanks @mrramam — all three points plus the two smaller fixes are in, pushed as bf48ce4.
Tests: a server unit test proving a restart makes plugin frames read as moved (seeded value differs, re-seeding is a no-op, the first write after a seed takes the next value, seeding sends no event), plus a real-server restart test (write, restart on the same database, counter present and changed); a real-server test that PUT/DELETE/increment return their revision and the stream event carries the same one; client tests for the visible-mode failed-first-check-then-stream case (plugin and household); a platform test for the install rejection; and updated client assertions for the revision argument. Full client suite 891/891, full server suite 713/713, vite build and i18n check green. The PR awaits re-review — happy to adjust if you or Jay see anything else. Generated with Muse AI 🤖 |
|
Thanks @adamecker, bf48ce4 covers all of mrramam's points, and the counter-reuse fix holds. One problem is left, in what the guide tells plugin authors, plus one small cleanup. Skipping your own revisions can lose another display's writeThe guide says: "remember the revisions your writes returned and skip events carrying one of them." That isn't safe, because one event can stand for several writes:
So if another display writes and then this frame writes before the next event or check, the frame sees only its own revision, skips it, and never reloads the other display's write. A quick run against const hc = createHouseholdChanges({ base: 100, onPluginStorage: (id, rev) => events.push({ id, rev }) });
hc.bumpPlugin('lists'); // another display's PUT -> 101
hc.bumpPlugin('lists'); // this frame's PUT, response says 102
hc.flush();
// events: [ { id: 'lists', rev: 102 } ] -> the frame skips it; 101 is never reloadedThis matters most on installs where the event stream doesn't reach the browser (Jay's is one), since the check is then the only path and the window is up to 15 seconds every time. On phones, it lasts until the phone next becomes visible. Suggested fix: drop the skip-your-own advice. Reloading after your own write costs one extra fetch, while skipping can lose data. Keep the useful warning, reworded: don't write storage in response to this event, or you'll loop. If you'd rather keep the skip, writes would also need to return the counter from before the write, and a frame could skip only when its own writes chain back to the last revision it saw. That's fiddly for plugin authors, so I'd go with the simpler fix. Smaller: the response hook's backstop can't fireIn Checked and fine
🤖 Generated with Claude Code |
- Guide: drop the skip-your-own-revisions advice — one event can stand for several writes (100 ms coalescing, latest-counter-only changes check), so skipping your own revision can lose another display's write. Keep the warning reworded: don't write storage in response to this event, or you'll loop. Revision fields stay, harmless. - Remove the onResponse hook's plugin-storage backstop: it only matched the three storage routes, all of which bump in-route, so it could never fire. Drop the now-unused pluginStorageWrite helper and its test with it; storage counters move in-route only. Server suite 712/712.
|
Thanks — both points addressed in the latest push (def6723):
Server suite green: 712/712 (one fewer than before — the removed helper's test). Generated with Muse AI 🤖 |
|
Thanks @adamecker, def6723 covers both points. To get this merged without another round trip, I've carried it to main as #377: your six commits unchanged (with your authorship), plus one comment-only commit (8908156) that drops the last skip-your-own-writes wording. That wording was in the SDK's 🤖 Generated with Claude Code |
…347, carries #363) (#377) * feat(plugins): push plugin storage writes as plugin.storage.changed (#347) Per-plugin storage counters on the #339 path: a write moves only its own plugin's counter, pushed over the event stream within a second and caught up by the 15s GET /api/changes check, so plugins need polling only as the older-server fallback. The event carries the plugin id only, never a key; the frame reloads what it uses. Counters are created on a plugin's first storage write and dropped when its data is purged, so the set grows and shrinks with installed plugins; the ETag is built from the five fixed kinds plus the plugin kinds in sorted order. A frame receives only its own plugin's events: the dashboard subscribes per plugin id and forwards nothing else into the frame. Covered write paths: PUT, DELETE and POST increment through the request hook, server-side reactions, and uninstall with purge; reactions never fire on a storage change itself, which would self-trigger. The guide documents the no-key payload and the per-plugin scoping. Tests: per-path and isolation coverage in server/tests (unit + real server, incl. the stream carrying no key) and client tracker tests (a frame's subscription hears only its own plugin, baseline rules, prune on uninstall, reinstall reading as changed). * Address review on plugin storage push (#347) - Reaction stream test: wait out the coalescing window before opening the stream, and count only this test's own plugin's events, so an earlier test's burst can't be mistaken for this test's event. - Plugin counters now start from a server-wide sequence, never reused within a boot, so uninstall-with-purge plus reinstall can't repeat the old counter or ETag; the client also prunes vanished plugin keys on every 200, so a reinstall reads as changed even at an equal value. - Drop the dead /api/users/:id/google-tasks/sync route entry: HomeGlow has no such route. - Installing a manifest with a reaction on plugin.storage.changed now fails loudly instead of silently never running. - Guide: declaring the event requires a server with this support (no older-server fallback), and a frame hears its own writes too, so a write-on-load plus reload-on-event widget would loop. * Take the next plugin sequence value on every plugin bump (#347) A create-only sequence still lets a plugin's own count climb back onto a value it had before a purge (two writes, purge, reinstall, one write repeats the old counter and ETag), so a display holding the old ETag reads a 304 and drops the stream event as already known between checks. Every bump now takes ++pluginSeq, so no plugin counter value is ever reused within a boot. The reinstall test no longer leans on an intervening write to another plugin: two writes, purge, reinstall, one write must give a fresh counter and ETag. The moves-only-that-counter test expectation shifts accordingly (other takes 103, since lists took 101 and 102). Generated with Muse AI * Address mrramam's design review on plugin storage push (#347) 1. Restart seeding: seedPlugins() at boot seeds a counter for every plugin with stored data (distinct plugin_storage rows), so frames read as moved after a restart instead of silently pruning a vanished key and showing stale data until the next write. 2. Self-write detection: storage writes return their revision (PUT/DELETE { success, revision }, increment adds revision), bumped in-route so the response carries the exact revision the event will carry; the tracker and wrapper pass the revision through to the frame, which compares it against its own writes' revisions. The event still carries no key and no value. 3. Visible-mode (phones) deafness: in visible mode the stream now establishes the baseline instead of dropping events until a first GET succeeds; interval-mode behavior is unchanged. 4. Install rejection: declaring plugin.storage.changed without storage: true now fails at install, like reactions do. 5. Docs: backend-api.md describes the dynamic plugin:<pluginId> counters alongside the fixed household kinds; the plugin guide covers the restart promise, revision comparison, phone behavior, and the install failure. Tests: server unit + real-server revision/restart tests, client tracker visible-mode tests, platform install-rejection test; full client (891) and server (713) suites green. * Address jherforth's re-review on plugin storage push (#347) - Guide: drop the skip-your-own-revisions advice — one event can stand for several writes (100 ms coalescing, latest-counter-only changes check), so skipping your own revision can lose another display's write. Keep the warning reworded: don't write storage in response to this event, or you'll loop. Revision fields stay, harmless. - Remove the onResponse hook's plugin-storage backstop: it only matched the three storage routes, all of which bump in-route, so it could never fire. Drop the now-unused pluginStorageWrite helper and its test with it; storage counters move in-route only. Server suite 712/712. * Drop the last skip-your-own-writes wording (#347) The guide already says not to skip plugin.storage.changed events that carry your own revision, since one event can stand for several writes. The SDK's storage.set doc comment still gave the old advice, as did four internal comments; bring them in line with the guide. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: adamecker <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
Implements #347: plugins can now hear about storage writes without polling.
plugin.storage.changedwith{ pluginId }only — no key, no value — and reloads what it uses. The plugin guide says so explicitly, so authors don't expect a key.GET /api/changescheck. Counters live underplugin:<pluginId>, are created on a plugin's first storage write, and drop on uninstall-with-purge (kept without purge, since the data is kept). The ETag stays deterministic (fixed kinds plus plugin kinds in sorted order);GET /api/changesreturns the full map;household.changedpayloads still carry household kinds only.plugin.storage.changeditself, since the reaction writes plugin storage and would otherwise re-trigger in a loop.Tests: new server unit tests (routing, flush partitioning, ETag determinism, prune), new real-server tests for each write path plus the no-key payload, purge behavior, and ETag movement, and extended client tracker tests including frame isolation (a subscription hears only its own plugin from both the check and the stream). Full client and server suites pass, as do the build and i18n checks.
Generated with Muse AI 🤖