feat: basic entry data sync (#2597)

This commit is contained in:
Stephen Zhou 2025-01-17 21:12:38 +08:00 committed by GitHub
parent 0ea516c9f9
commit 3479a89b1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 978 additions and 3 deletions

View File

@ -0,0 +1 @@
ALTER TABLE `lists` ADD `entry_ids` text;

View File

@ -0,0 +1,501 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0d5c6f89-f31c-405c-8c2d-7b59ab03a275",
"prevId": "9f943ca5-ddaf-4916-98cd-d3e3f7ead201",
"tables": {
"entries": {
"name": "entries",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"guid": {
"name": "guid",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"author_url": {
"name": "author_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"author_avatar": {
"name": "author_avatar",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inserted_at": {
"name": "inserted_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"published_at": {
"name": "published_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"media": {
"name": "media",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"categories": {
"name": "categories",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"attachments": {
"name": "attachments",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"extra": {
"name": "extra",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_handle": {
"name": "inbox_handle",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"read": {
"name": "read",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"entry_ids": {
"name": "entry_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"unread": {
"name": "unread",
"columns": {
"subscription_id": {
"name": "subscription_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"count": {
"name": "count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"handle": {
"name": "handle",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_me": {
"name": "is_me",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -50,6 +50,13 @@
"when": 1737080887634,
"tag": "0006_exotic_kid_colt",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1737108493439,
"tag": "0007_curvy_tarantula",
"breakpoints": true
}
]
}

View File

@ -7,6 +7,7 @@ import m0003 from "./0003_known_roland_deschain.sql"
import m0004 from "./0004_majestic_thunderbolt_ross.sql"
import m0005 from "./0005_tense_sleepwalker.sql"
import m0006 from "./0006_exotic_kid_colt.sql"
import m0007 from "./0007_curvy_tarantula.sql"
import journal from "./meta/_journal.json"
export default {
@ -19,5 +20,6 @@ export default {
m0004,
m0005,
m0006,
m0007,
},
}

View File

@ -44,6 +44,7 @@ export const listsTable = sqliteTable("lists", {
image: text("image"),
fee: integer("fee"),
ownerUserId: text("owner_user_id"),
entryIds: text("entry_ids", { mode: "json" }).$type<string[]>(),
})
export const unreadTable = sqliteTable("unread", {

View File

@ -1,4 +1,5 @@
import type { SubscriptionSchema } from "../database/schemas/types"
import type { EntrySchema, SubscriptionSchema } from "../database/schemas/types"
import type { EntryModel } from "../store/entry/types"
import type { SubscriptionModel } from "../store/subscription/store"
class DbStoreMorph {
@ -8,6 +9,10 @@ class DbStoreMorph {
isPrivate: subscription.isPrivate ? true : false,
}
}
toEntryModel(entry: EntrySchema): EntryModel {
return entry
}
}
export const dbStoreMorph = new DbStoreMorph()

View File

@ -1,4 +1,5 @@
import type { FeedSchema, InboxSchema } from "../database/schemas/types"
import type { EntryModel } from "../store/entry/types"
import type { ListModel } from "../store/list/store"
import type { SubscriptionModel } from "../store/subscription/store"
import type { HonoApiClient } from "./types"
@ -72,6 +73,7 @@ class Morph {
ownerUserId: list.owner.id,
feedIds: list.feedIds!,
fee: list.fee!,
entryIds: [],
})
}
@ -91,8 +93,42 @@ class Morph {
ownerUserId: data.ownerUserId!,
feedIds: data.feedIds!,
fee: data.fee!,
entryIds: [],
}
}
toEntry(data?: HonoApiClient.Entry_Get): EntryModel[] {
const entries: EntryModel[] = []
for (const item of data ?? []) {
entries.push({
id: item.entries.id,
title: item.entries.title,
url: item.entries.url,
content: "",
description: item.entries.description,
guid: item.entries.guid,
author: item.entries.author,
authorUrl: item.entries.authorUrl,
authorAvatar: item.entries.authorAvatar,
insertedAt: new Date(item.entries.insertedAt),
publishedAt: new Date(item.entries.publishedAt),
media: item.entries.media ?? null,
categories: item.entries.categories ?? null,
attachments: item.entries.attachments ?? null,
extra: item.entries.extra
? {
links: item.entries.extra.links ?? undefined,
}
: null,
language: item.entries.language,
feedId: item.feeds.id,
// TODO: handle inboxHandle
inboxHandle: "",
read: false,
})
}
return entries
}
}
export const honoMorph = new Morph()

View File

@ -1,4 +1,5 @@
import type { ListSchema, SubscriptionSchema } from "../database/schemas/types"
import type { EntrySchema, ListSchema, SubscriptionSchema } from "../database/schemas/types"
import type { EntryModel } from "../store/entry/types"
import type { ListModel } from "../store/list/store"
import type { SubscriptionModel } from "../store/subscription/store"
@ -16,6 +17,9 @@ class StoreDbMorph {
isPrivate: subscription.isPrivate ? 1 : 0,
}
}
toEntrySchema(entry: EntryModel): EntrySchema {
return entry
}
}
export const storeDbMorph = new StoreDbMorph()

View File

@ -3,9 +3,10 @@ import type { apiClient } from "../lib/api-fetch"
// Add ExtractData type utility
type ExtractData<T extends (...args: any) => any> =
Awaited<ReturnType<T>> extends { data: infer D } ? D : never
Awaited<ReturnType<T>> extends { data?: infer D } ? D : never
export namespace HonoApiClient {
export type Subscription_Get = ExtractData<typeof apiClient.subscriptions.$get>
export type List_Get = ExtractData<typeof apiClient.lists.$get>
export type Entry_Get = ExtractData<typeof apiClient.entries.$post>
}

View File

@ -0,0 +1,37 @@
import { eq } from "drizzle-orm"
import { db } from "../database"
import { entriesTable } from "../database/schemas"
import type { EntrySchema } from "../database/schemas/types"
import { dbStoreMorph } from "../morph/db-store"
import { entryActions } from "../store/entry/store"
import type { Hydratable, Resetable } from "./internal/base"
import { conflictUpdateAllExcept } from "./internal/utils"
class EntryServiceStatic implements Hydratable, Resetable {
async reset() {
await db.delete(entriesTable).execute()
}
async upsertMany(entries: EntrySchema[]) {
if (entries.length === 0) return
await db
.insert(entriesTable)
.values(entries)
.onConflictDoUpdate({
target: [entriesTable.id],
set: conflictUpdateAllExcept(entriesTable, ["id"]),
})
}
async patch(entry: Partial<EntrySchema> & { id: string }) {
await db.update(entriesTable).set(entry).where(eq(entriesTable.id, entry.id))
}
async hydrate() {
const entries = await db.query.entriesTable.findMany()
entryActions.upsertManyInSession(entries.map((e) => dbStoreMorph.toEntryModel(e)))
}
}
export const EntryService = new EntryServiceStatic()

View File

@ -1,3 +1,5 @@
import { eq } from "drizzle-orm"
import { db } from "../database"
import { listsTable } from "../database/schemas"
import type { ListSchema } from "../database/schemas/types"
@ -28,6 +30,20 @@ class ListServiceStatic implements Hydratable, Resetable {
set: conflictUpdateAllExcept(listsTable, ["id"]),
})
}
async addEntryIds(params: { listId: string; entryIds: string[] }) {
const list = await db.query.listsTable.findFirst({
where: eq(listsTable.id, params.listId),
})
if (!list) return
await db
.update(listsTable)
.set({
entryIds: [...new Set([...(list.entryIds ?? []), ...params.entryIds])],
})
.where(eq(listsTable.id, params.listId))
}
}
export const ListService = new ListServiceStatic()

View File

@ -0,0 +1,8 @@
import { useEntryStore } from "./store"
import type { EntryModel } from "./types"
const get = useEntryStore.getState
export const getEntry = (id: string): EntryModel | undefined => {
return get().data[id]
}

View File

@ -0,0 +1,78 @@
import type { FeedViewType } from "@follow/constants"
import { useQuery } from "@tanstack/react-query"
import { useCallback } from "react"
import { getEntry } from "./getter"
import { entrySyncServices, useEntryStore } from "./store"
import type { EntryModel, FetchEntriesProps } from "./types"
export const usePrefetchEntries = (props: FetchEntriesProps) => {
const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props
return useQuery({
queryKey: ["entries", feedId, inboxId, listId, view, read, limit, pageParam, isArchived],
queryFn: () => entrySyncServices.fetchEntries(props),
})
}
export const useEntry = (id: string): EntryModel | undefined => {
return useEntryStore((state) => state.data[id])
}
function sortEntryIdsByPublishDate(a: string, b: string) {
const entryA = getEntry(a)
const entryB = getEntry(b)
if (!entryA || !entryB) return 0
return entryB.publishedAt.getTime() - entryA.publishedAt.getTime()
}
export const useEntryIdsByView = (view: FeedViewType) => {
return useEntryStore(
useCallback(
(state) => {
const ids = state.entryIdByView[view]
if (!ids) return []
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
},
[view],
),
)
}
export const useEntryIdsByFeedId = (feedId: string) => {
return useEntryStore(
useCallback(
(state) => {
const ids = state.entryIdByFeed[feedId]
if (!ids) return []
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
},
[feedId],
),
)
}
export const useEntryIdsByInboxId = (inboxId: string) => {
return useEntryStore(
useCallback(
(state) => {
const ids = state.entryIdByInbox[inboxId]
if (!ids) return []
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
},
[inboxId],
),
)
}
export const useEntryIdsByCategory = (category: string) => {
return useEntryStore(
useCallback(
(state) => {
const ids = state.entryIdByCategory[category]
if (!ids) return []
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
},
[category],
),
)
}

View File

@ -0,0 +1,174 @@
import { FeedViewType } from "@follow/constants"
import { apiClient } from "@/src/lib/api-fetch"
import { honoMorph } from "@/src/morph/hono"
import { storeDbMorph } from "@/src/morph/store-db"
import { EntryService } from "@/src/services/entry"
import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper"
import { listActions } from "../list/store"
import { getSubscription } from "../subscription/getter"
import type { EntryModel, FetchEntriesProps } from "./types"
import { getEntriesParams } from "./utils"
type EntryId = string
type FeedId = string
type InboxId = string
type Category = string
interface EntryState {
data: Record<EntryId, EntryModel>
entryIdByView: Record<FeedViewType, Set<EntryId>>
entryIdByCategory: Record<Category, Set<EntryId>>
entryIdByFeed: Record<FeedId, Set<EntryId>>
entryIdByInbox: Record<InboxId, Set<EntryId>>
}
const defaultState: EntryState = {
data: {},
entryIdByView: {
[FeedViewType.Articles]: new Set(),
[FeedViewType.Audios]: new Set(),
[FeedViewType.Notifications]: new Set(),
[FeedViewType.Pictures]: new Set(),
[FeedViewType.SocialMedia]: new Set(),
[FeedViewType.Videos]: new Set(),
},
entryIdByCategory: {},
entryIdByFeed: {},
entryIdByInbox: {},
}
export const useEntryStore = createZustandStore<EntryState>("entry")(() => defaultState)
const set = useEntryStore.setState
const immerSet = createImmerSetter(useEntryStore)
class EntryActions {
private addEntryIdToFeed({
draft,
feedId,
entryId,
}: {
draft: EntryState
feedId?: FeedId | null
entryId: EntryId
}) {
if (!feedId) return
const entryIdSetByFeed = draft.entryIdByFeed[feedId]
if (!entryIdSetByFeed) {
draft.entryIdByFeed[feedId] = new Set([entryId])
} else {
entryIdSetByFeed.add(entryId)
}
const subscription = getSubscription(feedId)
if (subscription?.view) {
draft.entryIdByView[subscription.view].add(entryId)
}
if (subscription?.category) {
const entryIdSetByCategory = draft.entryIdByCategory[subscription.category]
if (!entryIdSetByCategory) {
draft.entryIdByCategory[subscription.category] = new Set([entryId])
} else {
entryIdSetByCategory.add(entryId)
}
}
}
private addEntryIdToInbox({
draft,
inboxHandle,
entryId,
}: {
draft: EntryState
inboxHandle?: InboxId | null
entryId: EntryId
}) {
if (!inboxHandle) return
const entryIdSetByInbox = draft.entryIdByInbox[inboxHandle]
if (!entryIdSetByInbox) {
draft.entryIdByInbox[inboxHandle] = new Set([entryId])
} else {
entryIdSetByInbox.add(entryId)
}
}
upsertManyInSession(entries: EntryModel[]) {
if (entries.length === 0) return
immerSet((draft) => {
for (const entry of entries) {
draft.data[entry.id] = entry
const { feedId, inboxHandle } = entry
this.addEntryIdToFeed({
draft,
feedId,
entryId: entry.id,
})
this.addEntryIdToInbox({
draft,
inboxHandle,
entryId: entry.id,
})
}
})
}
async upsertMany(entries: EntryModel[]) {
const tx = createTransaction()
tx.store(() => {
this.upsertManyInSession(entries)
})
tx.persist(() => {
return EntryService.upsertMany(entries.map((e) => storeDbMorph.toEntrySchema(e)))
})
await tx.run()
}
reset() {
set(defaultState)
}
}
class EntrySyncServices {
async fetchEntries(props: FetchEntriesProps) {
const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props
const params = getEntriesParams({
feedId,
inboxId,
listId,
view,
})
const res = await apiClient.entries.$post({
json: {
publishedAfter: pageParam,
read,
limit,
isArchived,
...params,
},
})
if (!pageParam) {
entryActions.reset()
}
const entries = honoMorph.toEntry(res.data)
await entryActions.upsertMany(entries)
if (params.listId) {
await listActions.addEntryIds({
listId: params.listId,
entryIds: entries.map((e) => e.id),
})
}
return entries
}
}
export const entrySyncServices = new EntrySyncServices()
export const entryActions = new EntryActions()

View File

@ -0,0 +1,13 @@
import type { EntrySchema } from "@/src/database/schemas/types"
export type EntryModel = EntrySchema
export type FetchEntriesProps = {
feedId?: number | string
inboxId?: number | string
listId?: number | string
view?: number
read?: boolean
limit?: number
pageParam?: string
isArchived?: boolean
}

View File

@ -0,0 +1,56 @@
import { FeedViewType } from "@follow/constants"
/// Feed
export const FEED_COLLECTION_LIST = "collections"
/// Route Keys
export const ROUTE_FEED_PENDING = "all"
export const ROUTE_ENTRY_PENDING = "pending"
export const ROUTE_FEED_IN_FOLDER = "folder-"
export const ROUTE_FEED_IN_LIST = "list-"
export const ROUTE_FEED_IN_INBOX = "inbox-"
export const INBOX_PREFIX_ID = "inbox-"
export function getEntriesParams({
feedId,
inboxId,
listId,
view,
}: {
feedId?: number | string
inboxId?: number | string
listId?: number | string
view?: number
}) {
const params: {
feedId?: string
feedIdList?: string[]
isCollection?: boolean
withContent?: boolean
inboxId?: string
listId?: string
} = {}
if (inboxId) {
params.inboxId = `${inboxId}`
} else if (listId) {
params.listId = `${listId}`
} else if (feedId) {
if (feedId === FEED_COLLECTION_LIST) {
params.isCollection = true
} else if (feedId !== ROUTE_FEED_PENDING) {
if (feedId.toString().includes(",")) {
params.feedIdList = `${feedId}`.split(",")
} else {
params.feedId = `${feedId}`
}
}
}
if (view === FeedViewType.SocialMedia) {
params.withContent = true
}
return {
view,
...params,
}
}

View File

@ -12,3 +12,9 @@ export const useIsOwnList = (id: string) => {
return state.lists[id]?.userId === whoami()?.id
})
}
export const useListEntryIds = (id: string) => {
return useListStore((state) => {
return state.lists[id]?.entryIds || []
})
}

View File

@ -45,6 +45,34 @@ class ListActions {
})
tx.run()
}
addEntryIdsInSession(params: { listId: string; entryIds: string[] }) {
const state = get()
const list = state.lists[params.listId]
if (!list) return
set({
...state,
lists: {
...state.lists,
[params.listId]: { ...list, feedIds: [...list.feedIds, ...params.entryIds] },
},
})
}
async addEntryIds(params: { listId: string; entryIds: string[] }) {
const tx = createTransaction()
tx.store(() => {
this.addEntryIdsInSession(params)
})
tx.persist(() => {
return ListService.addEntryIds(params)
})
await tx.run()
}
reset() {
set(defaultState)
}

View File

@ -211,6 +211,7 @@ class SubscriptionSyncService {
{
...data.list,
userId: data.list.ownerUserId,
entryIds: [],
},
])
}