TemplateResourceForm publishes kind 30142 without the #62 created_at guard or the #64 failed-publish cleanup #72

Closed
opened 2026-07-30 22:31:41 +00:00 by laoc · 1 comment
Owner

TemplateResourceForm.svelte publishes kind 30142 with neither guard from #62 nor #64. Found by TestOER while chasing a loose end on #64, measured at e0aa0aba.

The shape

// src/lib/components/forms/TemplateResourceForm.svelte ~160-165
const template = { kind: 30142, tags, content };   // <- no created_at
const built  = await factory.build(template);
const signed = await factory.sign(built);
eventStore.add(signed);                             // <- BEFORE the publish
await publishEvent(signed, []);                     // <- no failure path at all

publishEvent — unlike publishEventOptimistic — has no eventStore.remove and no uncacheEvent. So the optimistic add has nothing to undo it.

Measured at e0aa0aba (TestOER)

T1 — same-second edit, relay healthy throughout (#62's bug):

afterV1                       V1
afterV2 (same-second edit)    V1     <- edit lost
afterV3 (newer, control)      V3     <- harness does see updates
v2 publish reported success   true

right after the edit:   memory=V2  IDB=V1   publish reported success
after a reload:         memory=EMPTY  IDB=V1

The user sees their edit, the app says saved, it is gone on reload.

T2 — failed publish (#64's bug):

publish reported success      false
IDB after failure             PHANTOM
IDB after reload, relay down  PHANTOM   <- persists

T2c — identical sequence through publishEventOptimistic, same run, same relay: IDB after failure = GOOD. So the difference is the call shape, not the fixture.

Reachability — traced, upgrading TestOER's scope

TestOER measured the call shape but flagged that they had not confirmed a user can reach edit mode there. I traced it:

create/resource/[variant=resourceVariant]/+page.svelte:148   <TemplateResourceForm editNaddr={data.editNaddr} />
                                                     :77-81   the wizard's edit fetch is SKIPPED when templateNaddr is set,
                                                              "that component handles edit prefill itself"
TemplateResourceForm.svelte:113                       isEditMode = !!editNaddr && !editTarget?.error
                            :155                      existingDTag: isEditMode && resourceEvent ? ... : undefined
forms/[naddr]/create-resource/+page.svelte:11          <TemplateResourceForm templateNaddr editNaddr />

Edit mode is wired deliberately on two routes, and the wizard explicitly defers to it. So this is the designed edit path for any resource authored from a kind-30168 form template — not latent. (I traced the code; I did not drive the UI.)

Why the #64 sweep missed it

#64 looked for publish sites with no eventStore.add. This one has one — just on the wrong side of the publish. Grepping for an absence cannot find a misordering. Credit to TestOER for the framing.

The wider question this opens

eventStore.add before a non-optimistic publishEvent is not unique to this file. A grep of src/ returns ~70 eventStore.add call sites; most are loaders (correct) or add-after-success (correct), but the add-before-publishEvent shape appears at several more, e.g. helpers/community.js:138, services/relay-list-backfill.js:50, components/calendar/CalendarManagement.svelte:111, components/CreateCommunityModal.svelte:351. Not audited — listed so the next person starts from a candidate set rather than a blank page. Only sites publishing a CACHEABLE kind (event-cache.svelte.js CACHEABLE_KINDS) can produce the #64 symptom.

`TemplateResourceForm.svelte` publishes kind 30142 with **neither** guard from #62 nor #64. Found by TestOER while chasing a loose end on #64, measured at `e0aa0aba`. ## The shape ```js // src/lib/components/forms/TemplateResourceForm.svelte ~160-165 const template = { kind: 30142, tags, content }; // <- no created_at const built = await factory.build(template); const signed = await factory.sign(built); eventStore.add(signed); // <- BEFORE the publish await publishEvent(signed, []); // <- no failure path at all ``` `publishEvent` — unlike `publishEventOptimistic` — has no `eventStore.remove` and no `uncacheEvent`. So the optimistic add has nothing to undo it. ## Measured at `e0aa0aba` (TestOER) **T1 — same-second edit, relay healthy throughout (#62's bug):** ``` afterV1 V1 afterV2 (same-second edit) V1 <- edit lost afterV3 (newer, control) V3 <- harness does see updates v2 publish reported success true right after the edit: memory=V2 IDB=V1 publish reported success after a reload: memory=EMPTY IDB=V1 ``` The user sees their edit, the app says saved, it is gone on reload. **T2 — failed publish (#64's bug):** ``` publish reported success false IDB after failure PHANTOM IDB after reload, relay down PHANTOM <- persists ``` **T2c — identical sequence through `publishEventOptimistic`**, same run, same relay: `IDB after failure = GOOD`. So the difference is the call shape, not the fixture. ## Reachability — traced, upgrading TestOER's scope TestOER measured the call shape but flagged that they had not confirmed a user can reach edit mode there. I traced it: ``` create/resource/[variant=resourceVariant]/+page.svelte:148 <TemplateResourceForm editNaddr={data.editNaddr} /> :77-81 the wizard's edit fetch is SKIPPED when templateNaddr is set, "that component handles edit prefill itself" TemplateResourceForm.svelte:113 isEditMode = !!editNaddr && !editTarget?.error :155 existingDTag: isEditMode && resourceEvent ? ... : undefined forms/[naddr]/create-resource/+page.svelte:11 <TemplateResourceForm templateNaddr editNaddr /> ``` Edit mode is wired deliberately on two routes, and the wizard explicitly defers to it. So this is the designed edit path for any resource authored from a kind-30168 form template — not latent. (I traced the code; I did not drive the UI.) ## Why the #64 sweep missed it #64 looked for publish sites with **no** `eventStore.add`. This one *has* one — just on the wrong side of the publish. Grepping for an absence cannot find a misordering. Credit to TestOER for the framing. ## The wider question this opens `eventStore.add` before a non-optimistic `publishEvent` is not unique to this file. A grep of `src/` returns ~70 `eventStore.add` call sites; most are loaders (correct) or add-after-success (correct), but the add-before-`publishEvent` shape appears at several more, e.g. `helpers/community.js:138`, `services/relay-list-backfill.js:50`, `components/calendar/CalendarManagement.svelte:111`, `components/CreateCommunityModal.svelte:351`. **Not audited** — listed so the next person starts from a candidate set rather than a blank page. Only sites publishing a CACHEABLE kind (`event-cache.svelte.js` `CACHEABLE_KINDS`) can produce the #64 symptom.
laoc closed this issue 2026-07-31 09:23:53 +00:00
Author
Owner

Fixed by PR #73, merged to dev as 1393efe8.

Closed on measurement, not on assertion. @TestOER drove the real /create/resource/amb?edit=<naddr> route with a seeded kind-30168 template — real component, real factory + signer, real publish-service, real IndexedDB — so reachability is observed, not traced:

10cdb4f8 control, this file reverted to e0aa0aba
T1 same-second edit predecessor 1785488952 -> published 1785488953, IDB = V2-EDIT (2/2 runs, tie forced deterministically) published 1785489287 = exact tie, IDB = V1-ORIGINAL, navigated as though it worked
T2 failed publish error shown, no navigation, IDB = GOOD before and after reload, detail page renders the pre-edit version IDB = PHANTOM, survives reload

Two corrections to my own write-up, both from that run:

  • I predicted T2 would end EMPTY. It ends GOOD. The restore machinery #64 needed (uncacheEvent + recacheEvent) exists because publishEventOptimistic destroys the cached predecessor before it knows the publish failed. This path never adds anything before the publish lands, so the predecessor's row is never touched. A restore here would be dead code — deliberately not added.
  • Failed create: error, no navigation, 0 rows anywhere in IndexedDB (measured against a whole-store dump, not just the address).

Suite re-run on the merged tree, git rev-parse HEAD printed in the same shell before and after: 1393efe8, 474 files / 5231 tests, 0 failed, exit 0.

One boundary this fix deliberately does not cover — create mode is unguarded and the d tag is a url the user types, so a same-second create at an address that already holds a resource still diverges. Measured by TestOER, filed separately as #74.

Evidence: RESEARCH/ISSUE_72_TEMPLATE_RESOURCE_FORM_GUARDS.md.

Fixed by PR #73, merged to `dev` as `1393efe8`. **Closed on measurement, not on assertion.** @TestOER drove the real `/create/resource/amb?edit=<naddr>` route with a seeded kind-30168 template — real component, real factory + signer, real `publish-service`, real IndexedDB — so reachability is *observed*, not traced: | | `10cdb4f8` | control, this file reverted to `e0aa0aba` | |---|---|---| | **T1** same-second edit | predecessor `1785488952` -> published `1785488953`, IDB = `V2-EDIT` (2/2 runs, tie forced deterministically) | published `1785489287` = exact tie, IDB = `V1-ORIGINAL`, **navigated as though it worked** | | **T2** failed publish | error shown, no navigation, IDB = `GOOD` before *and* after reload, detail page renders the pre-edit version | IDB = `PHANTOM`, survives reload | Two corrections to my own write-up, both from that run: - I predicted T2 would end **EMPTY**. It ends **GOOD**. The restore machinery #64 needed (`uncacheEvent` + `recacheEvent`) exists because `publishEventOptimistic` *destroys* the cached predecessor before it knows the publish failed. This path never adds anything before the publish lands, so the predecessor's row is never touched. A restore here would be dead code — deliberately not added. - Failed **create**: error, no navigation, **0 rows anywhere in IndexedDB** (measured against a whole-store dump, not just the address). Suite re-run on the merged tree, `git rev-parse HEAD` printed in the same shell before and after: `1393efe8`, **474 files / 5231 tests, 0 failed, exit 0**. One boundary this fix deliberately does not cover — create mode is unguarded and the `d` tag is a url the user types, so a same-second create at an address that already holds a resource still diverges. Measured by TestOER, filed separately as #74. Evidence: `RESEARCH/ISSUE_72_TEMPLATE_RESOURCE_FORM_GUARDS.md`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app#72
No description provided.