v4: every live update reaches every page that shows what it changed — the client kept each kind of event in one "latest" slot, so two arriving before a render lost the first (two notifications of one dump going public showed one); they now go through a subscription, useWSEvent, and each reaches every subscriber. A playlist made private sent its new title and description to everyone, and kept them on a visitor's page; playlists now follow the dumps' rule, broadcastChange: others get playlist_deleted, and playlist_created when it goes public again. A dump deleted or turned private sends its playlists' dumps again, so their counts and pages follow. Comments on a private dump reach its poster's other tabs. Reading the notifications clears the bell in every tab (notifications_read), and a category change makes every client fetch the categories again (categories_changed). The dump page gives way to a notice when its dump is deleted or made private. A profile gains the user's new dumps, the followed feed those of the users followed and the playlists followed, the search results follow edits and deletions, the index's category feed drops a dump edited out of it, and a renamed user's name follows on cards and comments. After a reconnection, every live list fetches the pages it holds again (usePagedList's resync), and the playlist, the profile, the dump, the categories and the follows are fetched again. The position memory of usePositionAwareSync is now inside the list sync hooks, so a returning item is put back without depending on the order the hooks are called.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,10 +25,8 @@ import {
|
||||
import { isPlaylistRow, playlistRowToApi } from "../model/playlist.ts";
|
||||
import { type Page, pageOf } from "../lib/pagination.ts";
|
||||
import {
|
||||
broadcastPlaylistCreated,
|
||||
broadcastPlaylistDeleted,
|
||||
broadcastPlaylistChange,
|
||||
broadcastPlaylistDumpsUpdated,
|
||||
broadcastPlaylistUpdated,
|
||||
} from "./ws-service.ts";
|
||||
import {
|
||||
notifyMentions,
|
||||
@@ -138,7 +136,7 @@ export function createPlaylist(
|
||||
}
|
||||
linkAttachments(req.description, "playlist", id);
|
||||
}
|
||||
broadcastPlaylistCreated(playlist);
|
||||
broadcastPlaylistChange(null, playlist);
|
||||
return playlist;
|
||||
}
|
||||
|
||||
@@ -276,7 +274,7 @@ export function listPlaylistsByUser(
|
||||
* playlist is public, it notifies, in the owner's name, the users the
|
||||
* description mentions. When it goes public, its followers are notified of
|
||||
* its public dumps they weren't yet told of. It links the description's
|
||||
* attachments and broadcasts `playlist_updated`.
|
||||
* attachments and tells clients of the change (`broadcastPlaylistChange`).
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such playlist, 403 when the user
|
||||
* is neither its owner nor a moderator, 400 when the slug isn't valid or a
|
||||
@@ -349,7 +347,7 @@ export function updatePlaylist(
|
||||
}
|
||||
linkAttachments(newDescription, "playlist", playlist.id);
|
||||
}
|
||||
broadcastPlaylistUpdated(updated, playlist.isPublic);
|
||||
broadcastPlaylistChange(playlist, updated);
|
||||
// Its followers weren't told of the dumps added while it was private.
|
||||
if (!playlist.isPublic && newIsPublic) {
|
||||
notifyPlaylistFollowersNowPublic({ playlistId: playlist.id });
|
||||
@@ -376,7 +374,7 @@ export function deletePlaylist(
|
||||
}
|
||||
|
||||
db.prepare(`DELETE FROM playlists WHERE id = ?;`).run(playlist.id);
|
||||
broadcastPlaylistDeleted(playlist.id, playlist.userId, playlist.isPublic);
|
||||
broadcastPlaylistChange(playlist, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,7 +422,7 @@ export function setPlaylistImage(
|
||||
`UPDATE playlists SET image_mime = ?, updated_at = ? WHERE id = ?;`,
|
||||
).run(imageMime, new Date().toISOString(), playlist.id);
|
||||
const updated = getPlaylistById(playlist.id);
|
||||
broadcastPlaylistUpdated(updated, playlist.isPublic);
|
||||
broadcastPlaylistChange(playlist, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -676,3 +674,37 @@ function getCurrentMembers(playlistId: string): PlaylistMember[] {
|
||||
userId: r.user_id,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A playlist holding a dump, as {@link playlistsHolding} returns it: what
|
||||
* `broadcastPlaylistDumpsUpdated` needs to choose its audience.
|
||||
*/
|
||||
export type PlaylistHolder = Pick<Playlist, "id" | "isPublic" | "userId">;
|
||||
|
||||
/**
|
||||
* Returns the playlists holding dump `dumpId`. The dump service reads them
|
||||
* before it deletes the dump or changes its visibility, and passes them to
|
||||
* {@link broadcastHoldersDumps} after.
|
||||
*/
|
||||
export function playlistsHolding(dumpId: string): PlaylistHolder[] {
|
||||
const rows = db.prepare(
|
||||
`SELECT p.id, p.is_public, p.user_id
|
||||
FROM playlist_dumps pd INNER JOIN playlists p ON p.id = pd.playlist_id
|
||||
WHERE pd.dump_id = ?;`,
|
||||
).all(dumpId) as Array<{ id: string; is_public: number; user_id: string }>;
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
isPublic: Boolean(r.is_public),
|
||||
userId: r.user_id,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends each of `holders` its dumps again, after one of them was deleted or
|
||||
* changed visibility: the count moved, or what each viewer may see of them.
|
||||
*/
|
||||
export function broadcastHoldersDumps(holders: PlaylistHolder[]): void {
|
||||
for (const holder of holders) {
|
||||
broadcastPlaylistDumpsUpdated(holder, getCurrentMembers(holder.id));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user