Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/core/src/page.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { eventHandler } from './eventHandler'
import { fireNavigateEvent } from './events'
import { history } from './history'
import { prefetchedRequests } from './prefetched'
import { Scroll } from './scroll'
import { Component, Page, PageEvent, PageHandler, PageResolver, RouterInitParams, Visit } from './types'
import { hrefToUrl, isSameUrlWithoutHash } from './url'
Expand Down Expand Up @@ -86,6 +87,10 @@ class CurrentPage {
this.page = page
this.cleared = false

if (this.hasOnceProps()) {
prefetchedRequests.updateCachedOncePropsFromCurrentPage()
}

if (isNewComponent) {
this.fireEventsFor('newComponent')
}
Expand Down Expand Up @@ -157,6 +162,10 @@ class CurrentPage {
return this.page
}

public hasOnceProps(): boolean {
return Object.keys(this.page.onceProps ?? {}).length > 0
}

public merge(data: Partial<Page>): void {
this.page = { ...this.page, ...data }
}
Expand Down Expand Up @@ -216,6 +225,21 @@ class CurrentPage {
public fireEventsFor(event: PageEvent): void {
this.listeners.filter((listener) => listener.event === event).forEach((listener) => listener.callback())
}

public mergeOncePropsIntoResponse(response: Page, { force = false }: { force?: boolean } = {}): void {
Object.entries(response.onceProps ?? {}).forEach(([key, onceProp]) => {
const existingOnceProp = this.page.onceProps?.[key]

if (existingOnceProp === undefined) {
return
}

if (force || response.props[onceProp.prop] === undefined) {
response.props[onceProp.prop] = this.page.props[existingOnceProp.prop]
response.onceProps![key].expiresAt = existingOnceProp.expiresAt
}
})
}
}

export const page = new CurrentPage()
54 changes: 51 additions & 3 deletions packages/core/src/prefetched.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { cloneDeep } from 'lodash-es'
import { objectsAreEqual } from './objectUtils'
import { page as currentPage } from './page'
import { Response } from './response'
import { timeToMs } from './time'
import {
ActiveVisit,
CacheForOption,
InFlightPrefetch,
InternalActiveVisit,
Page,
PrefetchedResponse,
PrefetchOptions,
PrefetchRemovalTimer,
Expand Down Expand Up @@ -35,7 +37,7 @@ class PrefetchedRequests {
return Promise.resolve()
}

const [stale, expires] = this.extractStaleValues(cacheFor)
const [stale, prefetchExpiresIn] = this.extractStaleValues(cacheFor)

const promise = new Promise<Response>((resolve, reject) => {
sendFunc({
Expand Down Expand Up @@ -67,17 +69,26 @@ class PrefetchedRequests {
}).then((response) => {
this.remove(params)

const pageResponse = response.getPageResponse()

currentPage.mergeOncePropsIntoResponse(pageResponse)

this.cached.push({
params: { ...params },
staleTimestamp: Date.now() + stale,
expiresAt: Date.now() + prefetchExpiresIn,
response: promise,
singleUse: expires === 0,
singleUse: prefetchExpiresIn === 0,
timestamp: Date.now(),
inFlight: false,
tags: Array.isArray(cacheTags) ? cacheTags : [cacheTags],
})

this.scheduleForRemoval(params, expires)
const oncePropExpiresIn = this.getShortestOncePropTtl(pageResponse)
this.scheduleForRemoval(
params,
oncePropExpiresIn ? Math.min(prefetchExpiresIn, oncePropExpiresIn) : prefetchExpiresIn,
)
this.removeFromInFlight(params)

response.handlePrefetch()
Expand Down Expand Up @@ -258,6 +269,43 @@ class PrefetchedRequests {
],
)
}

public updateCachedOncePropsFromCurrentPage(): void {
this.cached.forEach((prefetched) => {
prefetched.response.then((response) => {
const pageResponse = response.getPageResponse()

currentPage.mergeOncePropsIntoResponse(pageResponse, { force: true })

const oncePropExpiresIn = this.getShortestOncePropTtl(pageResponse)

if (oncePropExpiresIn === null) {
return
}

const prefetchExpiresIn = prefetched.expiresAt - Date.now()
const expiresIn = Math.min(prefetchExpiresIn, oncePropExpiresIn)

if (expiresIn > 0) {
this.scheduleForRemoval(prefetched.params, expiresIn)
} else {
this.remove(prefetched.params)
}
})
})
}

protected getShortestOncePropTtl(page: Page): number | null {
const expiryTimestamps = Object.values(page.onceProps ?? {})
.map((onceProp) => onceProp.expiresAt)
.filter((expiresAt): expiresAt is number => !!expiresAt)

if (expiryTimestamps.length === 0) {
return null
}

return Math.min(...expiryTimestamps) - Date.now()
}
}

export const prefetchedRequests = new PrefetchedRequests()
14 changes: 12 additions & 2 deletions packages/core/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,18 @@ export class Request {
'X-Inertia': true,
}

if (currentPage.get().version) {
headers['X-Inertia-Version'] = currentPage.get().version
const page = currentPage.get()

if (page.version) {
headers['X-Inertia-Version'] = page.version
}

const onceProps = Object.entries(page.onceProps || {})
.filter(([, onceProp]) => !onceProp.expiresAt || onceProp.expiresAt > Date.now())
.map(([key]) => key)

if (onceProps.length > 0) {
headers['X-Inertia-Except-Once-Props'] = onceProps.join(',')
}

return headers
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export class Response {
this.requestParams.merge(params)
}

public getPageResponse(): Page {
return (this.response.data = this.getDataFromResponse(this.response.data))
}

protected async handleNonInertiaResponse() {
if (this.isLocationVisit()) {
const locationUrl = hrefToUrl(this.getHeader('x-inertia-location'))
Expand Down Expand Up @@ -158,13 +162,14 @@ export class Response {
}

protected async setPage(): Promise<void> {
const pageResponse = this.getDataFromResponse(this.response.data)
const pageResponse = this.getPageResponse()

if (!this.shouldSetPage(pageResponse)) {
return Promise.resolve()
}

this.mergeProps(pageResponse)
currentPage.mergeOncePropsIntoResponse(pageResponse)
this.preserveEqualProps(pageResponse)

await this.setRememberedState(pageResponse)
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ export interface Page<SharedProps extends PageProps = PageProps> {
deepMergeProps?: string[]
matchPropsOn?: string[]
scrollProps?: Record<keyof PageProps, ScrollProp>
onceProps?: Record<
string,
{
prop: keyof PageProps
expiresAt?: number | null
}
>

/** @internal */
rememberedState: Record<string, unknown>
Expand Down Expand Up @@ -564,6 +571,7 @@ export type PrefetchCancellationToken = {
export type PrefetchedResponse = PrefetchObject & {
staleTimestamp: number
timestamp: number
expiresAt: number
singleUse: boolean
inFlight: false
tags: string[]
Expand Down
11 changes: 11 additions & 0 deletions packages/react/test-app/Pages/OnceProps/CustomKeyPageA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from '@inertiajs/react'

export default ({ userPermissions, bar }: { userPermissions: string; bar: string }) => {
return (
<>
<p id="permissions">Permissions: {userPermissions}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/custom-key/b">Go to Custom Key Page B</Link>
</>
)
}
11 changes: 11 additions & 0 deletions packages/react/test-app/Pages/OnceProps/CustomKeyPageB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from '@inertiajs/react'

export default ({ permissions, bar }: { permissions: string; bar: string }) => {
return (
<>
<p id="permissions">Permissions: {permissions}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/custom-key/a">Go to Custom Key Page A</Link>
</>
)
}
26 changes: 26 additions & 0 deletions packages/react/test-app/Pages/OnceProps/DeferredPageA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Deferred, Link, usePage } from '@inertiajs/react'

const Foo = () => {
const { foo } = usePage<{ foo?: { text: string } }>().props

return <>{foo?.text}</>
}

export default ({ bar }: { bar: string }) => {
return (
<>
<Deferred data="foo" fallback={<div>Loading foo...</div>}>
<p id="foo">
Foo: <Foo />
</p>
</Deferred>

<p id="bar">Bar: {bar}</p>

<Link href="/once-props/deferred/b">Go to Deferred Page B</Link>
<Link href="/once-props/deferred/c" prefetch="mount">
Go to Deferred Page C
</Link>
</>
)
}
23 changes: 23 additions & 0 deletions packages/react/test-app/Pages/OnceProps/DeferredPageB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Deferred, Link, usePage } from '@inertiajs/react'

const Foo = () => {
const { foo } = usePage<{ foo?: { text: string } }>().props

return <>{foo?.text}</>
}

export default ({ bar }: { bar: string }) => {
return (
<>
<Deferred data="foo" fallback={<div>Loading foo...</div>}>
<p id="foo">
Foo: <Foo />
</p>
</Deferred>

<p id="bar">Bar: {bar}</p>

<Link href="/once-props/deferred/a">Go to Deferred Page A</Link>
</>
)
}
23 changes: 23 additions & 0 deletions packages/react/test-app/Pages/OnceProps/DeferredPageC.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Deferred, Link, usePage } from '@inertiajs/react'

const Foo = () => {
const { foo } = usePage<{ foo?: { text: string } }>().props

return <>{foo?.text}</>
}

export default ({ bar }: { bar: string }) => {
return (
<>
<Deferred data="foo" fallback={<div>Loading foo...</div>}>
<p id="foo">
Foo: <Foo />
</p>
</Deferred>

<p id="bar">Bar: {bar}</p>

<Link href="/once-props/deferred/a">Go to Deferred Page A</Link>
</>
)
}
12 changes: 12 additions & 0 deletions packages/react/test-app/Pages/OnceProps/MergePageA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Link, router } from '@inertiajs/react'

export default ({ items, bar }: { items: string[]; bar: string }) => {
return (
<>
<p id="items">Items count: {items.length}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/merge/b">Go to Merge Page B</Link>
<button onClick={() => router.reload({ only: ['items'] })}>Load more items</button>
</>
)
}
12 changes: 12 additions & 0 deletions packages/react/test-app/Pages/OnceProps/MergePageB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Link, router } from '@inertiajs/react'

export default ({ items, bar }: { items: string[]; bar: string }) => {
return (
<>
<p id="items">Items count: {items.length}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/merge/a">Go to Merge Page A</Link>
<button onClick={() => router.reload({ only: ['items'] })}>Load more items</button>
</>
)
}
12 changes: 12 additions & 0 deletions packages/react/test-app/Pages/OnceProps/OptionalPageA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Link, router } from '@inertiajs/react'

export default ({ foo, bar }: { foo?: string; bar: string }) => {
return (
<>
<p id="foo">Foo: {foo ?? 'not loaded'}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/optional/b">Go to Optional Page B</Link>
<button onClick={() => router.reload({ only: ['foo'] })}>Load foo</button>
</>
)
}
12 changes: 12 additions & 0 deletions packages/react/test-app/Pages/OnceProps/OptionalPageB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Link, router } from '@inertiajs/react'

export default ({ foo, bar }: { foo?: string; bar: string }) => {
return (
<>
<p id="foo">Foo: {foo ?? 'not loaded'}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/optional/a">Go to Optional Page A</Link>
<button onClick={() => router.reload({ only: ['foo'] })}>Load foo</button>
</>
)
}
20 changes: 20 additions & 0 deletions packages/react/test-app/Pages/OnceProps/PageA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Link, router } from '@inertiajs/react'

export default ({ foo, bar }: { foo: string; bar: string }) => {
return (
<>
<p id="foo">Foo: {foo}</p>
<p id="bar">Bar: {bar}</p>
<Link href="/once-props/page-b">Go to Page B</Link>
<Link href="/once-props/page-c">Go to Page C</Link>
<Link href="/once-props/page-d" prefetch="mount">
Go to Page D
</Link>
<Link href="/once-props/page-e" prefetch="mount" cacheFor={1000}>
Go to Page E (short cache)
</Link>
<button onClick={() => router.reload({ only: ['foo'] })}>Reload (only foo)</button>
<button onClick={() => router.replaceProp('foo', 'replaced-foo')}>Replace foo</button>
</>
)
}
Loading