Browse Source

Merge pull request #15376 from nextcloud/perf/async-libphonenumber

perf(frontend): lazy load `libphonenumber-js`
pull/15387/head
Grigorii K. Shartsev 5 months ago
committed by GitHub
parent
commit
6e1b3a8d44
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      src/components/SelectPhoneNumber.vue
  2. 70
      src/composables/useAsyncInit.ts
  3. 33
      src/composables/useLibphonenumber.ts

16
src/components/SelectPhoneNumber.vue

@ -22,11 +22,11 @@
<script>
import { t } from '@nextcloud/l10n'
import { parsePhoneNumberFromString, validatePhoneNumberLength } from 'libphonenumber-js'
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
import NcListItem from '@nextcloud/vue/components/NcListItem'
import Phone from 'vue-material-design-icons/Phone.vue'
import Hint from './UIShared/Hint.vue'
import { useLibphonenumber } from '../composables/useLibphonenumber.ts'
import { ATTENDEE, AVATAR } from '../constants.ts'
export default {
@ -59,8 +59,12 @@ export default {
emits: ['select', 'update:participantPhoneItem'],
setup() {
const { isLibphonenumberReady, libphonenumber } = useLibphonenumber()
return {
AVATAR,
isLibphonenumberReady,
libphonenumber,
}
},
@ -70,13 +74,17 @@ export default {
* @return {import('libphonenumber-js').PhoneNumber|undefined}
*/
libPhoneNumber() {
return this.value
? parsePhoneNumberFromString(this.value)
return this.isLibphonenumberReady && this.value
? this.libphonenumber.parsePhoneNumberFromString(this.value)
: undefined
},
errorHint() {
switch (validatePhoneNumberLength(this.value)) {
if (!this.isLibphonenumberReady) {
return t('spreed', 'Loading …')
}
switch (this.libphonenumber.validatePhoneNumberLength(this.value)) {
case 'INVALID_LENGTH': return t('spreed', 'Number length is not valid')
case 'INVALID_COUNTRY': return t('spreed', 'Region code is not valid')
case 'TOO_SHORT': return t('spreed', 'Number length is too short')

70
src/composables/useAsyncInit.ts

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ShallowRef } from 'vue'
import { shallowReadonly, shallowRef } from 'vue'
type ResultContainer<T> = {
result: Readonly<ShallowRef<T | undefined>>
isReady: ShallowRef<boolean>
isLoading: ShallowRef<boolean>
init: () => Promise<void>
}
/**
* Cache modules by loader to immediately return resolved module.
* Note: the loader itself is safe to be called multiple times. It is resolved by ESM/Bundler.
* But with a promise it requires at least one tick to resolve.
*/
const cache = new Map<() => Promise<unknown>, ResultContainer<unknown>>()
/**
* Use lazy/async initialization.
* Allows to use something requiring asynchronous load/initialization with reactivity, such as dynamically imported modules or heavy initialized modules.
* Make sure that the same initiator function reference is used for the same initialization.
* @param initiator - Initialization function
* @param immediate - Whether to call the initiator immediately
*/
export function useAsyncInit<T>(initiator: () => Promise<T>, immediate: boolean = false): ResultContainer<T> {
if (cache.has(initiator)) {
return cache.get(initiator) as ResultContainer<T>
}
// Use shallowRef to avoid unexpected deep reactivity for the result
const result = shallowRef<T | undefined>(undefined)
const isReady = shallowRef(false)
const isLoading = shallowRef(false)
/**
* Initialize
*/
async function init() {
// Avoid multiple initializations
if (isReady.value || isLoading.value) {
return
}
isLoading.value = true
result.value = await initiator()
isLoading.value = false
isReady.value = true
}
const resultContainer: ResultContainer<T> = {
result: shallowReadonly(result),
isReady: shallowReadonly(isReady),
isLoading: shallowReadonly(isLoading),
init,
}
cache.set(initiator, resultContainer)
if (immediate) {
init()
}
return resultContainer
}

33
src/composables/useLibphonenumber.ts

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { useAsyncInit } from './useAsyncInit.ts'
/**
* Dynamically import libphonenumber-js module
*/
async function loadLibphonenumber() {
// Destructure immediately to help bundlers to tree-shake unused parts
const { parsePhoneNumberFromString, validatePhoneNumberLength } = await import('libphonenumber-js')
return {
parsePhoneNumberFromString,
validatePhoneNumberLength,
}
}
/**
* Use libphonenumber-js asynchronously loaded module
*/
export function useLibphonenumber() {
const {
isReady: isLibphonenumberReady,
result: libphonenumber,
} = useAsyncInit(loadLibphonenumber, true)
return {
isLibphonenumberReady,
libphonenumber,
}
}
Loading…
Cancel
Save