Fix leaveEvent persistence: use SPARQL DELETE instead of ORM ngSet.delete()
ngSet.delete() updates the local reactive set but does not persist to the broker. Use ng.sparql_update() with SPARQL DELETE WHERE to remove RDF triples directly — the broker sends back a GraphOrmUpdate that reactively removes the item from the ORM set. The two methods must not be combined as they conflict in the CRDT. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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: "/<syntheticId>" }` 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 <graph> { <subject> ?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`
|
||||
@@ -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`
|
||||
|
||||
@@ -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<FpEvent>, 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<FpEvent>, 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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user