diff --git a/client/client.js b/client/client.js index 32e0a2c..5a4f39d 100644 --- a/client/client.js +++ b/client/client.js @@ -385,6 +385,16 @@ window.__ModuleLoader__.load({ */ constructor(ctx) { this.ctx = ctx + // Every field the projection reads must exist BEFORE anything projects. + // + // `SettingsFormModel.bind()` is not a subscription setter: it builds the + // snapshot store from one immediate `project()` call, so the projection + // runs once, right here, inside this constructor. Assigning `describe` + // after `bind` made that first projection read `undefined.getSnapshot()` + // — a TypeError thrown while the inject callback was registering the + // card, which aborted the registration and left the plugin with no + // settings section at all and nothing in the UI to say so. + this.describe = ctx.configForms.describe() this.form = ctx.configForms.get(SETTINGS_ENTRY) this.model = new SettingsFormModel( this.form, @@ -399,8 +409,6 @@ window.__ModuleLoader__.load({ })), ) this.store = this.model.bind(() => this.projection()) - /** The describe mirror that knows whether a secret is configured. */ - this.describe = ctx.configForms.describe() } /** Everything the page renders, rebuilt whenever the form or a draft moves. */ diff --git a/test/client.test.js b/test/client.test.js index 63ea60d..55b3f01 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -109,6 +109,11 @@ function makePrimitives(record) { bind(project) { this.project = project + // Faithful to the framework: `bind` is not a subscription setter, it + // builds the snapshot store from ONE immediate projection. A stub that + // only stored the projection would let a controller read a field it has + // not assigned yet — which is exactly the bug this reproduces. + project() return { getSnapshot: () => project(), subscribe: () => () => {} } }