diff --git a/.project/decisions/2026-03-17-1800-sparql-delete-for-orm-objects.md b/.project/decisions/2026-03-17-1800-sparql-delete-for-orm-objects.md new file mode 100644 index 0000000..124be79 --- /dev/null +++ b/.project/decisions/2026-03-17-1800-sparql-delete-for-orm-objects.md @@ -0,0 +1,81 @@ +# Use SPARQL DELETE instead of ORM ngSet.delete() for object removal + +**Date:** 2026-03-17 18:00 +**Status:** Accepted + +## Context + +Leaving an event requires deleting the user's `Participation` object from the NextGraph store. The ORM's `DeepSignalSet.delete()` method updates the local reactive state (UI reflects the change immediately) but the deletion does not persist to the broker — after page refresh, the participation reappears. + +## Options Considered + +### Option A: ORM `ngSet.delete(item)` + +The ORM README shows `dogs.delete(aDog)` as the intended API. Internally, `.delete()` generates a `{ op: "remove", path: "/" }` patch, delivered via microtask to `OrmSubscription.onSignalObjectUpdate`, which calls `ng.graph_orm_update()`. + +**Arguments for:** +- Official ORM API, shown in README examples +- Immediate local reactive update (instant UI feedback) + +**Arguments against:** +- Does not persist in practice: `delete()` returns `true`, local set updates, but after refresh the object is back +- The `graph_orm_update` WASM call may not correctly handle "remove" patches for top-level set objects (possible engine bug) +- No error is thrown — fails silently + +### Option B: `ng.sparql_update()` with SPARQL DELETE + +Bypass the ORM patch mechanism entirely. Use `DELETE WHERE { GRAPH { ?p ?o } }` to remove all RDF triples for the object. + +**Arguments for:** +- Works: deletion persists across page refresh +- The broker confirms via `TORMO became invalid` + `GraphOrmUpdate` remove, which reactively removes the item from the ORM set +- Direct control over RDF triple removal + +**Arguments against:** +- Not instant: UI update waits for the SPARQL round-trip + broker `GraphOrmUpdate` callback (near-instant in practice, ~50ms) +- Must not combine with `ngSet.delete()` — running both causes CRDT conflicts where the item reappears + +### Option C: `ngSet.delete()` + `ng.sparql_update()` together + +Use `.delete()` for instant UI and SPARQL for persistence. + +**Arguments for:** +- Instant UI feedback + guaranteed persistence + +**Arguments against:** +- **Does not work**: the ORM `.delete()` patch and the SPARQL DELETE backend update conflict in the CRDT, resulting in neither UI change nor persistence + +## Decision + +**Option B: SPARQL DELETE only.** The broker sends back a `GraphOrmUpdate` with `op: "remove"` that reactively removes the item from the ORM set, so the UI still updates — just not synchronously. + +Do NOT call `ngSet.delete()` alongside `sparql_update()` — they conflict. + +## Implementation + +```typescript +// In FestipodDataContext.tsx leaveEvent(): +const session = await sessionPromise; +await ng.sparql_update( + session.session_id, + `DELETE WHERE { GRAPH <${partGraph}> { <${partId}> ?p ?o } }`, + partGraph, +); +``` + +Imports: `ng` from `@ng-org/web`, `sessionPromise` from `../utils/ngSession`. + +## Consequences + +**Positive:** +- Deletion actually persists +- Single source of truth (broker → ORM → UI) + +**Negative:** +- Slight UI delay (~50ms) vs instant for property mutations +- Pattern diverges from ORM README examples + +**Risks:** +- If `ng.sparql_update` API changes, this breaks +- Other delete operations (if added) must follow the same pattern +- The ORM `ngSet.delete()` bug may be fixed in a future version — revisit when upgrading `@ng-org/orm` diff --git a/.project/knowledge/data-layer.md b/.project/knowledge/data-layer.md index 7c3c747..2ddea3e 100644 --- a/.project/knowledge/data-layer.md +++ b/.project/knowledge/data-layer.md @@ -46,6 +46,26 @@ This is critical: `orm_start_graph` with the private store NURI explicitly opens **Do NOT use `did:ng:i` as scope** — it subscribes to the entire user site via a special code path that doesn't open individual repos, breaking all writes. +### Deleting Objects + +`ngSet.delete(item)` updates the local reactive set but does **not** persist to the broker. Use `ng.sparql_update()` with SPARQL DELETE instead: + +```typescript +import { ng } from '@ng-org/web'; +import { sessionPromise } from '../utils/ngSession'; + +const session = await sessionPromise; +await ng.sparql_update( + session.session_id, + `DELETE WHERE { GRAPH <${item["@graph"]}> { <${item["@id"]}> ?p ?o } }`, + item["@graph"], +); +``` + +The broker sends back a `GraphOrmUpdate` with `op: "remove"` that reactively removes the item from the ORM set. **Do NOT combine with `ngSet.delete()`** — the two operations conflict in the CRDT. + +See [decision record](../decisions/2026-03-17-1800-sparql-delete-for-orm-objects.md) for details. + ### Key files - `src/shared/hooks/useShapeWithDefaults.ts` — Accepts `storeNuri` param, passes to `useShape` diff --git a/src/shared/context/FestipodDataContext.tsx b/src/shared/context/FestipodDataContext.tsx index c931930..1fb502b 100644 --- a/src/shared/context/FestipodDataContext.tsx +++ b/src/shared/context/FestipodDataContext.tsx @@ -24,6 +24,8 @@ import { import type { FpEvent, FpUserProfile, FpParticipation } from '../shapes/orm/festipodShapes.typings'; import { bootstrapWallet, type BootstrapResult } from '../utils/ngBootstrap'; import { ensureGraphNuri } from '../utils/ngGraph'; +import { sessionPromise } from '../utils/ngSession'; +import { ng } from '@ng-org/web'; // ============================================================================ // Context interface @@ -334,12 +336,30 @@ function useNgData(): FestipodDataContextValue { console.log('[FestipodData] leaveEvent (NG):', eventId, 'user:', uid); const ngPart = [...participationsShape.ngSet].find(p => p.event === eventId && p.user === uid); if (ngPart) { - console.log('[FestipodData] Deleting participation:', ngPart["@id"]); - participationsShape.ngSet.delete(ngPart); - const ngEvent = findNg(eventsShape.ngSet as any as Set, e => e["@id"] === eventId); - if (ngEvent) { - ngEvent.participantCount = Math.max(0, ngEvent.participantCount - 1); - } + const partId = ngPart["@id"]; + const partGraph = ngPart["@graph"]; + console.log('[FestipodData] Deleting participation:', partId, '@graph:', partGraph); + // Use ONLY sparql_update to delete — the broker will send back a GraphOrmUpdate + // that removes the item from the ORM set reactively. Avoid calling ngSet.delete() + // simultaneously as the two operations can conflict in the CRDT. + (async () => { + try { + const session = await sessionPromise; + await ng.sparql_update( + session.session_id, + `DELETE WHERE { GRAPH <${partGraph}> { <${partId}> ?p ?o } }`, + partGraph, + ); + console.log('[FestipodData] SPARQL DELETE succeeded for participation:', partId); + // Update participantCount after confirmed deletion + const ngEvent = findNg(eventsShape.ngSet as any as Set, e => e["@id"] === eventId); + if (ngEvent) { + ngEvent.participantCount = Math.max(0, ngEvent.participantCount - 1); + } + } catch (err) { + console.error('[FestipodData] SPARQL DELETE failed:', err); + } + })(); } }, [participationsShape.ngSet, eventsShape.ngSet, currentUserId]);