feat(ST-804): solo-store realtime dispatch + pendingOps
Wire de SSE-events uit /api/realtime/solo door naar de Zustand-store
zodat het Solo Paneel zonder refresh meebeweegt met DB-mutaties uit
welke bron dan ook (web, REST, MCP).
Migratie 20260427000216_extend_realtime_payload: voegt new-state
velden aan de pg_notify-payload toe (task_status, task_sort_order,
task_title, story_status, story_sort_order, story_title, story_code)
zodat de client geen extra fetch nodig heeft per event.
Store-uitbreiding (stores/solo-store.ts):
- pendingOps: Set<task-id> die optimistic-writes markeert; realtime
echos voor die ids worden onderdrukt zodat eigen UI-mutaties niet
twee keer toegepast worden of door een latere echo overschreven
- handleRealtimeEvent: dispatch op entity + op
- task UPDATE/INSERT: bestaande tasks krijgen status/title/sort_order
bijgewerkt; onbekende tasks worden genegeerd (story-context
ontbreekt — gebruiker ziet ze pas na refresh)
- task DELETE: verwijdert uit store
- story UPDATE: werkt story_title/story_code bij op alle child-tasks
in de store
- story DELETE: verwijdert alle child-tasks (cascade reflectie)
Unit-test: 7 scenario's (status update, pendingOps echo-suppression,
DELETE, story-rename cascade, story-delete cascade, unknown task
no-op).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1e548da9bf
commit
562735b98b
3 changed files with 345 additions and 6 deletions
|
|
@ -0,0 +1,111 @@
|
|||
-- ST-804 prereq: extend the realtime trigger payload with the new-state
|
||||
-- fields the Solo Paneel needs for in-place rendering, so the client doesn't
|
||||
-- have to refetch on every update.
|
||||
--
|
||||
-- Added fields:
|
||||
-- task → task_status, task_sort_order, task_title
|
||||
-- story → story_status, story_sort_order, story_title, story_code
|
||||
--
|
||||
-- Description and implementation_plan stay out of the payload — they can
|
||||
-- be large and aren't needed for kanban-board rendering. UI fetches them
|
||||
-- on demand when the detail dialog opens.
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_task_change() RETURNS trigger AS $$
|
||||
DECLARE
|
||||
rec record;
|
||||
story_row record;
|
||||
payload jsonb;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
rec := OLD;
|
||||
ELSE
|
||||
rec := NEW;
|
||||
END IF;
|
||||
|
||||
SELECT product_id, sprint_id, assignee_id
|
||||
INTO story_row
|
||||
FROM stories
|
||||
WHERE id = rec.story_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN rec;
|
||||
END IF;
|
||||
|
||||
payload := jsonb_build_object(
|
||||
'op', CASE TG_OP
|
||||
WHEN 'INSERT' THEN 'I'
|
||||
WHEN 'UPDATE' THEN 'U'
|
||||
WHEN 'DELETE' THEN 'D'
|
||||
END,
|
||||
'entity', 'task',
|
||||
'id', rec.id,
|
||||
'story_id', rec.story_id,
|
||||
'product_id', story_row.product_id,
|
||||
'sprint_id', story_row.sprint_id,
|
||||
'assignee_id', story_row.assignee_id,
|
||||
'task_status', rec.status,
|
||||
'task_sort_order', rec.sort_order,
|
||||
'task_title', rec.title
|
||||
);
|
||||
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
payload := payload || jsonb_build_object(
|
||||
'changed_fields',
|
||||
COALESCE((
|
||||
SELECT jsonb_agg(n.key)
|
||||
FROM jsonb_each(to_jsonb(NEW)) n
|
||||
JOIN jsonb_each(to_jsonb(OLD)) o USING (key)
|
||||
WHERE n.value IS DISTINCT FROM o.value
|
||||
), '[]'::jsonb)
|
||||
);
|
||||
END IF;
|
||||
|
||||
PERFORM pg_notify('scrum4me_changes', payload::text);
|
||||
RETURN rec;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_story_change() RETURNS trigger AS $$
|
||||
DECLARE
|
||||
rec record;
|
||||
payload jsonb;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
rec := OLD;
|
||||
ELSE
|
||||
rec := NEW;
|
||||
END IF;
|
||||
|
||||
payload := jsonb_build_object(
|
||||
'op', CASE TG_OP
|
||||
WHEN 'INSERT' THEN 'I'
|
||||
WHEN 'UPDATE' THEN 'U'
|
||||
WHEN 'DELETE' THEN 'D'
|
||||
END,
|
||||
'entity', 'story',
|
||||
'id', rec.id,
|
||||
'product_id', rec.product_id,
|
||||
'sprint_id', rec.sprint_id,
|
||||
'assignee_id', rec.assignee_id,
|
||||
'story_status', rec.status,
|
||||
'story_sort_order', rec.sort_order,
|
||||
'story_title', rec.title,
|
||||
'story_code', rec.code
|
||||
);
|
||||
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
payload := payload || jsonb_build_object(
|
||||
'changed_fields',
|
||||
COALESCE((
|
||||
SELECT jsonb_agg(n.key)
|
||||
FROM jsonb_each(to_jsonb(NEW)) n
|
||||
JOIN jsonb_each(to_jsonb(OLD)) o USING (key)
|
||||
WHERE n.value IS DISTINCT FROM o.value
|
||||
), '[]'::jsonb)
|
||||
);
|
||||
END IF;
|
||||
|
||||
PERFORM pg_notify('scrum4me_changes', payload::text);
|
||||
RETURN rec;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
Loading…
Add table
Add a link
Reference in a new issue