#1 New features

Öppen
öppnade 3 veckor sedan av Tilo15 · 3 kommentarer

Server side attributes

  • pstm- prefix
  • HTML files run through process at compile time, which processes the templates, creates variants, generates Vala code for predicates from Invercargill expressions. Compresses variants in all compression methods at max compression. Result is Vala files referenced in code.
  • Invercargill expressions for pstm-if with all possible pages pre-rendered and the correct one sent based on predicate matrix. Works with pstm-else and pstm-else-if
  • <pstm-guard> element in the <head> tag can cause an error page to be sent when a predicate isn't met.
  • Nested page templates (like spry templates, but precomputed) using a <pstm-template> element in the <head> tag.
  • Template/page/fragment name defined by <pstm-name> in head tag.
  • Reusable fragments precomputed into pages.
  • <pstm-uri> elements in head tag define (templated) routes for the page.
  • Fragment includable using <pstm-fragment>
  • <pstm-input> element in head tag that defines a variable that the fragment accepts as an input. Translates to a stm-def-x-as attribute when composited
  • <pstm-title> translates to a <title> tag but with extra attributes possible for composition, e.g. a template could define <pstm-title suffix> | My App</pstm-title> to append text to the child's title. prefix also supported.
  • <pstm-content> used in templates as the child outlet.
  • pstm-materialise-as attribute can be applied to a body tag of a fragment or template child to define what type of element the body tag should be converted to. Defaults to div
  • <pstm-fragment name="fragName"> embeds a fragment into the template. Any attributes matching a pstm-input of the target are translated to stm-def-x-as attributes
  • pstm-res-x attribute takes a resource name and translates it to a full resource URL e.g. /_statum/resource/xxx and assigns that to attribute x e.g. <img pstm-res-src="logo.webp" /> might translate to <img src="/_statum/resources/logo.webp" />
  • Add resource generator for meson (similar to HTML templates) to generate these resources.

Client side additions

  • New subscribe and unsubscribe directives.
  • New notify directive with type and message default handler triggers JavaScript alert
  • onErrorHandler, onConfirmHandler, and onNotifyHandler to JS API.
  • stm-def-x-as attribute allows defining a variable available to this element and child elements.
  • Detect any pstm-* tags and add a console warning.
  • Use GET /_statum/channel as an SSE source in a shared worker - channel ID sent in response header. Connection is established on first subscribe directive in the session. Connection is closed when all subscriptions have been unsubscribed.
  • Use PATCH /_statum/channel to manage the channel's subscriptions (to subscribe or unsubscribe from slots)
  • Perhaps use shared worker to hold session scoped slots instead of the cookie hack
Server side attributes - `pstm-` prefix - HTML files run through process at compile time, which processes the templates, creates variants, generates Vala code for predicates from Invercargill expressions. Compresses variants in all compression methods at max compression. Result is Vala files referenced in code. - Invercargill expressions for `pstm-if` with all possible pages pre-rendered and the correct one sent based on predicate matrix. Works with `pstm-else` and `pstm-else-if` - `<pstm-guard>` element in the `<head>` tag can cause an error page to be sent when a predicate isn't met. - Nested page templates (like spry templates, but precomputed) using a `<pstm-template>` element in the `<head>` tag. - Template/page/fragment name defined by `<pstm-name>` in head tag. - Reusable fragments precomputed into pages. - `<pstm-uri>` elements in head tag define (templated) routes for the page. - Fragment includable using `<pstm-fragment>` - `<pstm-input>` element in head tag that defines a variable that the fragment accepts as an input. Translates to a `stm-def-x-as` attribute when composited - `<pstm-title>` translates to a `<title>` tag but with extra attributes possible for composition, e.g. a template could define `<pstm-title suffix> | My App</pstm-title>` to append text to the child's title. `prefix` also supported. - `<pstm-content>` used in templates as the child outlet. - `pstm-materialise-as` attribute can be applied to a `body` tag of a fragment or template child to define what type of element the `body` tag should be converted to. Defaults to `div` - `<pstm-fragment name="fragName">` embeds a fragment into the template. Any attributes matching a `pstm-input` of the target are translated to `stm-def-x-as` attributes - `pstm-res-x` attribute takes a resource name and translates it to a full resource URL e.g. `/_statum/resource/xxx` and assigns that to attribute `x` e.g. `<img pstm-res-src="logo.webp" />` might translate to `<img src="/_statum/resources/logo.webp" />` - Add resource generator for meson (similar to HTML templates) to generate these resources. Client side additions - New `subscribe` and `unsubscribe` directives. - New `notify` directive with `type` and `message` default handler triggers JavaScript `alert` - `onErrorHandler`, `onConfirmHandler`, and `onNotifyHandler` to JS API. - `stm-def-x-as` attribute allows defining a variable available to this element and child elements. - Detect any `pstm-*` tags and add a console warning. - Use `GET /_statum/channel` as an SSE source in a shared worker - channel ID sent in response header. Connection is established on first `subscribe` directive in the session. Connection is closed when all subscriptions have been unsubscribed. - Use `PATCH /_statum/channel` to manage the channel's subscriptions (to subscribe or unsubscribe from slots) - Perhaps use shared worker to hold `session` scoped slots instead of the cookie hack
Billy Barrow kommenterad 2 veckor sedan
Ägare
  • stm-markdown attribute (like stm-html and stm-text) escapes all HTML so can be from untrusted input.
  • stm-h-demotion attribute that demotes all child header tags by a particular amount (must occurr after stm-html and stm-markdown)
  • form directive allowing submit (with optional action and URI) and reset of a form on the template by ID
  • Add before and after evaluation JS Api hooks
  • Automatic "set" directive sent by API if as_at date is before the last update of a state
  • Make HTML preprocessor usable as a meson generator instead of specifying command each time
- `stm-markdown` attribute (like stm-html and stm-text) escapes all HTML so can be from untrusted input. - `stm-h-demotion` attribute that demotes all child header tags by a particular amount (must occurr after stm-html and stm-markdown) - `form` directive allowing submit (with optional action and URI) and reset of a form on the template by ID - Add before and after evaluation JS Api hooks - Automatic "set" directive sent by API if `as_at` date is before the last update of a state - Make HTML preprocessor usable as a meson generator instead of specifying command each time
Billy Barrow kommenterad 2 dagar sedan
Ägare

Also add:

  • stm-data-form attribute which references a form element to send data from along with the action
  • stm-selected which can set the selected value on a <select> element.
  • stm-open which specifies an element to "open" when clicked (e.g. a <dialog>) by ID. If the element specifying the stm-open attribute also has a stm-action then the open happens after the action successfully competes.
  • stm-close does the opposite to stm-open also completing only after any actions if present.
  • stm-dialog can be used only on <dialog> elements and can be set to either "confirm" or "alert". Statum will automatically override the default Statum alert and confirm behaviour to show that dialog instead of using the JavaScript alert() and confirm() functions. For type " confirm" one button must have a stm-confirm attribute on it (boolean attribute). This designates it as the confirm button for the dialog, overriding the default stm-confirm attribute behaviour
Also add: - stm-data-form attribute which references a form element to send data from along with the action - stm-selected which can set the selected value on a `<select>` element. - stm-open which specifies an element to "open" when clicked (e.g. a `<dialog>`) by ID. If the element specifying the `stm-open` attribute also has a `stm-action` then the open happens after the action successfully competes. - stm-close does the opposite to `stm-open` also completing only after any actions if present. - stm-dialog can be used only on `<dialog>` elements and can be set to either "confirm" or "alert". Statum will automatically override the default Statum alert and confirm behaviour to show that dialog instead of using the JavaScript `alert()` and `confirm()` functions. For type " confirm" one button must have a `stm-confirm` attribute on it (boolean attribute). This designates it as the confirm button for the dialog, overriding the default `stm-confirm` attribute behaviour
clanker kommenterad 1 dag sedan
Deltagare

Statum friction points (from the Percipio migration)

Concrete issues, footguns, and missing ergonomics encountered while building a real app (Percipio) on Statum. Each item lists what happened, the impact, and a suggested improvement. Items are grouped by side and roughly ordered by how much pain they caused.


Already noted

  • stm-data-form — an attribute letting an action element (e.g. a button) reference a <form> whose named inputs to send with the action. Would have avoided the "submit vs save-draft share one form" gymnastics (statum.js collectData only reads form inputs when the trigger is a <form>, so a co-equal "Save draft" button sent an empty body).
  • stm-selected — set the selected value on a <select>. Would have avoided the per-option stm-attribute.selected="q.skill == '…'" verbosity (stm-attribute.value on a <select> only calls setAttribute('value', …), which does not change the selected option).

Client / JS side

1. Slot names that shadow JS globals silently break stm-if (blank page) — severe

Naming a slot eval made every stm-if="… eval == null …" evaluate wrong: inside with(scope){ … }, an identifier not in scope resolves to the global (eval is the built-in function), so eval == null was always false and eval != null always true. Result: a fully blank page, no console error.

  • Impact: hours of debugging; the page was blank with zero diagnostics.
  • Improve: reserve/validate slot type names against JS globals & reserved words at registration (warn or reject), and/or make the expression evaluator resolve unknown identifiers to undefined instead of falling through to window.

2. Referencing an unset slot makes the whole expression silently falsy — severe

evalExpr is new Function('scope','with(scope){try{return (EXPR);}catch(e){return undefined;}}'). If EXPR mentions a slot that isn't currently held, the reference throws a ReferenceError, the inner catch returns undefined, so stm-if is falsy and stm-text is empty — silently. unsetSlot == null does not evaluate to true (as plain JS undefined == null would), because the entire expression is swallowed by the catch.

  • Impact: the challenge page was blank because stage-A's stm-if mentioned evaluation (unset for a fresh attempt). Very hard to spot — there's no error.
  • Improve: use an evaluator that resolves missing scope members to undefined (not throw), so unsetSlot == nullundefined == nulltrue as expected. Only property access on a truly-undefined value should be caught. Also: a debug mode that logs which stm-if/expression evaluated to undefined (and why) would have saved hours.

3. [hidden] is defeated by ordinary author CSS — severe, sneaky

Statum toggles stm-if branches and stm-for holders with the hidden attribute, but the UA [hidden]{display:none} is lower specificity than a normal rule like .challenge-list li{display:flex}. So the hidden loop holder leaked as a persistent empty row at the top of the list.

  • Impact: "phantom empty entry" with no clue it was a CSS-specificity issue.
  • Improve: inject [hidden]{display:none !important} from statum.js itself (so it can't be overridden), or switch to a dedicated stm-hidden class with !important. At minimum, document this prominently.

4. stm-text on an editable <input>/<textarea> resets the user's typing

Binding a form field's value with stm-text is a one-way state→DOM binding; on any re-evaluation (e.g. after a co-equal action's notify) it wipes what the user just typed, resetting it to the stale state value.

  • Impact: "Save draft clears my answers" until I worked around it by having the action echo the saved values back via an update.
  • Improve: an "initial-value / set-once" binding (e.g. stm-value) that sets the value when first bound and then leaves it alone while the field is editable (or while focused), and/or document the pattern for editable fields.

5. collectData ignores the enclosing form for non-<form> triggers

A <button stm-action=…> inside a <form> collects only its own stm-data-* attributes, not the form's named inputs. (Covered by the stm-data-form note; reiterating the symptom — multiple data-collecting actions on one form are impossible today without a hidden intent field + onclick hack.)

6. No first-class spinner/busy binding

setBusyState adds the stm-busy-class classes to the form (the action element), and disabling cascades to children — fine — but to actually show a spinner I had to hand-roll <span class="spinner"> elements plus CSS that keys off .btn-loading. A built-in stm-spinner (inject a spinner while busy) would be a nice ergonomic.


Server / Vala side

7. int64 values segfault during JSON serialisation — severe

GObjectMapping.to_properties → InvercargillJson from_element crashes at node.set_int(element.as<int64?>()) for any int64-typed value. GDB backtrace confirmed the segfault in encryption_provider_author_propertiesJsonElement.from_properties. (as<int64?>() on an int64 ValueElement returns something that crashes Json.Node.set_int.)

  • Impact: the login action hard-crashed the whole server (segfault) until I purged every int64 from slot state.
  • Improve: fix the int64 path in from_element/ValueElement.as<int64?>. This is the single most dangerous footgun — it crashes the process.

8. int (32-bit) serialises as a string, not a number

to_properties on a GObject with an int field produces a JSON string ("best_score": "-1") instead of a number. (The int32 branch in from_element apparently isn't reached for ValueElements; it falls through to the assignable_to<string>() transform fallback.) JS coercion mostly hides it for display, but it breaks round-tripping.

  • Impact: slot numbers render as quoted strings; server-side get_int on a round-tripped value fails (the encrypted JSON stored a string).
  • Improve: make to_properties/from_element emit numbers for numeric ValueElements consistently.

9. A bool field in encrypted/private data segfaults

set_private_typedauthor_propertiesfrom_properties crashes on a bool property (same family as #7). The public path handles bools fine; the encryption path does not.

  • Impact: had to strip bools out of AuthPrivate and derive admin status from the signed public role instead.
  • Improve: make the encryption serialisation path use the same robust serialiser as the public path (don't crash on bool/int64).

10. GObjectMapping cannot serialise array/list fields — foundational

Object[] / Lot<T> / Gee.List properties are silently skipped by to_properties. Every list-bearing slot must be hand-built as a PropertyDictionary with a JsonElement array, and set_typed<T> / update_held<T> are unusable for any model containing a list. For a real app (where nearly every page is a list) this is the dominant source of boilerplate.

  • Impact: wrote a custom StateJson.list<T>(lot) helper; can never use the typed-model convenience for list pages; update_held is effectively dead for real slots.
  • Improve: support list/collection fields in GObjectMapping (emit JSON arrays), and/or ship a built-in State.json_array<T>(lot) / State.nested(model) helper so the common "list of row models" pattern is one call instead of ~10 lines of PropertyDictionary plumbing.

11. TypedStatumAction<TPrivate>.request_private is broken (typeof(TPrivate)void)

request_private calls GObjectMapping.from_properties(typeof(TPrivate), …), but at runtime typeof(TPrivate) resolves to void (Vala generic erasure) → g_object_new_with_properties: assertion G_TYPE_IS_OBJECT failed → the private data maps to null. Had to bypass it and read request.action_private manually via a per-action read() helper.

  • Impact: every typed action's request_private silently returned null.
  • Improve: work around the Vala typeof(T)-in-generics issue (pass the GType explicitly from the subclass, or capture it another way) so request_private works as documented. (The example's counter action apparently works — worth diffing why; possibly same-compile-unit vs cross-namespace type registration.)

12. Action-private decryption failures are silent

decrypt_action_private returns an empty PropertyDictionary on any failure (namespace mismatch, bad blob) instead of throwing/log; requires_valid_private is false by default, so a mis-sealed action just runs with an empty context.

  • Impact: "retry button does nothing / bad_private" style bugs are invisible; the action happily proceeded with attempt_id == 0.
  • Improve: default requires_valid_private to true for typed actions (it is, but for plain actions consider it too), and log decryption failures with the namespace so mismatches are diagnosable.

13. The topic system can't push list payloads

A topic's StateModifier serialises via to_properties, so it can only carry scalar/registered-object state — not a per-question results list. I couldn't use topics for live eval results and fell back to a manual state_service.update(key, …) with a stashed slot key on the background worker.

  • Improve: let topic modifiers author a full PropertyDictionary (with JsonElement arrays), or provide a blessed "background worker → held slot" push keyed by a business id, so the common "job finishes → update the client's view" case doesn't require hand-stashing slot keys on the worker.

14. No built-in auth / identity primitive

Identity is entirely DIY: build an auth SESSION slot, read it back from request.held in every handler, derive admin from the signed public role, hand-roll guards. Getting the slot shape, scope, key persistence, and the "rebuild-each-entrypoint" pattern right was a large part of the effort.

  • Improve: a blessed "identity/auth slot" pattern + helpers (request.current_user, require_admin(), a server-side guard that issues a navigate/error directive) and documented guidance on SESSION vs DEVICE scope and static-key persistence.

15. Static crypto keys are required for persistent sessions — but only a console whisper

Without web-config.json static keys, every signed SESSION/DEVICE slot is invalidated on restart (ephemeral keys) → all users logged out. There's only a quiet console warning at startup.

  • Improve: fail loud (or a very prominent warning) when slots are used for identity without persistent keys, or auto-generate + persist keys by default.

16. Properties vs PropertyDictionary cast fragility

GObjectMapping.to_properties returns the Properties interface, but to add a list/nested value I had to cast to the concrete PropertyDictionary to use its indexer ((PropertyDictionary) to_properties(pub)). Fragile and undocumented.

  • Improve: have to_properties return PropertyDictionary directly (or give Properties a typed setter / set_json(key, element) method).

17. update_held<T> is effectively unusable for real models

Because of #10, update_held (which round-trips through to_properties/from_properties) can't preserve list fields — it would drop them. The real pattern is "reload from DB and update(key, State)". This isn't documented and update_held looks temptingly usable until your list vanishes.

  • Improve: either lift the list limitation (so update_held is safe) or document loudly that update_held is scalar-only and the canonical mutation pattern is rebuild-from-DB + update.

18. Page route params don't reach actions

Actions resolve to GUID endpoints, so a page's {slug}/{id} aren't in request.route_params. The documented workaround is to seal context into the action's private data — fine, but verbose and easy to forget.

  • Improve: a first-class "page context" slot/mechanism that actions can read without per-action sealing (e.g. automatically include the originating page's resolved route params in StatumRequest).

Cross-cutting / DX

19. Blank pages with zero diagnostics

The combination of #1, #2, and #3 produces blank pages (not even the preloader) with no console error and no server error. This was the hardest class of bug. A "statum debug" mode that logs: which slots are in scope, which stm-if evaluated falsy and why, and any with(scope) ReferenceErrors, would be transformative for adoption.

20. X-Statum-Slot / X-Statum-Private header shapes were hard to construct for testing

Figuring out the reference form (key=…; as_at=…) vs base64-frame form, and that action private travels on X-Statum-Private, took reverse-engineering the JS client. A short "testing Statum handlers with curl" recipe in the docs (or a helper that prints the exact headers a client would send) would help a lot.

21. Documentation gaps vs the example

The example app is minimal (counter + a scalar announcement). It doesn't exercise: list-bearing slots, encrypted private data, update_held, typed action private, editable form fields, selects, or background pushes — i.e. exactly the things that bit. A richer example (or a "patterns" doc) covering lists + private + actions would have prevented most of the above.


If I had to rank the top 5

  1. #7 int64 segfault — crashes the process; silent and catastrophic.
  2. #2 unset-slot → silent falsy → blank page — no diagnostics; affects every page that references an optional slot.
  3. #10 no list serialisation — makes the typed-model API unusable for real apps and forces verbose PropertyDictionary plumbing everywhere.
  4. #3 [hidden] vs author CSS — sneaky, app-wide risk, trivial to fix in the library.
  5. #11 broken request_private — a documented API that silently returns null.
# Statum friction points (from the Percipio migration) Concrete issues, footguns, and missing ergonomics encountered while building a real app (Percipio) on Statum. Each item lists **what happened**, the **impact**, and a suggested **improvement**. Items are grouped by side and roughly ordered by how much pain they caused. --- ## Already noted - **`stm-data-form`** — an attribute letting an action element (e.g. a button) reference a `<form>` whose named inputs to send with the action. Would have avoided the "submit vs save-draft share one form" gymnastics (`statum.js` `collectData` only reads form inputs when the trigger *is* a `<form>`, so a co-equal "Save draft" button sent an empty body). - **`stm-selected`** — set the selected value on a `<select>`. Would have avoided the per-option `stm-attribute.selected="q.skill == '…'"` verbosity (`stm-attribute.value` on a `<select>` only calls `setAttribute('value', …)`, which does not change the selected option). --- ## Client / JS side ### 1. Slot names that shadow JS globals silently break `stm-if` (blank page) — **severe** Naming a slot `eval` made every `stm-if="… eval == null …"` evaluate wrong: inside `with(scope){ … }`, an identifier not in scope resolves to the **global** (`eval` is the built-in function), so `eval == null` was always `false` and `eval != null` always `true`. Result: a fully blank page, no console error. - **Impact:** hours of debugging; the page was blank with zero diagnostics. - **Improve:** reserve/validate slot type names against JS globals & reserved words at registration (warn or reject), **and/or** make the expression evaluator resolve unknown identifiers to `undefined` instead of falling through to `window`. ### 2. Referencing an *unset* slot makes the whole expression silently falsy — **severe** `evalExpr` is `new Function('scope','with(scope){try{return (EXPR);}catch(e){return undefined;}}')`. If `EXPR` mentions a slot that isn't currently held, the reference throws a `ReferenceError`, the inner `catch` returns `undefined`, so `stm-if` is falsy and `stm-text` is empty — silently. `unsetSlot == null` does **not** evaluate to `true` (as plain JS `undefined == null` would), because the *entire* expression is swallowed by the catch. - **Impact:** the challenge page was blank because stage-A's `stm-if` mentioned `evaluation` (unset for a fresh attempt). Very hard to spot — there's no error. - **Improve:** use an evaluator that resolves missing scope members to `undefined` (not throw), so `unsetSlot == null` → `undefined == null` → `true` as expected. Only property access on a truly-`undefined` value should be caught. Also: a **debug mode** that logs which `stm-if`/expression evaluated to `undefined` (and why) would have saved hours. ### 3. `[hidden]` is defeated by ordinary author CSS — **severe, sneaky** Statum toggles `stm-if` branches and `stm-for` holders with the `hidden` attribute, but the UA `[hidden]{display:none}` is lower specificity than a normal rule like `.challenge-list li{display:flex}`. So the hidden loop *holder* leaked as a persistent empty row at the top of the list. - **Impact:** "phantom empty entry" with no clue it was a CSS-specificity issue. - **Improve:** inject `[hidden]{display:none !important}` from `statum.js` itself (so it can't be overridden), or switch to a dedicated `stm-hidden` class with `!important`. At minimum, document this prominently. ### 4. `stm-text` on an editable `<input>`/`<textarea>` resets the user's typing Binding a form field's value with `stm-text` is a one-way state→DOM binding; on any re-evaluation (e.g. after a co-equal action's `notify`) it wipes what the user just typed, resetting it to the stale state value. - **Impact:** "Save draft clears my answers" until I worked around it by having the action echo the saved values back via an `update`. - **Improve:** an "initial-value / set-once" binding (e.g. `stm-value`) that sets the value when first bound and then leaves it alone while the field is editable (or while focused), and/or document the pattern for editable fields. ### 5. `collectData` ignores the enclosing form for non-`<form>` triggers A `<button stm-action=…>` inside a `<form>` collects only its own `stm-data-*` attributes, not the form's named inputs. (Covered by the `stm-data-form` note; reiterating the *symptom* — multiple data-collecting actions on one form are impossible today without a hidden `intent` field + `onclick` hack.) ### 6. No first-class spinner/busy binding `setBusyState` adds the `stm-busy-class` classes to the *form* (the action element), and disabling cascades to children — fine — but to actually show a spinner I had to hand-roll `<span class="spinner">` elements plus CSS that keys off `.btn-loading`. A built-in `stm-spinner` (inject a spinner while busy) would be a nice ergonomic. --- ## Server / Vala side ### 7. `int64` values segfault during JSON serialisation — **severe** `GObjectMapping.to_properties` → InvercargillJson `from_element` crashes at `node.set_int(element.as<int64?>())` for any `int64`-typed value. GDB backtrace confirmed the segfault in `encryption_provider_author_properties` → `JsonElement.from_properties`. (`as<int64?>()` on an int64 `ValueElement` returns something that crashes `Json.Node.set_int`.) - **Impact:** the login action hard-crashed the whole server (segfault) until I purged every `int64` from slot state. - **Improve:** fix the int64 path in `from_element`/`ValueElement.as<int64?>`. This is the single most dangerous footgun — it crashes the *process*. ### 8. `int` (32-bit) serialises as a **string**, not a number `to_properties` on a GObject with an `int` field produces a JSON *string* (`"best_score": "-1"`) instead of a number. (The int32 branch in `from_element` apparently isn't reached for `ValueElement`s; it falls through to the `assignable_to<string>()` transform fallback.) JS coercion mostly hides it for display, but it breaks round-tripping. - **Impact:** slot numbers render as quoted strings; server-side `get_int` on a round-tripped value fails (the encrypted JSON stored a string). - **Improve:** make `to_properties`/`from_element` emit numbers for numeric `ValueElement`s consistently. ### 9. A `bool` field in **encrypted/private** data segfaults `set_private_typed` → `author_properties` → `from_properties` crashes on a `bool` property (same family as #7). The *public* path handles bools fine; the *encryption* path does not. - **Impact:** had to strip `bool`s out of `AuthPrivate` and derive admin status from the signed public `role` instead. - **Improve:** make the encryption serialisation path use the same robust serialiser as the public path (don't crash on bool/int64). ### 10. `GObjectMapping` cannot serialise array/list fields — **foundational** `Object[]` / `Lot<T>` / `Gee.List` properties are silently skipped by `to_properties`. **Every** list-bearing slot must be hand-built as a `PropertyDictionary` with a `JsonElement` array, and `set_typed<T>` / `update_held<T>` are **unusable** for any model containing a list. For a real app (where nearly every page is a list) this is the dominant source of boilerplate. - **Impact:** wrote a custom `StateJson.list<T>(lot)` helper; can never use the typed-model convenience for list pages; `update_held` is effectively dead for real slots. - **Improve:** support list/collection fields in `GObjectMapping` (emit JSON arrays), and/or ship a built-in `State.json_array<T>(lot)` / `State.nested(model)` helper so the common "list of row models" pattern is one call instead of ~10 lines of `PropertyDictionary` plumbing. ### 11. `TypedStatumAction<TPrivate>.request_private` is broken (`typeof(TPrivate)` → `void`) `request_private` calls `GObjectMapping.from_properties(typeof(TPrivate), …)`, but at runtime `typeof(TPrivate)` resolves to `void` (Vala generic erasure) → `g_object_new_with_properties: assertion G_TYPE_IS_OBJECT failed` → the private data maps to `null`. Had to bypass it and read `request.action_private` manually via a per-action `read()` helper. - **Impact:** every typed action's `request_private` silently returned null. - **Improve:** work around the Vala `typeof(T)`-in-generics issue (pass the GType explicitly from the subclass, or capture it another way) so `request_private` works as documented. (The example's counter action apparently works — worth diffing why; possibly same-compile-unit vs cross-namespace type registration.) ### 12. Action-private decryption failures are silent `decrypt_action_private` returns an empty `PropertyDictionary` on any failure (namespace mismatch, bad blob) instead of throwing/log; `requires_valid_private` is false by default, so a mis-sealed action just runs with an empty context. - **Impact:** "retry button does nothing / bad_private" style bugs are invisible; the action happily proceeded with `attempt_id == 0`. - **Improve:** default `requires_valid_private` to `true` for typed actions (it is, but for plain actions consider it too), and **log** decryption failures with the namespace so mismatches are diagnosable. ### 13. The topic system can't push **list** payloads A topic's `StateModifier` serialises via `to_properties`, so it can only carry scalar/registered-object state — not a per-question results list. I couldn't use topics for live eval results and fell back to a manual `state_service.update(key, …)` with a stashed slot key on the background worker. - **Improve:** let topic modifiers author a full `PropertyDictionary` (with `JsonElement` arrays), or provide a blessed "background worker → held slot" push keyed by a business id, so the common "job finishes → update the client's view" case doesn't require hand-stashing slot keys on the worker. ### 14. No built-in auth / identity primitive Identity is entirely DIY: build an `auth` SESSION slot, read it back from `request.held` in every handler, derive admin from the signed public `role`, hand-roll guards. Getting the slot shape, scope, key persistence, and the "rebuild-each-entrypoint" pattern right was a large part of the effort. - **Improve:** a blessed "identity/auth slot" pattern + helpers (`request.current_user`, `require_admin()`, a server-side guard that issues a `navigate`/error directive) and documented guidance on SESSION vs DEVICE scope and static-key persistence. ### 15. Static crypto keys are required for persistent sessions — but only a console whisper Without `web-config.json` static keys, every signed `SESSION`/`DEVICE` slot is invalidated on restart (ephemeral keys) → all users logged out. There's only a quiet console warning at startup. - **Improve:** fail loud (or a very prominent warning) when slots are used for identity without persistent keys, or auto-generate + persist keys by default. ### 16. `Properties` vs `PropertyDictionary` cast fragility `GObjectMapping.to_properties` returns the `Properties` interface, but to add a list/nested value I had to cast to the concrete `PropertyDictionary` to use its indexer (`(PropertyDictionary) to_properties(pub)`). Fragile and undocumented. - **Improve:** have `to_properties` return `PropertyDictionary` directly (or give `Properties` a typed setter / `set_json(key, element)` method). ### 17. `update_held<T>` is effectively unusable for real models Because of #10, `update_held` (which round-trips through `to_properties`/`from_properties`) can't preserve list fields — it would drop them. The real pattern is "reload from DB and `update(key, State)`". This isn't documented and `update_held` looks temptingly usable until your list vanishes. - **Improve:** either lift the list limitation (so `update_held` is safe) or document loudly that `update_held` is scalar-only and the canonical mutation pattern is rebuild-from-DB + `update`. ### 18. Page route params don't reach actions Actions resolve to GUID endpoints, so a page's `{slug}`/`{id}` aren't in `request.route_params`. The documented workaround is to seal context into the action's private data — fine, but verbose and easy to forget. - **Improve:** a first-class "page context" slot/mechanism that actions can read without per-action sealing (e.g. automatically include the originating page's resolved route params in `StatumRequest`). --- ## Cross-cutting / DX ### 19. Blank pages with zero diagnostics The combination of #1, #2, and #3 produces *blank pages* (not even the preloader) with **no console error and no server error**. This was the hardest class of bug. A "statum debug" mode that logs: which slots are in scope, which `stm-if` evaluated falsy and why, and any `with(scope)` ReferenceErrors, would be transformative for adoption. ### 20. `X-Statum-Slot` / `X-Statum-Private` header shapes were hard to construct for testing Figuring out the reference form (`key=…; as_at=…`) vs base64-frame form, and that action private travels on `X-Statum-Private`, took reverse-engineering the JS client. A short "testing Statum handlers with curl" recipe in the docs (or a helper that prints the exact headers a client would send) would help a lot. ### 21. Documentation gaps vs the example The example app is minimal (counter + a scalar announcement). It doesn't exercise: list-bearing slots, encrypted private data, `update_held`, typed action private, editable form fields, selects, or background pushes — i.e. exactly the things that bit. A richer example (or a "patterns" doc) covering lists + private + actions would have prevented most of the above. --- ## If I had to rank the top 5 1. **#7 int64 segfault** — crashes the process; silent and catastrophic. 2. **#2 unset-slot → silent falsy → blank page** — no diagnostics; affects every page that references an optional slot. 3. **#10 no list serialisation** — makes the typed-model API unusable for real apps and forces verbose `PropertyDictionary` plumbing everywhere. 4. **#3 `[hidden]` vs author CSS** — sneaky, app-wide risk, trivial to fix in the library. 5. **#11 broken `request_private`** — a documented API that silently returns null.
Logga in för att delta i denna konversation.
Ingen Etikett
Ingen Milsten
Ingen förvärvare
2 Deltagare
Laddar...
Avbryt
Spara
Det finns inget innehåll än.