Skip to content

Events and sync ​

Types: ContentProgressState, DeepLinkContext

These methods let a source mirror what the user does in Mana back to the site, and pull the site's state into Mana.

Events Mana sends
Reader, library and link actions that reach your source
MethodMana today
onChapterRead, onPageRead✓ (needs onChaptersMarked too, see below)
onChaptersMarkedRequired for chapter events, not called yet
onContentsAddedToLibrary, onContentsRemovedFromLibraryNot called yet
getProgressStateNot called yet
handleURL✓
syncUserLibrary, getGroupedUpdatesNot called yet

Reading events ​

Chapter events need both onChaptersMarked and onChapterRead; onPageRead is optional on top.

ts
private lastPageSent = 0;

async onChapterRead(contentId: string, chapterId: string): Promise<void> {
  await this.http.post(`/api/history`, Body.json({ series: contentId, chapter: chapterId }));
}

async onPageRead(contentId: string, chapterId: string, page: number): Promise<void> {
  // Called on every page turn: send at most every 10 seconds.
  if (Date.now() - this.lastPageSent < 10_000) return;
  this.lastPageSent = Date.now();
  await this.http.post(`/api/progress`, Body.json({ series: contentId, chapter: chapterId, page }));
}

async onChaptersMarked(contentId: string, chapterIds: string[], completed: boolean): Promise<void> {
  await this.http.post(`/api/series/${contentId}/read`, Body.json({ chapters: chapterIds, read: completed }));
}

Library events ​

Not called by Mana yet. Both methods are needed.

ts
async onContentsAddedToLibrary(ids: string[]): Promise<void> {
  await Promise.all(ids.map((id) => this.http.put(`/api/bookmarks/${id}`)));
}

async onContentsRemovedFromLibrary(ids: string[]): Promise<void> {
  await Promise.all(ids.map((id) => this.http.delete(`/api/bookmarks/${id}`)));
}

Progress sync ​

Not called by Mana yet. The method lets a source report which chapters the user already read on the site, so library updates could skip them.

ts
import type { ContentProgressState } from "@mana-app/types";

async getProgressState(contentId: string): Promise<ContentProgressState> {
  const history = await (await this.http.get(`/api/series/${contentId}/history`)).requireOk().json<any>();

  return {
    readChapterIds: history.read.map(String),
    currentReadingState: history.current && {
      chapterId: String(history.current.chapter),
      page: history.current.page,
      progress: history.current.page / history.current.total,   // 0…1
      readDate: new Date(history.current.at),
    },
    markAllBelowAsRead: history.last && { chapterNumber: history.last.number, chapterVolume: history.last.volume },
  };
}

When the user opens a link on one of your config.owningLinks domains, Mana asks your source what it points to. Return null for links you don't handle.

Deep links
Links on your config.owningLinks domains open in your source
ts
import type { DeepLinkContext } from "@mana-app/types";

config = { owningLinks: ["example.org", "www.example.org"] };

async handleURL(url: string): Promise<DeepLinkContext | null> {
  const series = /\/series\/(\d+)/.exec(url);
  if (series) {
    const content = await this.getContent(series[1]);
    return { content: { id: series[1], title: content.title, cover: content.cover } };
  }

  const genre = /\/genre\/([\w-]+)/.exec(url);
  if (genre) {
    return { link: { title: genre[1], link: { request: { page: 1, filters: { genres: [genre[1]] } } } } };
  }

  return null;
}

If several sources claim a link, the user picks one.

Library sync and grouped updates ​

Not called by Mana yet.

ts
// Merge the Mana library with the site's bookmarks; return what Mana should add.
async syncUserLibrary(library: UpSyncedContent[]): Promise<DownSyncedContent[]> {
  const known = new Set(library.map((c) => c.id));
  const bookmarks = await (await this.http.get("/api/bookmarks")).requireOk().json<any[]>();
  return bookmarks
    .filter((b) => !known.has(String(b.id)))
    .map((b) => ({ id: String(b.id), title: b.title, cover: b.cover }));
}

// One request for many titles' update counts since `date`.
async getGroupedUpdates(ids: string[], date: Date): Promise<GroupedUpdateResponse> {
  const json = await (await this.http.post("/api/updates", Body.json({ ids, since: date.toISOString() }))).requireOk().json<any>();
  return { updates: json.map((u: any) => ({ contentId: String(u.id), updateCount: u.new_chapters })) };
}