refactor(reference): adopt durable task notices - #185
Merged
FelineStateMachine merged 1 commit intoJul 20, 2026
Conversation
FelineStateMachine
marked this pull request as ready for review
July 20, 2026 09:46
FelineStateMachine
added a commit
that referenced
this pull request
Jul 20, 2026
…contract (#184) * fix(preact): sync-state quality pass — first-class blocked dispositions, typed enrollment errors, account-gate freshness The template half of #175, following the runtime half in #180. DeviceStatus names why nothing is syncing, not merely that it is not: the one-line Data sync verdict distinguishes owner mismatch, store refusals (no schema, rejected ticket), an unopenable sink record, and the absence of any sync location, with owner-mismatch copy naming the owning account when known. The no-sync hint now leads with ticket enrollment; the compiled-in env-var path is the stated alternative. TicketEnrollForm relays typed refusals verbatim — SyncEnrollmentError, SyncOwnerError, and DataSinkError carry their own remediation, and the generic paste-again line cannot fix an unprovisioned store or a foreign sync owner. AccountGate re-reads the session when the runtime is recreated, gains the owner-mismatch branch offering exactly the two remediations (stop sync releases the election, restore adopts the owner), and surfaces a definite store problem in the backed-up state. Starter mirror and snapshot regenerated. * feat(effects): built-in effect library and the custom-unit authoring contract Closes #141. Grows the #139 effect machinery into a tiered standard library and formalizes the contract for authoring custom units. Contract (effects.ts): - EffectContext gains `op` and `writeCreatedAt` so units can see the operation and measure saved→fate latency. - PermanentEffectError / isPermanentEffectError: a handler throws it to retire an obligation immediately (a hopeless failure) instead of re-arming until maxAttempts. The ledger honors the severity. - requireMutationRuntime + anonymousEffectName expose the seams the library is built on, keeping every built-in on the public authoring surface. Built-in units (schema/effect-library.ts), tiered by risk: - Observation — s.trace (saved→fate span in diagnostics, OTLP-shaped, no vendor coupling), s.debug (dev-only timeline, stripped in PROD). s.log stays in the core beside the runtime recorder. - Data-internal — s.notice (durable, UI-agnostic message queue, idempotent by journal id; closes "a rejected write flashed success"), s.mark (patch the row on fate, convergent, skips a rolled-back insert), s.chain (issue a follow-up verb on synced, no saga API). - External — s.webhook (POST row+fate with journalId as Idempotency-Key; 4xx → permanent, 5xx/429/network → retryable; default 24h delivery window). Runtime: - notice-queue.ts: durable queue (OPFS→localStorage→memory), idempotent enqueue, TTL + dismissal, subscribe; activeNotices diagnostic. - write-ledger.ts installs recordTrace/recordDebug/enqueueNotice/applyMark and owns the queue singleton; public listNotices/subscribeNotices/dismissNotice. - diagnostics.ts: effectTraces, effectDebugTimeline, activeNotices. Preact: useNotices hook + Notices component render the durable queue; a toast stack is a userland wrapper over the same hook. Docs: docs/effects.md (library reference + authoring contract + worked custom unit), teaching-path link from nouns-and-verbs, README reference entries. Tests: per-unit happy/rejection/idempotency, notice-queue idempotence/TTL/ reload, ledger permanent-failure retirement, and a custom unit built purely on the public contract. Reference app adds s.trace to addTask (dogfood); starter mirror + snapshot regenerated. Manifest updated for the three new modules. Deferred, noted for a follow-up: migrating the reference app's hand-rolled task-notice to s.notice (a focused island refactor). * fix(effects): review remediations — durable anonymous-unit identity, notice-queue boot merge, a11y Addresses findings from the multi-agent + Codex review of #184. Anonymous-unit identity (HIGH — silent mis-binding across reloads): - notice/mark/chain no longer take their durable name from a global declaration counter (module-load-order dependent, so a re-armed obligation could bind to the WRONG unit — for chain, the wrong follow-up verb). They are now named `<verb>#<position>` at mutation() time, from the author-chosen verb and their slot in its effects — an identity independent of which module loads first. - trace/debug/webhook are content-named and now share one unit per identity (cachedBuiltin), so reusing one across verbs aggregates instead of throwing a duplicate-name error. webhook keys on url+config. Notice queue: - Boot-window clobber (HIGH): load() now MERGES persisted entries with any enqueued during the async load window instead of overwriting, so a notice an effect enqueues at a boot re-arm is not silently dropped. - TTL drift (MEDIUM): list() is now a pure read; retirement happens in sweep(), wired to a periodic timer in the queue owner; the activeNotices count is computed from the live view so it never overcounts expired entries. mark best-effort (MEDIUM): applyMark swallows a denied/vanished-row patch instead of quarantine-spamming; documented that mark is a convenience over the status column, not a delivery guarantee (use notice/webhook for that). Notices a11y (MEDIUM): the aria-live region stays mounted (hidden) when empty so the first notice is announced; custom children are keyed. Tests: anonymous-unit verb-scoped naming, content-name sharing across verbs, notice boot-merge. All green; build refreshed. * fix(effects): make built-ins replay-safe * refactor(reference): adopt durable task notices (#185) * fix(sync): authenticate PoP preflights and align demo notices (#186) Use proof-of-possession connect URLs for store-status checks during enrollment and boot. Migrate the demo overlay to durable notices and improve recovery/passkey diagnostics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The reference task island still hand-rolls module state, listeners,
publishNotice, anduseTaskNoticefor the exact durable-notice pattern introduced by #184. Keeping the migration stacked on #184 lets the effect library and its reference usage land together while preserving a focused review boundary.Change
s.notice<Task>alongside the existing log and trace effects.<Notices />surface inTaskList.This PR intentionally targets
feat/141-effect-libraryand depends on #184.Testing Completed
deno check apps/reference/src/islands/use-tasks.ts apps/reference/src/islands/TaskList.tsxdeno task test:update:create— 8 passeddeno task test:unit— 460 passeddeno task build— production reference build and secret scan passedResponsible AI Disclosure
Implemented with AI assistance. The resulting diff was scope-reviewed and validated with the repository's type checks, generated-snapshot workflow, unit suite, linting, and production build.