Skip to content

Commit

Permalink
Add Paginator2 for paginating through PagedData model, use for notifi…
Browse files Browse the repository at this point in the history
…cations

Add State.id/setId for debugging
- Allow custom equality checking for State and State.Generator
- Add State.bindManual
- Fix Endpoint data search param input object being incorrect
  • Loading branch information
ChiriVulpes committed Jan 22, 2025
1 parent c69d57a commit 6235ccb
Show file tree
Hide file tree
Showing 12 changed files with 614 additions and 78 deletions.
4 changes: 2 additions & 2 deletions src/endpoint/Endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type EndpointQuery<ROUTE extends keyof Paths> =
) extends infer DATA ?

(
& (RESPONSE extends PaginatedResponse<any> ? { query: { page?: number, pageSize?: number } } : { query?: never })
& (RESPONSE extends PaginatedResponse<any> ? { page?: number, page_size?: number } : { page?: never, page_size?: never })
) extends infer QUERY ?

[keyof DATA] extends [never]
Expand Down Expand Up @@ -75,7 +75,7 @@ interface PreparedEndpointQuery<ROUTE extends keyof Paths, QUERY extends Endpoin
params?: Partial<PARAMS>
} : never : never
),
query?: Parameters<QUERY>[1] extends infer P ? P extends { query?: infer Q } ? Partial<Q> : never : never,
query?: Parameters<QUERY>[1] extends infer Q ? Partial<Q> : never,
): ReturnType<QUERY>
}

Expand Down
115 changes: 87 additions & 28 deletions src/model/Notifications.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Notification } from 'api.fluff4.me'
import EndpointNotificationGetAll from 'endpoint/notification/EndpointNotificationGetAll'
import EndpointNotificationGetCount from 'endpoint/notification/EndpointNotificationGetCount'
import EndpointNotificationGetUnread from 'endpoint/notification/EndpointNotificationGetUnread'
import EndpointNotificationMarkRead from 'endpoint/notification/EndpointNotificationMarkRead'
import PagedListData from 'model/PagedListData'
import Session from 'model/Session'
import State from 'utility/State'
import Store from 'utility/Store'
Expand All @@ -9,7 +11,8 @@ import Time from 'utility/Time'
interface NotificationsCache {
lastCheck?: number
lastUpdate?: number
recentUnreads?: Notification[]
cache?: Notification[]
hasMore?: boolean
unreadCount?: number
}

Expand All @@ -21,7 +24,31 @@ declare module 'utility/Store' {

namespace Notifications {

export const recentUnreads = State<Notification[]>(Store.items.notifications?.recentUnreads ?? [])
let simpleCache = Store.items.notifications?.cache ?? []
const pageSize = 25
export const cache = PagedListData(pageSize, {
async get (page) {
const start = page * pageSize
const end = (page + 1) * pageSize

if (simpleCache.length < start) {
const response = await EndpointNotificationGetAll.query(undefined, { page, page_size: pageSize })
if (toast.handleError(response))
return false

const notifications = response.data
if (!notifications.length)
return null

simpleCache.push(...notifications)
simpleCache.sort(...sortNotifs)
Store.items.notifications = { ...Store.items.notifications, cache: simpleCache }
}

return simpleCache.slice(start, end)
},
})
export const hasMore = State(Store.items.notifications?.hasMore ?? false)
export const unreadCount = State(Store.items.notifications?.unreadCount ?? 0)
export const lastUpdate = State(Store.items.notifications?.lastUpdate ?? 0)

Expand All @@ -37,14 +64,40 @@ namespace Notifications {
return checkNotifications()
}

let activeCheck = false
export async function await () {
await checkNotifications()
}

export async function markRead (read: boolean, ...ids: string[]) {
const response = await EndpointNotificationMarkRead.query({ body: { notification_ids: ids } })
if (toast.handleError(response))
return false

for (const notification of simpleCache)
if (ids.includes(notification.id))
notification.read = read

Store.items.notifications = { ...Store.items.notifications, cache: simpleCache }
for (const page of cache.pages)
if (Array.isArray(page.value) && page.value.some(n => ids.includes(n.id)))
page.emit()

return true
}

const sortNotifs = [
(notif: Notification) => -+notif.read,
(notif: Notification) => -new Date(notif.created_time).getTime(),
]

let activeCheck: Promise<void> | undefined

// eslint-disable-next-line @typescript-eslint/no-misused-promises
setInterval(checkNotifications, Time.seconds(5))

async function checkNotifications () {
if (activeCheck)
return
return await activeCheck

if (!Session.Auth.author.value)
return
Expand All @@ -55,31 +108,37 @@ namespace Notifications {
if (now - (notifications?.lastCheck ?? 0) < Time.minutes(1))
return

activeCheck = true

const response = await EndpointNotificationGetCount.query()
notifications ??= {}
notifications.lastCheck = Date.now()
Store.items.notifications = notifications

activeCheck = false

if (toast.handleError(response))
return

const time = new Date(response.data.notification_time_last_modified).getTime()
if (time <= (notifications.lastUpdate ?? 0))
return

const firstPage = await EndpointNotificationGetUnread.query()
if (toast.handleError(firstPage))
return

const count = response.data.unread_notification_count
notifications.unreadCount = unreadCount.value = count
notifications.recentUnreads = recentUnreads.value = firstPage.data
notifications.lastUpdate = lastUpdate.value = time
Store.items.notifications = notifications
let resolve!: () => void
activeCheck = new Promise(r => resolve = r)
try {
const response = await EndpointNotificationGetCount.query()
if (toast.handleError(response))
return

const time = new Date(response.data.notification_time_last_modified).getTime()
if (time <= (notifications.lastUpdate ?? 0))
return

const firstPage = await EndpointNotificationGetAll.query()
if (toast.handleError(firstPage))
return

const count = response.data.unread_notification_count
notifications.unreadCount = unreadCount.value = count
notifications.cache = simpleCache = firstPage.data.sort(...sortNotifs)
notifications.hasMore = hasMore.value = firstPage.has_more
notifications.lastUpdate = lastUpdate.value = time
cache.clear()
}
finally {
notifications.lastCheck = Date.now()
Store.items.notifications = notifications

resolve()
activeCheck = undefined
}
}
}

Expand Down
100 changes: 100 additions & 0 deletions src/model/PagedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { PaginatedEndpoint, PreparedPaginatedQueryReturning, PreparedQueryOf } from 'endpoint/Endpoint'
import type { StateOr } from 'utility/State'
import State from 'utility/State'
import type { PromiseOr } from 'utility/Type'

interface PagedData<T> {
readonly pageCount: State<number | undefined>
get pages (): readonly State<T | false | null>[]
/** @deprecated */
get rawPages (): State.Mutable<PromiseOr<State.Mutable<T | false | null>>[]>
get (page: number): PromiseOr<State<T | false | null>>
set (page: number, data: T, isLastPage?: true): void
setPageCount (count: number): void
clear (): void
}

export interface PagedDataDefinition<T> {
get (page: number): PromiseOr<StateOr<T | false | null>>
}

const PagedData = Object.assign(
function <T> (definition: PagedDataDefinition<T>): PagedData<T> {
const pageCount = State<number | undefined>(undefined)
const pages: State.Mutable<PromiseOr<State.Mutable<T | false | null>>[]> = State([], false)// .setId('PagedData pages')
return {
pageCount,
get pages () {
return pages.value.filter((page): page is Exclude<typeof page, Promise<any>> => !(page instanceof Promise))
},
rawPages: pages,
get (page): PromiseOr<State<T | false | null>> {
if (pages.value[page] instanceof Promise)
return pages.value[page]

const existing = pages.value[page]?.value
if (existing === undefined || existing === false) {
pages.value[page] = Promise.resolve(definition.get(page))
.then(data => {
if (!State.is(pages.value[page])) { // if it's already a State, it's been updated before this, don't overwrite
let newState: State.Mutable<T | false | null>
if (State.is(data)) {
newState = State(null, false)// .setId(`PagedData page ${page} get 1`)
newState.bindManual(data)
}
else
newState = State(data, false)// .setId(`PagedData page ${page} get 2`)

pages.value[page] = newState
pages.emit()
}

return pages.value[page]
})
pages.emit()
}

return pages.value[page]
},
set (page, data, isLastPage) {
if (State.is(pages.value[page]))
pages.value[page].value = data
else
pages.value[page] = State(data, false)// .setId(`PagedData page ${page} set`)

if (isLastPage)
pages.value.length = pageCount.value = page + 1
else if (pageCount.value !== undefined && page >= pageCount.value)
pageCount.value = undefined

pages.emit()
},
setPageCount (count) {
pageCount.value = count
},
clear () {
pages.value = []
pageCount.value = undefined
},
}
},
{
fromEndpoint<T> (endpoint: PreparedPaginatedQueryReturning<T>): PagedData<T> {
const e = endpoint as PreparedQueryOf<PaginatedEndpoint>
return PagedData({
async get (page) {
const response = await e.query(undefined, { page })
if (toast.handleError(response))
return false

if (!Array.isArray(response.data) || response.data.length)
return response.data as T

return null
},
})
},
}
)

export default PagedData
116 changes: 116 additions & 0 deletions src/model/PagedListData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { PagedDataDefinition } from 'model/PagedData'
import PagedData from 'model/PagedData'
import State from 'utility/State'
import type { PromiseOr } from 'utility/Type'

interface PagedListData<T> extends PagedData<T[]> {
readonly pageSize: number
resized (pageSize: number): PagedListData<T>
}

function PagedListData<T> (pageSize: number, definition: PagedDataDefinition<T[]>): PagedListData<T> {
const list = PagedData<T[]>(definition)
return Object.assign(
list,
{
pageSize,
resized (resizePageSize: number) {
const newList = PagedListData(resizePageSize, {
get: getPage,
})

list.rawPages.subscribeManual(() => {
// go through current pages and update each from the source list
const pages = newList.rawPages.value
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
if (State.is(page)) {
const pageState = page
const value = getPage(i)
if (value instanceof Promise)
void value.then(setPageValue)
else
setPageValue(value)

function setPageValue (value: false | State<T[] | null>) {
if (State.is(value))
pageState.bindManual(value)
else
pageState.value = value
}

continue
}

const value = getPage(i)
if (value instanceof Promise)
pages[i] = value.then(setPage)
else
pages[i] = setPage(value)

function setPage (value: false | State<T[] | null>) {
const state = pages[i] = State<T[] | null | false>(null, false)// .setId('PagedListData subscribeManual setPage')
if (State.is(value))
state.bindManual(value)
else
state.value = value
return state
}
}
})

return newList

type SourcePages = State.Mutable<false | T[] | null>[]

function getPage (page: number): PromiseOr<State<T[] | null> | false> {
const start = page * resizePageSize
const end = (page + 1) * resizePageSize
const startPageInSource = Math.floor(start / pageSize)
const endPageInSource = Math.ceil(end / pageSize)
const startIndexInFirstSourcePage = start % pageSize
const endIndexInLastSourcePage = (end % pageSize) || pageSize

const rawPages = list.rawPages.value.slice()
for (let i = 0; i < rawPages.length; i++) {
const rawPage = rawPages[i]
if (i >= startPageInSource && i < endPageInSource && State.is(rawPage) && rawPage.value === false) {
rawPages[i] = list.get(i) as PromiseOr<State.Mutable<T[] | false | null>>
}
}

const sourcePages = rawPages.slice(startPageInSource, endPageInSource)
if (sourcePages.some(page => page instanceof Promise))
return Promise.all(sourcePages).then(sourcePages => resolveData(sourcePages, startIndexInFirstSourcePage, endIndexInLastSourcePage))

return resolveData(sourcePages as SourcePages, startIndexInFirstSourcePage, endIndexInLastSourcePage)
}

function resolveData (sourcePages: SourcePages, startIndex: number, endIndex: number): State<T[] | null> | false {
const data: T[] = []
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i]
if (sourcePage.value === false)
return false

if (sourcePage.value === null)
continue

if (i === 0 && i === sourcePages.length - 1)
data.push(...sourcePage.value.slice(startIndex, endIndex))
else if (i === 0)
data.push(...sourcePage.value.slice(startIndex))
else if (i === sourcePages.length - 1)
data.push(...sourcePage.value.slice(0, endIndex))
else
data.push(...sourcePage.value)
}

return State.Generator(() => data, false).observeManual(...sourcePages)// .setId('PagedListData resolveData')
}
},
}
)
}

export default PagedListData
Loading

0 comments on commit 6235ccb

Please sign in to comment.