Skip to content

Commit

Permalink
fix(vue,scripts): support parallel triggers
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Sep 3, 2024
1 parent 0cd45ae commit bc8ee39
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 16 deletions.
6 changes: 5 additions & 1 deletion packages/schema/src/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ export interface ScriptInstance<T extends BaseScriptApi> {
entry?: ActiveHeadEntry<any>
load: () => Promise<T>
remove: () => boolean
updateTrigger: (trigger: UseScriptOptions['trigger']) => void
setupTriggerHandler: (trigger: UseScriptOptions['trigger']) => void
// cbs
onLoaded: (fn: (instance: T) => void | Promise<void>) => void
onError: (fn: (err?: Error) => void | Promise<void>) => void
/**
* @internal
*/
_triggerAbortController?: AbortController
/**
* @internal
*/
_triggerPromises?: Promise<void>[]
/**
* @internal
*/
Expand Down
21 changes: 13 additions & 8 deletions packages/unhead/src/composables/useScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
const id = resolveScriptKey(input)
const prevScript = head._scripts?.[id] as undefined | UseScriptContext<UseFunctionType<UseScriptOptions<T, U>, T>>
if (prevScript) {
prevScript.updateTrigger(options.trigger)
prevScript.setupTriggerHandler(options.trigger)
return prevScript
}
options.beforeInit?.()
Expand Down Expand Up @@ -113,6 +113,9 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
return false
},
load(cb?: () => void | Promise<void>) {
// cancel any pending triggers as we've started loading
script._triggerAbortController?.abort()
script._triggerPromises = [] // clear any pending promises
if (!script.entry) {
syncStatus('loading')
const defaults: Required<Head>['script'][0] = {
Expand All @@ -139,22 +142,24 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
onError(cb: (err?: Error) => void | Promise<void>) {
return _registerCb('error', cb)
},
updateTrigger(trigger: UseScriptOptions['trigger']) {
// cancel previous trigger
script._triggerAbortController?.abort()
setupTriggerHandler(trigger: UseScriptOptions['trigger']) {
if (script.status !== 'awaitingLoad') {
return
}
if (((typeof trigger === 'undefined' || trigger === 'client') && !head.ssr) || trigger === 'server') {
script.load()
}
else if (trigger instanceof Promise) {
script._triggerAbortController = new AbortController()
Promise.race([
script._triggerAbortController = script._triggerAbortController || new AbortController()
script._triggerPromises = script._triggerPromises || []
script._triggerPromises.push(Promise.race([
trigger.then(() => script.load),
new Promise<void>((resolve) => {
script._triggerAbortController!.signal.addEventListener('abort', () => resolve())
}),
]).then((res) => {
res?.()
})
}))
}
else if (typeof trigger === 'function') {
trigger(script.load)
Expand All @@ -177,7 +182,7 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
})
const hookCtx = { script }

script.updateTrigger(options.trigger)
script.setupTriggerHandler(options.trigger)
// support deprecated behavior
script.$script = script
const proxyChain = (instance: any, accessor?: string | symbol, accessors?: (string | symbol)[]) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/vue/src/composables/useScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ function registerVueScopeHandlers<T extends Record<symbol | string, any> = Recor

export function useScript<T extends Record<symbol | string, any> = Record<symbol | string, any>, U = Record<symbol | string, any>>(_input: UseScriptInput, _options?: UseScriptOptions<T, U>): UseScriptContext<UseFunctionType<UseScriptOptions<T, U>, T>> {
const input = (typeof _input === 'string' ? { src: _input } : _input) as UseScriptResolvedInput
const head = injectHead()
const options = _options || {}
const head = options?.head || injectHead()
// @ts-expect-error untyped
options.head = head
const scope = getCurrentInstance()
Expand Down
9 changes: 3 additions & 6 deletions test/unhead/e2e/scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('unhead e2e scripts', () => {
}, 25)
})
expect(script.status).toBe('awaitingLoad')
script.updateTrigger(newPromise)
script.setupTriggerHandler(newPromise)
expect(script.status).toBe('awaitingLoad')
await newPromise

Expand Down Expand Up @@ -114,18 +114,15 @@ describe('unhead e2e scripts', () => {
head: csrHead,
})

expect(originalAborted).toBeTruthy()

expect(scriptA.status).toEqual(scriptB.status)
expect(scriptA.status).toEqual(`awaitingLoad`)

// next tick
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve()
}, 25)
})

expect(originalAborted).toBeTruthy()

expect(scriptA.status).toEqual(scriptB.status)
expect(scriptA.status).toEqual(`loading`)
})
Expand Down
68 changes: 68 additions & 0 deletions test/vue/e2e/scripts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it } from 'vitest'
import { createHead, useScript } from '@unhead/vue'
import { ref, watch } from 'vue'
import { useDom } from '../../fixtures'

describe('unhead vue e2e scripts', () => {
it('multiple active promise handles', async () => {
const dom = useDom()
const head = createHead({
document: dom.window.document,
})

const isTrigger1Active = ref(false)
const trigger1 = new Promise<void>((resolve) => {
watch(isTrigger1Active, (val) => {
if (val) {
resolve()
}
})
})

const isTrigger2Active = ref(false)
const trigger2 = new Promise<void>((resolve) => {
watch(isTrigger2Active, (val) => {
if (val) {
resolve()
}
})
})

const { status } = useScript({
src: '//duplicate.script',
}, {
// leaving the page will stop the trigger from activating
trigger: trigger1,
head,
})

const { status: status2, _triggerPromises } = useScript({
src: '//duplicate.script',
}, {
// leaving the page will stop the trigger from activating
trigger: trigger2,
head,
})

// two promises pending
expect(_triggerPromises).toMatchInlineSnapshot(`
[
Promise {},
Promise {},
]
`)

// trigger using the first promise
isTrigger1Active.value = true
// wait next tick
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve()
}, 25)
})

// both should be loaded
expect(status.value).toEqual('loading')
expect(status2.value).toEqual('loading')
})
})

0 comments on commit bc8ee39

Please sign in to comment.