mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(reader): support dynamic token-fetcher for foliate PWA caching
Implement a custom callable fetcher function in the Foliate integration that stringifies to the current auth token. This ensures backward compatibility for older service-worker cached versions of the Foliate library expecting a string token, while allowing newer/uncached versions to perform authenticated requests correctly. Additionally, exclude Foliate assets from PWA precaching to prevent stale cache bugs on Firefox. Closes #575
This commit is contained in:
@@ -11,7 +11,16 @@
|
||||
* @returns {Object} Loader interface compatible with Foliate's EPUB class
|
||||
*/
|
||||
export const makeStreamingLoader = (bookId, baseUrl, bookInfo, fetchFile = fetch, bookType = null, fileId = null) => {
|
||||
const requestFile = typeof fetchFile === 'function' ? fetchFile : fetch
|
||||
const requestFile =
|
||||
typeof fetchFile === 'function'
|
||||
? fetchFile
|
||||
: typeof fetchFile === 'string' && fetchFile
|
||||
? (input, init) => {
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set('Authorization', `Bearer ${fetchFile}`)
|
||||
return fetch(input, { ...init, headers })
|
||||
}
|
||||
: fetch
|
||||
const OPTIONAL_TEXT_FILES = new Set([
|
||||
'META-INF/encryption.xml',
|
||||
'META-INF/com.apple.ibooks.display-options.xml',
|
||||
|
||||
@@ -53,4 +53,21 @@ describe('makeStreamingLoader auth integration', () => {
|
||||
{ url: chapterUrl, auth: 'Bearer fresh-token' },
|
||||
])
|
||||
})
|
||||
|
||||
it('supports the legacy static token argument for already-cached app bundles', async () => {
|
||||
const calls: { url: string; auth: string | null }[] = []
|
||||
const chapterUrl = '/api/v1/epub/42/file/text/chapter%201.xhtml?fileId=9'
|
||||
const fetchMock = vi.fn<typeof fetch>((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
||||
const headers = new Headers(init?.headers)
|
||||
calls.push({ url, auth: headers.get('Authorization') })
|
||||
return Promise.resolve(new Response('<html>chapter</html>', { status: 200 }))
|
||||
})
|
||||
globalThis.fetch = fetchMock as never
|
||||
|
||||
const loader = makeStreamingLoader(42, '/api/v1/epub', { manifest: [{ href: 'text/chapter 1.xhtml', size: 20 }] }, 'legacy-token', null, 9)
|
||||
|
||||
await expect(loader.loadText('text/chapter 1.xhtml')).resolves.toBe('<html>chapter</html>')
|
||||
expect(calls).toEqual([{ url: chapterUrl, auth: 'Bearer legacy-token' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: vi.fn<() => Promise<Response>>(),
|
||||
api: vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(),
|
||||
getAccessToken: vi.fn<() => string | null>(),
|
||||
}))
|
||||
|
||||
vi.mock('../useFoliateAnnotations', () => ({
|
||||
@@ -34,7 +35,7 @@ vi.mock('../useFoliateInput', () => ({
|
||||
useFoliateInput: () => inputMock,
|
||||
}))
|
||||
|
||||
import { api } from '@/lib/api'
|
||||
import { api, getAccessToken } from '@/lib/api'
|
||||
import { useFoliate } from '../useFoliate'
|
||||
|
||||
describe('useFoliate.open', () => {
|
||||
@@ -86,6 +87,7 @@ describe('useFoliate.open', () => {
|
||||
ok: true,
|
||||
json: vi.fn<() => Promise<unknown>>().mockResolvedValue({}),
|
||||
} as unknown as Response)
|
||||
vi.mocked(getAccessToken).mockReturnValue('reader-token')
|
||||
;(window as { makeStreamingBook?: unknown }).makeStreamingBook = vi
|
||||
.fn<(...args: unknown[]) => Promise<unknown>>()
|
||||
.mockResolvedValue({ type: 'book' })
|
||||
@@ -112,7 +114,22 @@ describe('useFoliate.open', () => {
|
||||
await foliate.open(1, 2, 'epub', null, undefined)
|
||||
|
||||
const makeStreamingBook = (window as unknown as { makeStreamingBook: ReturnType<typeof vi.fn> }).makeStreamingBook
|
||||
expect(makeStreamingBook).toHaveBeenCalledWith(1, '/api/v1/epub', {}, api, null, 2)
|
||||
expect(makeStreamingBook).toHaveBeenCalledWith(1, '/api/v1/epub', {}, expect.any(Function), null, 2)
|
||||
const fetchFile = makeStreamingBook.mock.calls[0]![3] as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
||||
await fetchFile('/api/v1/epub/1/file/chapter.xhtml')
|
||||
expect(api).toHaveBeenCalledWith('/api/v1/epub/1/file/chapter.xhtml', undefined)
|
||||
})
|
||||
|
||||
it('passes a token-stringable fetcher for old cached Foliate assets', async () => {
|
||||
const foliate = useFoliate(() => container)
|
||||
|
||||
await foliate.open(1, 2, 'epub', null, undefined)
|
||||
|
||||
const makeStreamingBook = (window as unknown as { makeStreamingBook: ReturnType<typeof vi.fn> }).makeStreamingBook
|
||||
const fetchFile = makeStreamingBook.mock.calls[0]![3] as { toString: () => string; [Symbol.toPrimitive]: () => string }
|
||||
expect(String(fetchFile)).toBe('reader-token')
|
||||
vi.mocked(getAccessToken).mockReturnValue('fresh-reader-token')
|
||||
expect(`${fetchFile}`).toBe('fresh-reader-token')
|
||||
})
|
||||
|
||||
it('forces fixed-layout EPUB spreads off before opening when requested', async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { api } from '@/lib/api'
|
||||
import { api, getAccessToken } from '@/lib/api'
|
||||
import { useFoliateAnnotations } from './useFoliateAnnotations'
|
||||
import { useFoliateSelection } from './useFoliateSelection'
|
||||
import { useFoliateInput } from './useFoliateInput'
|
||||
@@ -39,6 +39,11 @@ export interface EpubOpenOptions {
|
||||
fixedLayoutSpread?: EpubReaderSettings['fixedLayoutSpread']
|
||||
}
|
||||
|
||||
type FoliateFetchFile = ((input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) & {
|
||||
toString: () => string
|
||||
[Symbol.toPrimitive]: () => string
|
||||
}
|
||||
|
||||
function isResolvedNavigation(value: unknown): boolean {
|
||||
return Boolean(value && typeof value === 'object' && typeof (value as { index?: unknown }).index === 'number')
|
||||
}
|
||||
@@ -59,6 +64,14 @@ function applyEpubOpenOptions(book: unknown, options: EpubOpenOptions | undefine
|
||||
}
|
||||
}
|
||||
|
||||
function makeFoliateFetchFile(): FoliateFetchFile {
|
||||
const fetchFile = ((input: RequestInfo | URL, init?: RequestInit) => api(input, init)) as FoliateFetchFile
|
||||
const currentToken = () => getAccessToken() ?? ''
|
||||
fetchFile.toString = currentToken
|
||||
fetchFile[Symbol.toPrimitive] = currentToken
|
||||
return fetchFile
|
||||
}
|
||||
|
||||
export function useFoliate(
|
||||
container: () => HTMLElement | null,
|
||||
onRelocate?: (detail: RelocateDetail) => void,
|
||||
@@ -243,7 +256,7 @@ export function useFoliate(
|
||||
) => Promise<unknown>)
|
||||
| undefined
|
||||
if (!makeStreamingBook) throw new Error('makeStreamingBook not available')
|
||||
const book = await makeStreamingBook(bookId, '/api/v1/epub', bookInfo, api, null, fileId)
|
||||
const book = await makeStreamingBook(bookId, '/api/v1/epub', bookInfo, makeFoliateFetchFile(), null, fileId)
|
||||
applyEpubOpenOptions(book, options)
|
||||
shouldRestoreByFraction = isFixedLayoutBook(book)
|
||||
isFixedLayout.value = shouldRestoreByFraction
|
||||
|
||||
@@ -79,6 +79,7 @@ export default defineConfig({
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
||||
globIgnores: ['**/assets/foliate/**'],
|
||||
navigateFallback: 'index.html',
|
||||
navigateFallbackDenylist: [/^\/api\//],
|
||||
runtimeCaching: [
|
||||
|
||||
Reference in New Issue
Block a user