feat(fe2): parallel middlewares (#5314)
* parallel middlewares foundation + hydration mismatch * moved to fully parallel middlewares * a bit less hacky * some more cleanup * improved nuxt 4 error formatting * make parallel middlewares toggleable
This commit is contained in:
committed by
GitHub
parent
074b554faf
commit
843606775c
@@ -88,12 +88,12 @@ export default defineNuxtPlugin(() => {
|
||||
|
||||
```typescript
|
||||
// middleware/auth.ts (route middleware)
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware((to, from) => {
|
||||
// Route protection logic
|
||||
})
|
||||
|
||||
// middleware/global.global.ts (global middleware)
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware((to, from) => {
|
||||
// Runs on every route change
|
||||
})
|
||||
```
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
|
||||
<div
|
||||
v-if="isDev && error.stack"
|
||||
class="max-w-xl text-body-xs text-foreground-2"
|
||||
v-html="error.stack"
|
||||
/>
|
||||
class="whitespace-pre-line font-mono max-w-xl text-body-xs text-foreground-2"
|
||||
>
|
||||
{{ error.stack.trim() }}
|
||||
</div>
|
||||
<FormButton :to="homeRoute">Go home</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<LayoutMenu
|
||||
v-model:open="showActionsMenu"
|
||||
:items="actionsItems"
|
||||
:menu-id="menuId"
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
@@ -43,6 +44,7 @@ const props = defineProps<{
|
||||
|
||||
const { copy } = useClipboard()
|
||||
const copyModelLink = useCopyModelLink()
|
||||
const menuId = useId()
|
||||
|
||||
const disabledMessage = computed(
|
||||
() =>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { RouteMiddleware } from '#app'
|
||||
import type { NavigationGuardReturn } from '#vue-router'
|
||||
import { isUndefinedOrVoid } from '@speckle/shared'
|
||||
import { useScopedState } from '~/lib/common/composables/scopedState'
|
||||
|
||||
const useMiddlewareParallelizationState = () =>
|
||||
useScopedState('middleware_parallelization', () => ({
|
||||
middlewares: [] as Array<() => Promise<NavigationGuardReturn>>
|
||||
}))
|
||||
|
||||
/**
|
||||
* Make the middleware process in parallel with other middlewares and only get fully awaited at the end
|
||||
*/
|
||||
const withParallelization = (middleware: RouteMiddleware): RouteMiddleware => {
|
||||
return (...args) => {
|
||||
const app = useNuxtApp()
|
||||
const {
|
||||
public: { parallelMiddlewares }
|
||||
} = useRuntimeConfig()
|
||||
if (!parallelMiddlewares) {
|
||||
return middleware(...args)
|
||||
}
|
||||
|
||||
const { middlewares } = useMiddlewareParallelizationState()
|
||||
middlewares.push(async () => app.runWithContext(() => middleware(...args)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* defineParallelizedNuxtRouteMiddleware() w/ parallelization support
|
||||
*/
|
||||
export const defineParallelizedNuxtRouteMiddleware = (
|
||||
middleware: RouteMiddleware
|
||||
): RouteMiddleware => {
|
||||
return withParallelization(middleware)
|
||||
}
|
||||
|
||||
export const useFinalizeParallelMiddlewares = () => {
|
||||
const state = useMiddlewareParallelizationState()
|
||||
const logger = useLogger()
|
||||
|
||||
return {
|
||||
finalize: async () => {
|
||||
const middlewares = state.middlewares
|
||||
if (!middlewares.length) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Finalizing {count} parallel middlewares', {
|
||||
count: middlewares.length,
|
||||
middlewares
|
||||
})
|
||||
|
||||
try {
|
||||
const results = await Promise.all(middlewares.map((m) => m()))
|
||||
|
||||
// Report results
|
||||
for (const resultItem of results) {
|
||||
if (!isUndefinedOrVoid(resultItem) && resultItem !== true) {
|
||||
return resultItem
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
state.middlewares.length = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(() => {
|
||||
// Add response header that shows this is a FE2 request
|
||||
const { ssrContext } = useNuxtApp()
|
||||
if (ssrContext) {
|
||||
|
||||
@@ -67,7 +67,7 @@ const adminPageRgx = /^\/admin\/?/
|
||||
* Setting up all kinds of redirects (e.g. for FE1 backwards compatibility)
|
||||
*/
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const logger = useLogger()
|
||||
const path = to.path
|
||||
const apollo = useApolloClientFromNuxt()
|
||||
|
||||
@@ -23,7 +23,7 @@ const autoAcceptableWorkspaceInviteQuery = graphql(`
|
||||
/**
|
||||
* Handles all of the invite auto-accepting logic (when clicking on email accept links)
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
const shouldTryProjectAccept = to.path.startsWith('/projects/')
|
||||
|
||||
@@ -19,16 +19,21 @@ import { convertThrowIntoFetchResult } from '~~/lib/common/helpers/graphql'
|
||||
import { buildActiveUserWorkspaceExistenceCheckQuery } from '~/lib/workspaces/helpers/middleware'
|
||||
import { useMiddlewareQueryFetchPolicy } from '~/lib/core/composables/navigation'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to, from) => {
|
||||
const isAuthPage = to.path.startsWith('/authn/')
|
||||
const isSSOPath = to.path.includes('/sso/')
|
||||
if (isAuthPage || isSSOPath) return
|
||||
|
||||
const client = useApolloClientFromNuxt()
|
||||
const fetchPolicy = useMiddlewareQueryFetchPolicy()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
// Fetch required data
|
||||
const [{ data: serverInfoData }, { data: userData }] = await Promise.all([
|
||||
const [
|
||||
{ data: serverInfoData },
|
||||
{ data: userData },
|
||||
{ data: workspaceExistenceData }
|
||||
] = await Promise.all([
|
||||
client
|
||||
.query({
|
||||
query: mainServerInfoDataQuery
|
||||
@@ -39,7 +44,17 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
query: activeUserQuery,
|
||||
fetchPolicy: fetchPolicy(to, from)
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
.catch(convertThrowIntoFetchResult),
|
||||
...(isWorkspacesEnabled.value
|
||||
? [
|
||||
client
|
||||
.query({
|
||||
...buildActiveUserWorkspaceExistenceCheckQuery(),
|
||||
fetchPolicy: fetchPolicy(to, from)
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
]
|
||||
: [{ data: undefined }])
|
||||
])
|
||||
|
||||
// If user is not logged in, skip all checks
|
||||
@@ -81,17 +96,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
|
||||
// 3. Workspace join/create redirect
|
||||
// Everything past this point is only relevant for workspace enabled instances
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
if (!isWorkspacesEnabled.value) return
|
||||
|
||||
const { data: workspaceExistenceData } = await client
|
||||
.query({
|
||||
...buildActiveUserWorkspaceExistenceCheckQuery(),
|
||||
fetchPolicy: fetchPolicy(to, from)
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
const workspaces = workspaceExistenceData?.activeUser?.workspaces?.items ?? []
|
||||
const hasWorkspaces = workspaces.length > 0
|
||||
const hasDiscoverableWorkspaces =
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useFinalizeParallelMiddlewares } from '~/lib/core/helpers/middleware'
|
||||
|
||||
/**
|
||||
* Should be the very last middleware that's run on ALL pages and navigations
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const { finalize } = useFinalizeParallelMiddlewares()
|
||||
return await finalize()
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import { Roles } from '@speckle/shared'
|
||||
/**
|
||||
* Apply this to a page to prevent access by non-admin users
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async () => {
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
const { data } = await client
|
||||
|
||||
@@ -7,7 +7,7 @@ import { loginRoute } from '~~/lib/common/helpers/route'
|
||||
/**
|
||||
* Apply this to a page to prevent unauthenticated access
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const nuxt = useNuxtApp()
|
||||
const client = useApolloClientFromNuxt()
|
||||
const postAuthRedirect = usePostAuthRedirect({ route: to })
|
||||
|
||||
@@ -19,7 +19,7 @@ const canViewProjectTokensQuery = graphql(`
|
||||
/**
|
||||
* Apply this to a page to prevent unauthenticated access to tokens and ensure the user is the owner
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
const projectId = to.params.id as string
|
||||
|
||||
@@ -19,7 +19,7 @@ const canViewProjectSettingsQuery = graphql(`
|
||||
/**
|
||||
* Apply this to a page to prevent unauthenticated access to settings ensuring the user is a collaborator
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
// Fetch project role data to check if the user is a collaborator
|
||||
|
||||
@@ -19,7 +19,7 @@ const canViewProjectWebhooksQuery = graphql(`
|
||||
/**
|
||||
* Apply this to a page to prevent unauthenticated access to webhooks and ensure the user is the owner
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
// Fetch project role data to check if the user is the owner
|
||||
|
||||
@@ -2,7 +2,7 @@ import { projectsRoute, workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useApolloClientFromNuxt } from '~~/lib/common/composables/graphql'
|
||||
import { activeUserActiveWorkspaceCheckQuery } from '~/lib/auth/graphql/queries'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async () => {
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const exclusionList = ['authorize-app', 'reset-password']
|
||||
/**
|
||||
* Apply this to a page to prevent authenticated access
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const nuxt = useNuxtApp()
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineNuxtRouteMiddleware((to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware((to) => {
|
||||
const { ssrContext } = useNuxtApp()
|
||||
|
||||
if (ssrContext) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSetActiveWorkspace } from '~/lib/user/composables/activeWorkspace'
|
||||
/**
|
||||
* Clear active workspace when navigating to the projects page
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async () => {
|
||||
const { setActiveWorkspace } = useSetActiveWorkspace()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
@@ -6,7 +6,7 @@ import { homeRoute, workspaceCreateRoute } from '~~/lib/common/helpers/route'
|
||||
/**
|
||||
* Redirect user to /workspaces/actions/create, if they have no discoverable workspaces
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
if (!isWorkspacesEnabled.value) return
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useWorkspacePublicSsoCheck } from '~/lib/workspaces/composables/sso'
|
||||
/**
|
||||
* Used to validate that the workspace has SSO enabled, redirects to workspace page if not
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
// Skip middleware when handling SSO callback with access code.
|
||||
// This page serves as both the SSO login page and OAuth callback URL.
|
||||
// We need to let the callback through to process the access code before any redirects.
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '~/lib/common/helpers/graphql'
|
||||
import { projectAutomationAccessCheckQuery } from '~/lib/projects/graphql/queries'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const projectId = to.params.id as string
|
||||
// const automationId = to.params.aid as string
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
getFirstErrorMessage
|
||||
} from '~/lib/common/helpers/graphql'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const functionId = to.params.fid as string
|
||||
|
||||
const isAutomateEnabled = useIsAutomateModuleEnabled()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMiddlewareQueryFetchPolicy } from '~/lib/core/composables/navigation'
|
||||
import {
|
||||
castToSupportedVisibility,
|
||||
SupportedProjectVisibility
|
||||
@@ -13,16 +14,18 @@ import { projectModelCheckQuery } from '~~/lib/projects/graphql/queries'
|
||||
/**
|
||||
* Used in project page to validate that project ID refers to a valid project and redirects to 404 if not
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to, from) => {
|
||||
const projectId = to.params.id as string
|
||||
const modelId = to.params.modelId as string
|
||||
|
||||
const client = useApolloClientFromNuxt()
|
||||
const fetchPolicy = useMiddlewareQueryFetchPolicy()
|
||||
|
||||
const { data, errors } = await client
|
||||
.query({
|
||||
query: projectModelCheckQuery,
|
||||
variables: { projectId, modelId }
|
||||
variables: { projectId, modelId },
|
||||
fetchPolicy: fetchPolicy(to, from)
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useMiddlewareQueryFetchPolicy } from '~/lib/core/composables/navigation
|
||||
/**
|
||||
* Used in project page to validate that project ID refers to a valid project and redirects to 404 if not
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to, from) => {
|
||||
const projectId = to.params.id as string
|
||||
|
||||
// Check if embed token is present in URL
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useMiddlewareQueryFetchPolicy } from '~/lib/core/composables/navigation
|
||||
/**
|
||||
* Used to validate that the workspace ID refers to a valid workspace and redirects to 404 if not
|
||||
*/
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to, from) => {
|
||||
const workspaceSlug = to.params.slug as string
|
||||
|
||||
const client = useApolloClientFromNuxt()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(() => {
|
||||
const isAutomateEnabled = useIsAutomateModuleEnabled()
|
||||
if (!isAutomateEnabled.value) {
|
||||
return abortNavigation(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(() => {
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
// If workspaces are enabled, continue as normal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSettingsMenuState } from '~/lib/settings/composables/menu'
|
||||
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware((to, from) => {
|
||||
const settingsMenuState = useSettingsMenuState()
|
||||
|
||||
if (to.path.startsWith('/settings') && !from.path.startsWith('/settings')) {
|
||||
|
||||
@@ -14,7 +14,7 @@ const resolveLinkQuery = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineParallelizedNuxtRouteMiddleware(async (to) => {
|
||||
const client = useApolloClientFromNuxt()
|
||||
const threadId = to.params.threadId as string
|
||||
const projectId = to.params.id as string
|
||||
|
||||
@@ -83,12 +83,14 @@ export default defineNuxtConfig({
|
||||
datadogSite: '',
|
||||
datadogService: '',
|
||||
datadogEnv: '',
|
||||
intercomAppId: ''
|
||||
intercomAppId: '',
|
||||
parallelMiddlewares: true
|
||||
}
|
||||
},
|
||||
|
||||
experimental: {
|
||||
emitRouteChunkError: 'automatic-immediate'
|
||||
emitRouteChunkError: 'automatic-immediate',
|
||||
asyncContext: true // necessary for parallel middlewares
|
||||
},
|
||||
|
||||
alias: {
|
||||
@@ -171,7 +173,11 @@ export default defineNuxtConfig({
|
||||
headers: {
|
||||
// No search engine indexing on any of the pages anywhere! TODO: Come up with a more appropriate policy
|
||||
'X-Robots-Tag': 'noindex, nofollow, noarchive'
|
||||
}
|
||||
},
|
||||
appMiddleware: [
|
||||
// Has to be applied to all pages and as the very last app middleware (hence the 999 prefix)
|
||||
'999-parallel-finalize'
|
||||
]
|
||||
},
|
||||
'/functions': {
|
||||
redirect: {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '~/lib/common/helpers/graphql'
|
||||
import { checkIfIsInPlaceNavigation } from '~/lib/common/helpers/navigation'
|
||||
import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
|
||||
import { defineParallelizedNuxtRouteMiddleware } from '~/lib/core/helpers/middleware'
|
||||
|
||||
/**
|
||||
* Debugging helper to ensure variables are available in debugging scope
|
||||
@@ -40,5 +41,6 @@ export {
|
||||
ROOT_QUERY,
|
||||
ROOT_MUTATION,
|
||||
ROOT_SUBSCRIPTION,
|
||||
ViewerEventBusKeys
|
||||
ViewerEventBusKeys,
|
||||
defineParallelizedNuxtRouteMiddleware
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user