Skip to content

Commit 1c552fc

Browse files
committed
asyn requests, polling, deferred props
1 parent da297ac commit 1c552fc

24 files changed

+2593
-463
lines changed

packages/core/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"dev": "./build.js --watch",
4848
"build": "npm run clean && ./build.js && tsc --emitDeclarationOnly",
4949
"clean": "rm -rf types && rm -rf dist",
50-
"prepublishOnly": "npm run build"
50+
"prepublishOnly": "npm run build",
51+
"test": "vitest"
5152
},
5253
"dependencies": {
5354
"axios": "^1.6.0",
@@ -62,6 +63,8 @@
6263
"@types/qs": "^6.9.0",
6364
"esbuild": "^0.16.13",
6465
"esbuild-node-externals": "^1.6.0",
65-
"typescript": "^4.9.4"
66+
"happy-dom": "^14.12.3",
67+
"typescript": "^4.9.4",
68+
"vitest": "^1.6.0"
6669
}
6770
}

packages/core/src/formData.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { FormDataConvertible } from './types'
22

3+
export const isFormData = (value: any): value is FormData => value instanceof FormData
4+
35
export function objectToFormData(
46
source: Record<string, FormDataConvertible>,
57
form: FormData = new FormData(),

packages/core/src/history.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { page as currentPage } from './page'
2+
import { Page } from './types'
3+
4+
const isServer = typeof window === 'undefined'
5+
6+
export class History {
7+
public static remember(data: unknown, key: string): void {
8+
History.replaceState({
9+
...currentPage.get(),
10+
rememberedState: {
11+
...currentPage.get()?.rememberedState,
12+
[key]: data,
13+
},
14+
})
15+
}
16+
17+
public static restore(key: string): unknown {
18+
if (!isServer) {
19+
return this.getState<{ [key: string]: any }>('rememberedState', {})?.[key]
20+
}
21+
}
22+
23+
public static pushState(page: Page): void {
24+
window.history.pushState(page, '', page.url)
25+
}
26+
27+
public static replaceState(page: Page): void {
28+
window.history.replaceState(page, '', page.url)
29+
}
30+
31+
public static setState(key: string, value: any) {
32+
window.history.state[key] = value
33+
}
34+
35+
public static getState<T>(key: string, defaultValue?: T): T {
36+
return window.history.state[key] ?? defaultValue
37+
}
38+
39+
public static deleteState(key: string) {
40+
if (window.history.state?.[key] !== undefined) {
41+
delete window.history.state[key]
42+
}
43+
}
44+
45+
public static hasAnyState(): boolean {
46+
return !!this.getAllState()
47+
}
48+
49+
public static getAllState(): any {
50+
return window.history.state
51+
}
52+
}

packages/core/src/navigationType.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class NavigationType {
2+
protected type: NavigationTimingType
3+
4+
public constructor() {
5+
if (window?.performance.getEntriesByType('navigation').length > 0) {
6+
this.type = (window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming).type
7+
} else {
8+
this.type = 'navigate'
9+
}
10+
}
11+
12+
public get(): NavigationTimingType {
13+
return this.type
14+
}
15+
16+
public isBackForward(): boolean {
17+
return this.type === 'back_forward'
18+
}
19+
20+
public isReload(): boolean {
21+
return this.type === 'reload'
22+
}
23+
}
24+
25+
export const navigationType = new NavigationType()

packages/core/src/page.ts

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { fireNavigateEvent } from './events'
2+
import { History } from './history'
3+
import { Scroll } from './scroll'
4+
import { Component, Page, PageHandler, PageResolver, PreserveStateOption, RouterInitParams } from './types'
5+
import { hrefToUrl, isSameUrlWithoutHash } from './url'
6+
7+
class CurrentPage {
8+
protected page!: Page
9+
protected swapComponent!: PageHandler
10+
protected resolveComponent!: PageResolver
11+
protected componentId = {}
12+
protected onNewComponentCallbacks: VoidFunction[] = []
13+
protected firstPageLoad = true
14+
15+
public init({ initialPage, swapComponent, resolveComponent }: RouterInitParams) {
16+
this.page = initialPage
17+
this.swapComponent = swapComponent
18+
this.resolveComponent = resolveComponent
19+
20+
return this
21+
}
22+
23+
public set(
24+
page: Page,
25+
{
26+
replace = false,
27+
preserveScroll = false,
28+
preserveState = false,
29+
}: {
30+
replace?: boolean
31+
preserveScroll?: PreserveStateOption
32+
preserveState?: PreserveStateOption
33+
} = {},
34+
): Promise<void> {
35+
this.componentId = {}
36+
37+
const componentId = this.componentId
38+
39+
return this.resolve(page.component).then((component) => {
40+
if (componentId !== this.componentId) {
41+
// Component has changed since we started resolving this component, bail
42+
return
43+
}
44+
45+
page.scrollRegions ??= []
46+
page.rememberedState ??= {}
47+
replace = replace || isSameUrlWithoutHash(hrefToUrl(page.url), window.location)
48+
replace ? History.replaceState(page) : History.pushState(page)
49+
50+
const isNewComponent = !this.isTheSame(page) || this.firstPageLoad
51+
52+
this.page = page
53+
54+
if (isNewComponent) {
55+
this.onNewComponentCallbacks.forEach((cb) => cb())
56+
}
57+
58+
this.firstPageLoad = false
59+
60+
return this.swap({ component, page, preserveState }).then(() => {
61+
if (!preserveScroll) {
62+
Scroll.reset(page)
63+
}
64+
65+
if (!replace) {
66+
fireNavigateEvent(page)
67+
}
68+
})
69+
})
70+
}
71+
72+
public get(): Page {
73+
return this.page
74+
}
75+
76+
public setUrlHash(hash: string): void {
77+
this.page.url += hash
78+
}
79+
80+
public remember(data: Page['rememberedState']): void {
81+
this.page.rememberedState = data
82+
}
83+
84+
public scrollRegions(regions: Page['scrollRegions']): void {
85+
this.page.scrollRegions = regions
86+
}
87+
88+
public swap({
89+
component,
90+
page,
91+
preserveState,
92+
}: {
93+
component: Component
94+
page: Page
95+
preserveState: PreserveStateOption
96+
}): Promise<unknown> {
97+
return this.swapComponent({ component, page, preserveState })
98+
}
99+
100+
public resolve(component: string): Promise<Component> {
101+
return Promise.resolve(this.resolveComponent(component))
102+
}
103+
104+
public isTheSame(page: Page): boolean {
105+
return this.page.component === page.component
106+
}
107+
108+
public onNewComponent(cb: VoidFunction): VoidFunction {
109+
this.onNewComponentCallbacks.push(cb)
110+
111+
return () => {
112+
this.onNewComponentCallbacks = this.onNewComponentCallbacks.filter((callback) => callback !== cb)
113+
}
114+
}
115+
}
116+
117+
export const page = new CurrentPage()

packages/core/src/poll.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class Poll {
2+
protected polls: VoidFunction[] = []
3+
4+
add(interval: number, cb: VoidFunction): VoidFunction {
5+
const id = setInterval(cb, interval)
6+
7+
const stop = () => clearInterval(id)
8+
9+
this.polls.push(stop)
10+
11+
return stop
12+
}
13+
14+
clear() {
15+
this.polls.forEach((stop) => stop())
16+
17+
this.polls = []
18+
}
19+
}
20+
21+
export const poll = new Poll()

packages/core/src/progress.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { GlobalEvent } from './types'
44
let timeout: NodeJS.Timeout | null = null
55

66
function addEventListeners(delay: number): void {
7-
document.addEventListener('inertia:start', start.bind(null, delay))
7+
document.addEventListener('inertia:start', (e) => start(e, delay))
88
document.addEventListener('inertia:progress', progress)
99
document.addEventListener('inertia:finish', finish)
1010
}
1111

12-
function start(delay: number): void {
13-
timeout = setTimeout(() => NProgress.start(), delay)
12+
function start(event: GlobalEvent<'start'>, delay: number): void {
13+
if (!event.detail.visit.async) {
14+
timeout = setTimeout(() => NProgress.start(), delay)
15+
}
1416
}
1517

1618
function progress(event: GlobalEvent<'progress'>) {

packages/core/src/request.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { default as axios, AxiosProgressEvent, AxiosRequestConfig, AxiosResponse } from 'axios'
2+
import { fireExceptionEvent, fireFinishEvent, fireProgressEvent } from './events'
3+
import { page as currentPage } from './page'
4+
import { RequestParams } from './requestParams'
5+
import { Response } from './response'
6+
import { ActiveVisit, Page } from './types'
7+
import { urlWithoutHash } from './url'
8+
9+
export class Request {
10+
protected response!: AxiosResponse
11+
protected cancelToken!: AbortController
12+
protected requestParams: RequestParams
13+
14+
constructor(
15+
params: ActiveVisit,
16+
protected page: Page,
17+
) {
18+
this.requestParams = RequestParams.create(params)
19+
this.cancelToken = new AbortController()
20+
this.requestParams.onCancelToken(() => this.cancel({ cancelled: true }))
21+
}
22+
23+
public static create(params: ActiveVisit, page: Page): Request {
24+
return new Request(params, page)
25+
}
26+
27+
public async send() {
28+
return axios({
29+
method: this.requestParams.params.method,
30+
url: urlWithoutHash(this.requestParams.params.url).href,
31+
data: this.requestParams.data(),
32+
params: this.requestParams.queryParams(),
33+
signal: this.cancelToken.signal,
34+
headers: this.getHeaders(),
35+
onUploadProgress: this.onProgress.bind(this),
36+
})
37+
.then((response) => {
38+
return Response.create(this.requestParams, response, this.page).handle()
39+
})
40+
.catch((error) => {
41+
if (error?.response) {
42+
return Response.create(this.requestParams, error.response, this.page).handle()
43+
}
44+
45+
return Promise.reject(error)
46+
})
47+
.catch((error) => {
48+
if (axios.isCancel(error)) {
49+
return
50+
}
51+
52+
if (fireExceptionEvent(error)) {
53+
return Promise.reject(error)
54+
}
55+
})
56+
.finally(() => {
57+
this.finish()
58+
})
59+
}
60+
61+
protected finish(): void {
62+
if (this.requestParams.wasCancelledAtAll()) {
63+
return
64+
}
65+
66+
this.requestParams.markAsFinished()
67+
this.fireFinishEvents()
68+
}
69+
70+
protected fireFinishEvents(): void {
71+
fireFinishEvent(this.requestParams.all())
72+
this.requestParams.onFinish()
73+
}
74+
75+
public cancel({ cancelled = false, interrupted = false }: { cancelled?: boolean; interrupted?: boolean }): void {
76+
this.cancelToken.abort()
77+
78+
this.requestParams.markAsCancelled({ cancelled, interrupted })
79+
80+
this.fireFinishEvents()
81+
}
82+
83+
protected onProgress(progress: AxiosProgressEvent): void {
84+
if (this.requestParams.data() instanceof FormData) {
85+
progress.percentage = progress.progress ? Math.round(progress.progress * 100) : 0
86+
fireProgressEvent(progress)
87+
this.requestParams.params.onProgress(progress)
88+
}
89+
}
90+
91+
protected getHeaders(): AxiosRequestConfig['headers'] {
92+
const headers: AxiosRequestConfig['headers'] = {
93+
...this.requestParams.headers(),
94+
Accept: 'text/html, application/xhtml+xml',
95+
'X-Requested-With': 'XMLHttpRequest',
96+
'X-Inertia': true,
97+
}
98+
99+
if (currentPage.get().version) {
100+
headers['X-Inertia-Version'] = currentPage.get().version
101+
}
102+
103+
return headers
104+
}
105+
}

0 commit comments

Comments
 (0)