BEGIN; CREATE TABLE web_event ( id bigserial PRIMARY KEY, tenant_id uuid NOT NULL REFERENCES tenant(id), household_id uuid NOT NULL, subject_id uuid, event_type varchar(80) NOT NULL CHECK (event_type IN ( 'device.presence.changed', 'conversation.started','conversation.completed','conversation.failed', 'story.created','story.revised','media.upload.completed','export.ready', 'privacy_request.updated','device.command.updated')), resource_type varchar(40) NOT NULL, resource_public_id uuid NOT NULL, status varchar(40) NOT NULL, occurred_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL DEFAULT (now() + interval '15 minutes'), CONSTRAINT web_event_household_tenant_fk FOREIGN KEY (household_id,tenant_id) REFERENCES household(id,tenant_id), CONSTRAINT web_event_subject_tenant_fk FOREIGN KEY (subject_id,tenant_id) REFERENCES subject_profile(id,tenant_id) ON DELETE CASCADE ); CREATE INDEX web_event_household_cursor_idx ON web_event (tenant_id,household_id,id); CREATE INDEX web_event_expiry_idx ON web_event (expires_at); CREATE FUNCTION append_web_event( p_tenant uuid,p_household uuid,p_subject uuid,p_event_type varchar, p_resource_type varchar,p_resource_public_id uuid,p_status varchar ) RETURNS void LANGUAGE plpgsql AS $$ DECLARE new_id bigint; BEGIN INSERT INTO web_event(tenant_id,household_id,subject_id,event_type,resource_type,resource_public_id,status) VALUES(p_tenant,p_household,p_subject,p_event_type,p_resource_type,p_resource_public_id,p_status) RETURNING id INTO new_id; PERFORM pg_notify('web_event',new_id::text); END; $$; CREATE TABLE device_presence_state ( tenant_id uuid NOT NULL REFERENCES tenant(id), device_id uuid NOT NULL, household_id uuid NOT NULL, subject_id uuid NOT NULL, online boolean NOT NULL, last_seen_at timestamptz NOT NULL, changed_at timestamptz NOT NULL, PRIMARY KEY (tenant_id,device_id), CONSTRAINT device_presence_device_tenant_fk FOREIGN KEY (device_id,tenant_id) REFERENCES device(id,tenant_id), CONSTRAINT device_presence_household_tenant_fk FOREIGN KEY (household_id,tenant_id) REFERENCES household(id,tenant_id), CONSTRAINT device_presence_subject_tenant_fk FOREIGN KEY (subject_id,tenant_id) REFERENCES subject_profile(id,tenant_id) ON DELETE CASCADE ); CREATE INDEX device_presence_online_seen_idx ON device_presence_state (online,last_seen_at) WHERE online; CREATE FUNCTION record_device_presence( p_tenant uuid,p_device uuid,p_observed_at timestamptz ) RETURNS void LANGUAGE plpgsql AS $$ DECLARE binding device_binding%ROWTYPE; device_public uuid; was_online boolean; had_state boolean; BEGIN SELECT * INTO binding FROM device_binding WHERE tenant_id=p_tenant AND device_id=p_device AND unbound_at IS NULL; IF NOT FOUND THEN RETURN; END IF; SELECT public_id INTO device_public FROM device WHERE id=p_device AND tenant_id=p_tenant; SELECT online INTO was_online FROM device_presence_state WHERE tenant_id=p_tenant AND device_id=p_device FOR UPDATE; had_state := FOUND; INSERT INTO device_presence_state(tenant_id,device_id,household_id,subject_id,online,last_seen_at,changed_at) VALUES(p_tenant,p_device,binding.household_id,binding.subject_id,true,p_observed_at,p_observed_at) ON CONFLICT (tenant_id,device_id) DO UPDATE SET household_id=EXCLUDED.household_id,subject_id=EXCLUDED.subject_id,online=true, last_seen_at=GREATEST(device_presence_state.last_seen_at,EXCLUDED.last_seen_at), changed_at=CASE WHEN device_presence_state.online THEN device_presence_state.changed_at ELSE EXCLUDED.changed_at END; IF NOT had_state OR NOT was_online THEN PERFORM append_web_event(p_tenant,binding.household_id,binding.subject_id, 'device.presence.changed','device',device_public,'online'); END IF; END; $$; CREATE FUNCTION expire_device_presence( p_stale_before timestamptz,p_limit integer ) RETURNS integer LANGUAGE plpgsql AS $$ DECLARE item record; expired_count integer := 0; BEGIN FOR item IN SELECT state.tenant_id,state.device_id,state.household_id,state.subject_id,device.public_id FROM device_presence_state state JOIN device ON device.id=state.device_id AND device.tenant_id=state.tenant_id WHERE state.online AND state.last_seen_at