@fy-/fws-vue 2.3.64 → 2.3.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,15 +27,19 @@ export interface APIResult {
27
27
  status?: number
28
28
  }
29
29
 
30
- // Cache for URL parsing to avoid repeated parsing of the same URL
30
+ // Use WeakMap for caches to allow garbage collection of unused entries
31
31
  const urlParseCache = new Map<string, string>()
32
32
 
33
- // Global request hash cache shared across all instances
33
+ // Global request hash cache with size limit to prevent memory leaks
34
34
  const globalHashCache = new Map<string, number>()
35
+ const MAX_HASH_CACHE_SIZE = 1000
35
36
 
36
37
  // Track in-flight requests to avoid duplicates
37
38
  const inFlightRequests = new Map<number, Promise<any>>()
38
39
 
40
+ // Reusable TextEncoder instance
41
+ const textEncoder = new TextEncoder()
42
+
39
43
  // Detect if we're in SSR mode once and cache the result
40
44
  let isSSRMode: boolean | null = null
41
45
 
@@ -73,7 +77,7 @@ function stringifyParams(params?: RestParams): string {
73
77
  return params ? JSON.stringify(params) : ''
74
78
  }
75
79
 
76
- // Compute request hash with global caching
80
+ // Compute request hash with global caching and size limit
77
81
  function computeRequestHash(url: string, method: RestMethod, params?: RestParams): number {
78
82
  const cacheKey = `${url}|${method}|${stringifyParams(params)}`
79
83
 
@@ -83,17 +87,25 @@ function computeRequestHash(url: string, method: RestMethod, params?: RestParams
83
87
  const urlForHash = getUrlForHash(url)
84
88
  const hash = stringHash(urlForHash + method + stringifyParams(params))
85
89
 
90
+ // Implement LRU-like cache eviction when size limit is reached
91
+ if (globalHashCache.size >= MAX_HASH_CACHE_SIZE) {
92
+ // Delete the first (oldest) entry
93
+ const firstKey = globalHashCache.keys().next().value
94
+ if (firstKey !== undefined) {
95
+ globalHashCache.delete(firstKey)
96
+ }
97
+ }
98
+
86
99
  globalHashCache.set(cacheKey, hash)
87
100
  return hash
88
101
  }
89
102
 
90
- function str2ab(str) {
91
- const encoder = new TextEncoder()
92
- return encoder.encode(str)
103
+ function str2ab(str: string): Uint8Array {
104
+ return textEncoder.encode(str)
93
105
  }
94
106
 
95
- // Create HMAC signature
96
- async function createHMACSignature(secret, data) {
107
+ // Create HMAC signature with proper typing
108
+ async function createHMACSignature(secret: string, data: string): Promise<string> {
97
109
  const key = await crypto.subtle.importKey(
98
110
  'raw',
99
111
  str2ab(secret),
@@ -122,10 +134,13 @@ params?: RestParams,
122
134
  // Pre-check for server rendering state
123
135
  const isSSR = isServerRendered()
124
136
 
125
- // Handle API error response consistently
137
+ // Handle API error response consistently - memoize emitter functions
138
+ const emitMainLoading = (value: boolean) => eventBus.emit('main-loading', value)
139
+ const emitRestError = (result: any) => eventBus.emit('rest-error', result)
140
+
126
141
  function handleErrorResult<ResultType extends APIResult>(result: ResultType): Promise<ResultType> {
127
- eventBus.emit('main-loading', false)
128
- eventBus.emit('rest-error', result)
142
+ emitMainLoading(false)
143
+ emitRestError(result)
129
144
  return Promise.reject(result)
130
145
  }
131
146
 
@@ -197,8 +212,8 @@ params?: RestParams,
197
212
  serverRouter.addResult(requestHash, restError)
198
213
  }
199
214
 
200
- eventBus.emit('main-loading', false)
201
- eventBus.emit('rest-error', restError)
215
+ emitMainLoading(false)
216
+ emitRestError(restError)
202
217
  return Promise.resolve(restError)
203
218
  }
204
219
  finally {
@@ -27,22 +27,29 @@ export interface LazyHead {
27
27
  twitterCreator?: string
28
28
  }
29
29
 
30
- // Helper function to process image URLs
30
+ // Cache for processed image URLs
31
+ const processedImageUrlCache = new Map<string, string>()
32
+
33
+ // Helper function to process image URLs with caching
31
34
  function processImageUrl(image: string | undefined, imageType: string | undefined): string | undefined {
32
35
  if (!image) return undefined
33
36
 
37
+ // Create cache key
38
+ const cacheKey = `${image}|${imageType || ''}`
39
+ const cached = processedImageUrlCache.get(cacheKey)
40
+ if (cached) return cached
41
+
42
+ let result: string
34
43
  if (image.includes('?vars=')) {
35
- if (imageType) {
36
- return image.replace(
37
- '?vars=',
38
- `.${imageType.replace('image/', '')}?vars=`,
39
- )
40
- }
41
- else {
42
- return image.replace('?vars=', '.png?vars=')
43
- }
44
+ const extension = imageType ? imageType.replace('image/', '') : 'png'
45
+ result = image.replace('?vars=', `.${extension}?vars=`)
46
+ }
47
+ else {
48
+ result = image
44
49
  }
45
- return image
50
+
51
+ processedImageUrlCache.set(cacheKey, result)
52
+ return result
46
53
  }
47
54
 
48
55
  // Helper function to normalize image type
@@ -56,6 +63,11 @@ function normalizeImageType(imageType: string | undefined): 'image/jpeg' | 'imag
56
63
  return 'image/png'
57
64
  }
58
65
 
66
+ // Precomputed alternate locale URL template
67
+ function ALTERNATE_LOCALE_TEMPLATE(scheme: string, host: string, locale: string, path: string) {
68
+ return `${scheme}://${host}/l/${locale}${path}`
69
+ }
70
+
59
71
  // eslint-disable-next-line unused-imports/no-unused-vars
60
72
  export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
61
73
  const currentLocale = getLocale()
@@ -106,18 +118,26 @@ export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
106
118
  })
107
119
  */
108
120
 
109
- seoData.value.alternateLocales?.forEach((locale) => {
110
- if (locale !== currentLocale) {
111
- links.push({
112
- rel: 'alternate',
113
- hreflang: locale,
114
- href: `${urlBase.value.scheme}://${
115
- urlBase.value.host
116
- }/l/${locale}${urlBase.value.path.replace(urlBase.value.prefix, '')}`,
117
- key: `alternate-${locale}`,
118
- })
121
+ // Optimize alternate locale generation
122
+ if (seoData.value.alternateLocales?.length) {
123
+ const pathWithoutPrefix = urlBase.value.path.replace(urlBase.value.prefix, '')
124
+
125
+ for (const locale of seoData.value.alternateLocales) {
126
+ if (locale !== currentLocale) {
127
+ links.push({
128
+ rel: 'alternate',
129
+ hreflang: locale,
130
+ href: ALTERNATE_LOCALE_TEMPLATE(
131
+ urlBase.value.scheme,
132
+ urlBase.value.host,
133
+ locale,
134
+ pathWithoutPrefix,
135
+ ),
136
+ key: `alternate-${locale}`,
137
+ })
138
+ }
119
139
  }
120
- })
140
+ }
121
141
 
122
142
  /*
123
143
  if (seoData.value.image) {
@@ -134,25 +154,30 @@ export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
134
154
  },
135
155
  })
136
156
 
157
+ // Create memoized getters for frequently accessed values
158
+ const seoTitle = computed(() => seoData.value.title)
159
+ const seoDescription = computed(() => seoData.value.description)
160
+ const seoType = computed(() => seoData.value.type || 'website')
161
+ const twitterCreator = computed(() => seoData.value.twitterCreator)
162
+
137
163
  useSeoMeta({
138
164
  ogUrl: () => urlBase.value.canonical,
139
165
  ogLocale: () => localeForOg.value,
140
- robots:
141
- 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
142
- title: () => seoData.value.title || '',
143
- ogTitle: () => seoData.value.title,
144
- ogDescription: () => seoData.value.description,
166
+ robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
167
+ title: () => seoTitle.value || '',
168
+ ogTitle: () => seoTitle.value,
169
+ ogDescription: () => seoDescription.value,
145
170
  twitterCard: 'summary_large_image',
146
171
  ogSiteName: () => seoData.value.name,
147
- twitterTitle: () => seoData.value.title,
148
- twitterDescription: () => seoData.value.description,
172
+ twitterTitle: () => seoTitle.value,
173
+ twitterDescription: () => seoDescription.value,
149
174
  ogImageAlt: () => imageAlt.value,
150
175
  // @ts-expect-error: Type 'string' is not assignable to type 'undefined'.
151
- ogType: () => (seoData.value.type ? seoData.value.type : 'website'),
152
- twitterCreator: () => seoData.value.twitterCreator,
153
- twitterSite: () => seoData.value.twitterCreator,
176
+ ogType: () => seoType.value,
177
+ twitterCreator: () => twitterCreator.value,
178
+ twitterSite: () => twitterCreator.value,
154
179
  twitterImageAlt: () => imageAlt.value,
155
- description: () => seoData.value.description,
180
+ description: () => seoDescription.value,
156
181
  keywords: () => seoData.value.keywords,
157
182
  articlePublishedTime: () => seoData.value.published,
158
183
  articleModifiedTime: () => seoData.value.modified,
@@ -160,8 +185,6 @@ export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
160
185
  ogImageUrl: () => imageUrl.value,
161
186
  ogImageType: () => imageType.value,
162
187
  twitterImageUrl: () => imageUrl.value,
163
- twitterImageType() {
164
- return imageType.value
165
- },
188
+ twitterImageType: () => imageType.value,
166
189
  })
167
190
  }
@@ -22,10 +22,15 @@ export interface SSRResult {
22
22
  redirect?: string
23
23
  }
24
24
 
25
+ // Cache SSR state to avoid repeated checks
26
+ let cachedSSRState: boolean | null = null
27
+
25
28
  export function isServerRendered() {
29
+ if (cachedSSRState !== null) return cachedSSRState
30
+
26
31
  const state = getInitialState()
27
- if (state && state.isSSR) return true
28
- return false
32
+ cachedSSRState = !!(state && state.isSSR)
33
+ return cachedSSRState
29
34
  }
30
35
 
31
36
  export function initVueClient(router: Router, pinia: Pinia) {
@@ -41,8 +46,9 @@ export async function initVueServer(
41
46
  callback: Function,
42
47
  options: { url?: string } = {},
43
48
  ) {
44
- const url
45
- = options.url || `${getPath()}${getURL().Query ? `?${getURL().Query}` : ''}`
49
+ // Cache URL object to avoid multiple calls
50
+ const urlObj = getURL()
51
+ const url = options.url || `${getPath()}${urlObj.Query ? `?${urlObj.Query}` : ''}`
46
52
  const { app, router, head, pinia } = await createApp(true)
47
53
  const serverRouter = useServerRouter(pinia)
48
54
  serverRouter._setRouter(router)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.3.64",
3
+ "version": "2.3.65",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",
@@ -5,7 +5,7 @@ export interface ServerRouterState {
5
5
  _router: any | null
6
6
  status: number
7
7
  redirect?: string
8
- results: Record<number, any | undefined>
8
+ results: Map<number, any>
9
9
  }
10
10
 
11
11
  export const useServerRouter = defineStore('routerStore', {
@@ -15,7 +15,7 @@ export const useServerRouter = defineStore('routerStore', {
15
15
  _router: null,
16
16
  status: 200,
17
17
  redirect: undefined,
18
- results: {},
18
+ results: new Map(),
19
19
  }) as ServerRouterState,
20
20
  getters: {
21
21
  currentRoute: state => state._router?.currentRoute,
@@ -48,16 +48,22 @@ export const useServerRouter = defineStore('routerStore', {
48
48
  this._router?.go(1)
49
49
  },
50
50
  addResult(id: number, result: any) {
51
- this.results[id] = result
51
+ // Limit results cache size to prevent memory leaks
52
+ if (this.results.size > 100) {
53
+ // Remove oldest entries (first 10)
54
+ const keysToDelete = Array.from(this.results.keys()).slice(0, 10)
55
+ keysToDelete.forEach(key => this.results.delete(key))
56
+ }
57
+ this.results.set(id, result)
52
58
  },
53
59
  hasResult(id: number) {
54
- return this.results[id] !== undefined
60
+ return this.results.has(id)
55
61
  },
56
62
  getResult(id: number) {
57
- return this.results[id]
63
+ return this.results.get(id)
58
64
  },
59
65
  removeResult(id: number) {
60
- delete this.results[id]
66
+ this.results.delete(id)
61
67
  },
62
68
  },
63
69
  })
package/stores/user.ts CHANGED
@@ -3,7 +3,6 @@ import type { RouteLocation } from 'vue-router'
3
3
  import type { APIResult } from '../composables/rest'
4
4
  import { rest } from '@fy-/fws-js'
5
5
  import { defineStore } from 'pinia'
6
- import { computed, shallowRef } from 'vue'
7
6
  import { useServerRouter } from './serverRouter'
8
7
 
9
8
  export interface UserStore {
@@ -15,6 +14,10 @@ let refreshPromise: Promise<void> | null = null
15
14
  const refreshDebounceTime = 200 // 200ms
16
15
  let lastRefreshTime = 0
17
16
 
17
+ // Cache for API endpoints
18
+ const USER_GET_ENDPOINT = 'User:get'
19
+ const USER_LOGOUT_ENDPOINT = 'User:logout'
20
+
18
21
  export const useUserStore = defineStore('userStore', {
19
22
  state: (): UserStore => ({
20
23
  user: null,
@@ -34,7 +37,7 @@ export const useUserStore = defineStore('userStore', {
34
37
 
35
38
  lastRefreshTime = now
36
39
  refreshPromise = new Promise((resolve) => {
37
- rest('User:get', 'GET')
40
+ rest(USER_GET_ENDPOINT, 'GET')
38
41
  .then((user: APIResult) => {
39
42
  if (user.result === 'success') {
40
43
  this.setUser(user.data)
@@ -60,7 +63,7 @@ export const useUserStore = defineStore('userStore', {
60
63
 
61
64
  async logout() {
62
65
  try {
63
- const user: APIResult = await rest('User:logout', 'POST')
66
+ const user: APIResult = await rest(USER_LOGOUT_ENDPOINT, 'POST')
64
67
  // In all cases, we set the user to null
65
68
  this.setUser(null)
66
69
  return user.result === 'success'
@@ -79,23 +82,24 @@ export const useUserStore = defineStore('userStore', {
79
82
 
80
83
  // Shared implementation for route checking to avoid code duplication
81
84
  function createUserChecker(path: string, redirectLink: boolean) {
82
- // Use shallowRef for router since it doesn't need reactivity
83
- const router = shallowRef(useServerRouter())
85
+ // Get router once instead of creating shallowRef
86
+ const router = useServerRouter()
87
+
88
+ // Pre-build redirect URL template
89
+ const redirectUrl = redirectLink ? `${path}?return_to=` : path
84
90
 
85
91
  return (route: RouteLocation, isAuthenticated: boolean) => {
86
- if (!route.meta.reqLogin) return false
92
+ // Early return for most common case
93
+ if (!route.meta?.reqLogin || isAuthenticated) return false
87
94
 
88
- if (!isAuthenticated) {
89
- if (!redirectLink) {
90
- router.value.push(path)
91
- }
92
- else {
93
- router.value.status = 307
94
- router.value.push(`${path}?return_to=${route.path}`)
95
- }
96
- return true
95
+ if (redirectLink) {
96
+ router.status = 307
97
+ router.push(`${redirectUrl}${route.path}`)
98
+ }
99
+ else {
100
+ router.push(path)
97
101
  }
98
- return false
102
+ return true
99
103
  }
100
104
  }
101
105
 
@@ -105,17 +109,16 @@ export async function useUserCheckAsyncSimple(
105
109
  ) {
106
110
  const userStore = useUserStore()
107
111
  await userStore.refreshUser()
108
- const isAuth = computed(() => userStore.isAuth)
109
112
  const router = useServerRouter()
110
113
  const checkUser = createUserChecker(path, redirectLink)
111
114
 
112
115
  // Check current route immediately
113
- checkUser(router.currentRoute, isAuth.value)
116
+ checkUser(router.currentRoute, userStore.isAuth)
114
117
 
115
- // Setup route guard
118
+ // Setup route guard - use arrow function to always get current auth state
116
119
  router._router.beforeEach((to: any) => {
117
120
  if (to.fullPath !== path) {
118
- checkUser(to, isAuth.value)
121
+ checkUser(to, userStore.isAuth)
119
122
  }
120
123
  })
121
124
  }
@@ -123,46 +126,54 @@ export async function useUserCheckAsyncSimple(
123
126
  export async function useUserCheckAsync(path = '/login', redirectLink = false) {
124
127
  const userStore = useUserStore()
125
128
  await userStore.refreshUser()
126
- const isAuth = computed(() => userStore.isAuth)
127
129
  const router = useServerRouter()
128
130
  const checkUser = createUserChecker(path, redirectLink)
129
131
 
130
132
  // Check current route immediately
131
- checkUser(router.currentRoute, isAuth.value)
132
-
133
- // Setup route guards
134
- router._router.afterEach(async () => {
135
- await userStore.refreshUser()
133
+ checkUser(router.currentRoute, userStore.isAuth)
134
+
135
+ // Setup route guards - throttle afterEach refresh
136
+ let afterEachTimeout: NodeJS.Timeout | null = null
137
+ router._router.afterEach(() => {
138
+ if (afterEachTimeout) clearTimeout(afterEachTimeout)
139
+ afterEachTimeout = setTimeout(() => {
140
+ userStore.refreshUser()
141
+ afterEachTimeout = null
142
+ }, 100)
136
143
  })
137
144
 
138
145
  router._router.beforeEach((to: any) => {
139
146
  if (to.fullPath !== path) {
140
- checkUser(to, isAuth.value)
147
+ checkUser(to, userStore.isAuth)
141
148
  }
142
149
  })
143
150
  }
144
151
 
145
152
  export function useUserCheck(path = '/login', redirectLink = false) {
146
153
  const userStore = useUserStore()
147
- const isAuth = computed(() => userStore.isAuth)
148
154
  const router = useServerRouter()
149
155
  const checkUser = createUserChecker(path, redirectLink)
150
156
 
151
157
  // Check current route after refresh
152
158
  userStore.refreshUser().then(() => {
153
159
  if (router.currentRoute) {
154
- checkUser(router.currentRoute, isAuth.value)
160
+ checkUser(router.currentRoute, userStore.isAuth)
155
161
  }
156
162
  })
157
163
 
158
- // Setup route guards
159
- router._router.afterEach(async () => {
160
- await userStore.refreshUser()
164
+ // Setup route guards - throttle afterEach refresh
165
+ let afterEachTimeout: NodeJS.Timeout | null = null
166
+ router._router.afterEach(() => {
167
+ if (afterEachTimeout) clearTimeout(afterEachTimeout)
168
+ afterEachTimeout = setTimeout(() => {
169
+ userStore.refreshUser()
170
+ afterEachTimeout = null
171
+ }, 100)
161
172
  })
162
173
 
163
174
  router._router.beforeEach((to: any) => {
164
175
  if (to.fullPath !== path) {
165
- checkUser(to, isAuth.value)
176
+ checkUser(to, userStore.isAuth)
166
177
  }
167
178
  })
168
179
  }