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
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules
.DS_Store
/test-results
.idea
/packages/*/test-app/test-results/.last-run.json
/packages/svelte/test-app/vite.config.js.timestamp-*.mjs
/playwright-report
/test-results
node_modules
13 changes: 12 additions & 1 deletion packages/core/src/prefetched.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ class PrefetchedRequests {
protected removalTimers: PrefetchRemovalTimer[] = []
protected currentUseId: string | null = null

public add(params: ActiveVisit, sendFunc: (params: InternalActiveVisit) => void, { cacheFor }: PrefetchOptions) {
public add(
params: ActiveVisit,
sendFunc: (params: InternalActiveVisit) => void,
{ cacheFor, cacheTags }: PrefetchOptions,
) {
const inFlight = this.findInFlight(params)

if (inFlight) {
Expand Down Expand Up @@ -70,6 +74,7 @@ class PrefetchedRequests {
singleUse: expires === 0,
timestamp: Date.now(),
inFlight: false,
tags: Array.isArray(cacheTags) ? cacheTags : [cacheTags],
})

this.scheduleForRemoval(params, expires)
Expand Down Expand Up @@ -98,6 +103,12 @@ class PrefetchedRequests {
this.removalTimers = []
}

public removeByTags(tags: string[]): void {
this.cached = this.cached.filter((prefetched) => {
return !prefetched.tags.some((tag) => tags.includes(tag))
})
}

public remove(params: ActiveVisit): void {
this.cached = this.cached.filter((prefetched) => {
return !this.paramsAreEqual(prefetched.params, params)
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/response.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AxiosResponse } from 'axios'
import { router } from '.'
import { fireErrorEvent, fireInvalidEvent, firePrefetchedEvent, fireSuccessEvent } from './events'
import { history } from './history'
import modal from './modal'
Expand Down Expand Up @@ -64,6 +65,8 @@ export class Response {
return this.requestParams.all().onError(scopedErrors)
}

router.flushByCacheTags(this.requestParams.all().invalidateCacheTags || [])

fireSuccessEvent(currentPage.get())

await this.requestParams.all().onSuccess(currentPage.get())
Expand Down
18 changes: 15 additions & 3 deletions packages/core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ export class Router {
prefetchedRequests.removeAll()
}

public flushByCacheTags(tags: string | string[]): void {
prefetchedRequests.removeByTags(Array.isArray(tags) ? tags : [tags])
}

public getPrefetching(
href: string | URL | UrlMethodPair,
options: VisitOptions = {},
Expand All @@ -225,7 +229,7 @@ export class Router {
public prefetch(
href: string | URL | UrlMethodPair,
options: VisitOptions = {},
{ cacheFor = 30_000 }: PrefetchOptions,
prefetchOptions: Partial<PrefetchOptions> = {},
) {
const method: Method = options.method ?? (isUrlMethodPair(href) ? href.method : 'get')

Expand Down Expand Up @@ -284,7 +288,11 @@ export class Router {
(params) => {
this.asyncRequestStream.send(Request.create(params, currentPage.get()))
},
{ cacheFor },
{
cacheFor: 30_000,
cacheTags: [],
...prefetchOptions,
},
)
})
}
Expand All @@ -309,7 +317,10 @@ export class Router {
this.clientVisit(params)
}

protected clientVisit<TProps = Page['props']>(params: ClientSideVisitOptions<TProps>, { replace = false }: { replace?: boolean } = {}): void {
protected clientVisit<TProps = Page['props']>(
params: ClientSideVisitOptions<TProps>,
{ replace = false }: { replace?: boolean } = {},
): void {
const current = currentPage.get()

const props =
Expand Down Expand Up @@ -385,6 +396,7 @@ export class Router {
reset: [],
preserveUrl: false,
prefetch: false,
invalidateCacheTags: [],
...options,
}

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export type Visit<T extends RequestPayload = RequestPayload> = {
fresh: boolean
reset: string[]
preserveUrl: boolean
invalidateCacheTags: string | string[]
}

export type GlobalEventsMap<T extends RequestPayload = RequestPayload> = {
Expand Down Expand Up @@ -350,6 +351,7 @@ export type CacheForOption = number | string

export type PrefetchOptions = {
cacheFor: CacheForOption | CacheForOption[]
cacheTags: string | string[]
}

export interface LinkComponentBaseProps
Expand All @@ -372,6 +374,7 @@ export interface LinkComponentBaseProps
onCancelToken: (cancelToken: import('axios').CancelTokenSource) => void
prefetch: boolean | LinkPrefetchOption | LinkPrefetchOption[]
cacheFor: CacheForOption | CacheForOption[]
cacheTags: string | string[]
}
> {}

Expand All @@ -395,6 +398,7 @@ export type PrefetchedResponse = PrefetchObject & {
timestamp: number
singleUse: boolean
inFlight: false
tags: string[]
}

export type PrefetchRemovalTimer = {
Expand Down Expand Up @@ -426,7 +430,7 @@ export type FormComponentOptions = Pick<
>

export type FormComponentProps = Partial<
Pick<Visit, 'headers' | 'queryStringArrayFormat' | 'errorBag' | 'showProgress'> &
Pick<Visit, 'headers' | 'queryStringArrayFormat' | 'errorBag' | 'showProgress' | 'invalidateCacheTags'> &
Omit<VisitCallbacks, 'onPrefetched' | 'onPrefetching'>
> & {
method?: Method | Uppercase<Method>
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
onCancelToken = noop,
onSubmitComplete = noop,
disableWhileProcessing = false,
invalidateCacheTags = [],
children,
...props
},
Expand Down Expand Up @@ -110,6 +111,7 @@ const Form = forwardRef<FormComponentRef, ComponentProps>(
headers,
errorBag,
showProgress,
invalidateCacheTags,
onCancelToken,
onBefore,
onStart,
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const Link = forwardRef<unknown, InertiaLinkProps>(
onPrefetched = noop,
prefetch = false,
cacheFor = 0,
cacheTags = [],
...props
},
ref,
Expand Down Expand Up @@ -158,10 +159,10 @@ const Link = forwardRef<unknown, InertiaLinkProps>(
onPrefetching,
onPrefetched,
},
{ cacheFor: cacheForValue },
{ cacheFor: cacheForValue, cacheTags },
)
}
}, [url, baseParams, onPrefetching, onPrefetched, cacheForValue])
}, [url, baseParams, onPrefetching, onPrefetched, cacheForValue, cacheTags])

useEffect(() => {
return () => {
Expand Down
43 changes: 43 additions & 0 deletions packages/react/test-app/Pages/FormComponent/InvalidateTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Form, Link } from '@inertiajs/react'

export default ({ lastLoaded, propType }: { lastLoaded: number; propType: string }) => {
return (
<div>
<div id="links">
<Link href="/prefetch/tags/1" prefetch="hover" cacheTags={propType === 'string' ? 'user' : ['user']}>
User Tagged Page
</Link>
<Link href="/prefetch/tags/2" prefetch="hover" cacheTags={propType === 'string' ? 'product' : ['product']}>
Product Tagged Page
</Link>
</div>

<div id="form-section">
<h3>Form Component with invalidateCacheTags</h3>
<Form
action="/dump/post"
method="post"
invalidateCacheTags={propType === 'string' ? 'user' : ['user']}
>
<input
id="form-name"
name="name"
type="text"
placeholder="Enter name"
defaultValue=""
/>
<button id="submit-invalidate-user" type="submit">
Submit (Invalidate User Tags)
</button>
</Form>
</div>

<div>
<div>Form Component Invalidate Tags Test Page</div>
<div>
Last loaded at <span id="last-loaded">{lastLoaded}</span>
</div>
</div>
</div>
)
}
95 changes: 95 additions & 0 deletions packages/react/test-app/Pages/Prefetch/Tags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Link, router, useForm } from '@inertiajs/react'

export default ({ pageNumber, lastLoaded, propType }: { pageNumber: number; lastLoaded: number; propType: string }) => {
const form = useForm({
name: '',
})

const flushUserTags = () => {
router.flushByCacheTags(propType === 'string' ? 'user' : ['user'])
}

const flushUserProductTags = () => {
router.flushByCacheTags(['user', 'product'])
}

const programmaticPrefetch = () => {
router.prefetch('/prefetch/tags/2', { method: 'get' }, { cacheTags: propType === 'string' ? 'user' : ['user'] })
router.prefetch(
'/prefetch/tags/3',
{ method: 'get' },
{ cacheFor: '1m', cacheTags: propType === 'string' ? 'product' : ['product'] },
)
router.prefetch(
'/prefetch/tags/6',
{ method: 'get' },
{ cacheFor: '1m' }, // No tags (untagged)
)
}

const submitWithUserInvalidation = (e: React.MouseEvent) => {
e.preventDefault()
form.post('/dump/post', {
invalidateCacheTags: propType === 'string' ? 'user' : ['user'],
})
}

return (
<div>
<div id="links">
<Link href="/prefetch/tags/1" prefetch="hover" cacheTags={['user', 'profile']}>
User Page 1
</Link>
<Link href="/prefetch/tags/2" prefetch="hover" cacheTags={['user', 'settings']}>
User Page 2
</Link>
<Link href="/prefetch/tags/3" prefetch="hover" cacheTags={['product', 'catalog']}>
Product Page 3
</Link>
<Link href="/prefetch/tags/4" prefetch="hover" cacheTags={['product', 'details']}>
Product Page 4
</Link>
<Link href="/prefetch/tags/5" prefetch="hover" cacheTags={propType === 'string' ? 'admin' : ['admin']}>
Admin Page 5
</Link>
<Link href="/prefetch/tags/6" prefetch="hover">
Untagged Page 6
</Link>
</div>
<div id="controls">
<button id="flush-user" onClick={flushUserTags}>
Flush User Tags
</button>
<button id="flush-user-product" onClick={flushUserProductTags}>
Flush User + Product Tags
</button>
<button id="programmatic-prefetch" onClick={programmaticPrefetch}>
Programmatic Prefetch
</button>
</div>

<div id="form-section">
<h3>Form Test</h3>
<form onSubmit={(e) => e.preventDefault()}>
<input
id="form-name"
value={form.data.name}
onChange={(e) => form.setData('name', e.target.value)}
type="text"
placeholder="Enter name"
/>
<button id="submit-invalidate-user" onClick={submitWithUserInvalidation}>
Submit (Invalidate User)
</button>
</form>
</div>

<div>
<div>This is tags page {pageNumber}</div>
<div>
Last loaded at <span id="last-loaded">{lastLoaded}</span>
</div>
</div>
</div>
)
}
1 change: 0 additions & 1 deletion packages/react/test-app/Pages/Prefetch/Wayfinder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export default function Wayfinder() {
setTimeout(checkStatus)
},
},
{ cacheFor: 5000 },
)
}

Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/components/Form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
export let onError: FormComponentProps['onError'] = noop
export let onSubmitComplete: FormComponentProps['onSubmitComplete'] = noop
export let disableWhileProcessing: boolean = false
export let invalidateCacheTags: FormComponentProps['invalidateCacheTags'] = []

type FormSubmitOptions = Omit<VisitOptions, 'data' | 'onPrefetched' | 'onPrefetching'>

Expand Down Expand Up @@ -65,6 +66,7 @@
headers,
errorBag,
showProgress,
invalidateCacheTags,
onCancelToken,
onBefore,
onStart,
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/components/Link.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
export let async: boolean = false
export let prefetch: boolean | LinkPrefetchOption | LinkPrefetchOption[] = false
export let cacheFor: CacheForOption | CacheForOption[] = 0
export let cacheTags: string | string[] = []

$: _method = typeof href === 'object' ? href.method : method
$: _href = typeof href === 'object' ? href.url : href
Expand Down Expand Up @@ -52,6 +53,7 @@
async,
prefetch,
cacheFor,
cacheTags,
}}
{...$$restProps}
{...elProps}
Expand Down
Loading