sanity-plugin-internationalized-array 3.1.3 → 3.1.5

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.
package/src/cache.ts CHANGED
@@ -2,19 +2,147 @@
2
2
 
3
3
  import * as suspend from 'suspend-react'
4
4
 
5
- import type {Language} from './types'
5
+ import type {Language, LanguageCallback} from './types'
6
6
 
7
7
  export const namespace = 'sanity-plugin-internationalized-array'
8
8
 
9
9
  export const version = 'v1'
10
10
 
11
+ // Simple in-memory cache for validation functions that run outside React context
12
+ const validationCache = new Map<string, Language[]>()
13
+
14
+ // Cache for function references to enable sharing between same functions
15
+ const functionCache = new Map<string, Language[]>()
16
+
17
+ // Cache for function keys to avoid recalculating them
18
+ const functionKeyCache = new WeakMap<LanguageCallback, string>()
19
+
11
20
  // https://github.com/pmndrs/suspend-react#preloading
12
21
  export const preload = (fn: () => Promise<Language[]>) =>
13
22
  suspend.preload(() => fn(), [version, namespace])
14
23
 
24
+ // Enhanced preload function that can use custom cache keys
25
+ export const preloadWithKey = (
26
+ fn: () => Promise<Language[]>,
27
+ key: (string | number)[]
28
+ ) => suspend.preload(() => fn(), key)
29
+
15
30
  // https://github.com/pmndrs/suspend-react#cache-busting
16
31
  export const clear = () => suspend.clear([version, namespace])
17
32
 
18
33
  // https://github.com/pmndrs/suspend-react#peeking-into-entries-outside-of-suspense
19
34
  export const peek = (selectedValue: Record<string, unknown>) =>
20
35
  suspend.peek([version, namespace, selectedValue]) as Language[] | undefined
36
+
37
+ // Helper function to create a stable cache key that matches the component's key structure
38
+ export const createCacheKey = (
39
+ selectedValue: Record<string, unknown>,
40
+ workspaceId?: string
41
+ ) => {
42
+ const selectedValueHash = JSON.stringify(selectedValue)
43
+ return workspaceId
44
+ ? [version, namespace, selectedValueHash, workspaceId]
45
+ : [version, namespace, selectedValueHash]
46
+ }
47
+
48
+ // Enhanced peek function that can work with workspace context
49
+ export const peekWithWorkspace = (
50
+ selectedValue: Record<string, unknown>,
51
+ workspaceId?: string
52
+ ) =>
53
+ suspend.peek(createCacheKey(selectedValue, workspaceId)) as
54
+ | Language[]
55
+ | undefined
56
+
57
+ // Generate a unique key for a function reference (cached for performance)
58
+ export const getFunctionKey = (fn: LanguageCallback): string => {
59
+ // Check if we already have a cached key for this function
60
+ const cachedKey = functionKeyCache.get(fn)
61
+ if (cachedKey) {
62
+ return cachedKey
63
+ }
64
+
65
+ // Create a hash for functions (only when needed)
66
+ const fnStr = fn.toString()
67
+ let hash = 0
68
+ // Only hash the first 100 characters for performance
69
+ const maxLength = Math.min(fnStr.length, 100)
70
+ for (let i = 0; i < maxLength; i++) {
71
+ const char = fnStr.charCodeAt(i)
72
+ // eslint-disable-next-line no-bitwise
73
+ hash = (hash << 5) - hash + char
74
+ // eslint-disable-next-line no-bitwise
75
+ hash &= hash // Convert to 32-bit integer
76
+ }
77
+ const key = `anonymous_${Math.abs(hash)}`
78
+ functionKeyCache.set(fn, key)
79
+ return key
80
+ }
81
+
82
+ // Create a cache key that includes function identity
83
+ export const createFunctionCacheKey = (
84
+ fn: LanguageCallback,
85
+ selectedValue: Record<string, unknown>,
86
+ workspaceId?: string
87
+ ): string => {
88
+ const functionKey = getFunctionKey(fn)
89
+ const selectedValueHash = JSON.stringify(selectedValue)
90
+ return workspaceId
91
+ ? `${functionKey}:${selectedValueHash}:${workspaceId}`
92
+ : `${functionKey}:${selectedValueHash}`
93
+ }
94
+
95
+ // Cache for validation functions with function awareness
96
+ export const getValidationCache = (key: string): Language[] | undefined => {
97
+ return validationCache.get(key)
98
+ }
99
+
100
+ export const setValidationCache = (
101
+ key: string,
102
+ languages: Language[]
103
+ ): void => {
104
+ validationCache.set(key, languages)
105
+ }
106
+
107
+ export const clearValidationCache = (): void => {
108
+ validationCache.clear()
109
+ }
110
+
111
+ // Function-aware cache operations
112
+ export const getFunctionCache = (
113
+ fn: LanguageCallback,
114
+ selectedValue: Record<string, unknown>,
115
+ workspaceId?: string
116
+ ): Language[] | undefined => {
117
+ const key = createFunctionCacheKey(fn, selectedValue, workspaceId)
118
+ return functionCache.get(key)
119
+ }
120
+
121
+ export const setFunctionCache = (
122
+ fn: LanguageCallback,
123
+ selectedValue: Record<string, unknown>,
124
+ languages: Language[],
125
+ workspaceId?: string
126
+ ): void => {
127
+ const key = createFunctionCacheKey(fn, selectedValue, workspaceId)
128
+ functionCache.set(key, languages)
129
+ }
130
+
131
+ export const clearFunctionCache = (): void => {
132
+ functionCache.clear()
133
+ }
134
+
135
+ // Clear function key cache as well
136
+ export const clearAllCaches = (): void => {
137
+ functionCache.clear()
138
+ // Note: WeakMap doesn't have a clear method, but it will be garbage collected
139
+ // when the function references are no longer held
140
+ }
141
+
142
+ // Check if two functions are the same reference
143
+ export const isSameFunction = (
144
+ fn1: LanguageCallback,
145
+ fn2: LanguageCallback
146
+ ): boolean => {
147
+ return fn1 === fn2 || getFunctionKey(fn1) === getFunctionKey(fn2)
148
+ }
@@ -6,7 +6,7 @@ import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
6
6
  import {useDocumentPane} from 'sanity/structure'
7
7
  import {suspend} from 'suspend-react'
8
8
 
9
- import {namespace, version} from '../cache'
9
+ import {createCacheKey, setFunctionCache} from '../cache'
10
10
  import {CONFIG_DEFAULT} from '../constants'
11
11
  import type {Language, PluginConfig} from '../types'
12
12
  import DocumentAddButtons from './DocumentAddButtons'
@@ -49,6 +49,27 @@ export function InternationalizedArrayProvider(
49
49
  [internationalizedArray.select, deferredDocument]
50
50
  )
51
51
 
52
+ // Use a stable workspace identifier to prevent unnecessary re-renders
53
+ const workspaceId = useMemo(() => {
54
+ // Use workspace name if available, otherwise create a stable hash
55
+ if (workspace?.name) {
56
+ return workspace.name
57
+ }
58
+ // Create a stable hash from workspace properties that matter for caching
59
+ const workspaceKey = {
60
+ name: workspace?.name,
61
+ title: workspace?.title,
62
+ // Add other stable properties as needed
63
+ }
64
+ return JSON.stringify(workspaceKey)
65
+ }, [workspace])
66
+
67
+ // Memoize the cache key to prevent expensive JSON.stringify calls
68
+ const cacheKey = useMemo(
69
+ () => createCacheKey(selectedValue, workspaceId),
70
+ [selectedValue, workspaceId]
71
+ )
72
+
52
73
  // Fetch or return languages
53
74
  const languages = Array.isArray(internationalizedArray.languages)
54
75
  ? internationalizedArray.languages
@@ -56,11 +77,22 @@ export function InternationalizedArrayProvider(
56
77
  // eslint-disable-next-line require-await
57
78
  async () => {
58
79
  if (typeof internationalizedArray.languages === 'function') {
59
- return internationalizedArray.languages(client, selectedValue)
80
+ const result = await internationalizedArray.languages(
81
+ client,
82
+ selectedValue
83
+ )
84
+ // Populate function cache for use outside React context
85
+ setFunctionCache(
86
+ internationalizedArray.languages,
87
+ selectedValue,
88
+ result,
89
+ workspaceId
90
+ )
91
+ return result
60
92
  }
61
93
  return internationalizedArray.languages
62
94
  },
63
- [version, namespace, selectedValue, workspace],
95
+ cacheKey,
64
96
  {equal}
65
97
  )
66
98
 
@@ -1,20 +1,30 @@
1
1
  import {memo} from 'react'
2
2
  import {useClient} from 'sanity'
3
3
 
4
- import {peek, preload} from '../cache'
4
+ import {createCacheKey, peek, preloadWithKey, setFunctionCache} from '../cache'
5
5
  import type {PluginConfig} from '../types'
6
6
 
7
7
  export default memo(function Preload(
8
8
  props: Required<Pick<PluginConfig, 'apiVersion' | 'languages'>>
9
9
  ) {
10
10
  const client = useClient({apiVersion: props.apiVersion})
11
+
12
+ // Use the same cache key structure as the main component
13
+ // This should match the main component when selectedValue is empty
14
+ const cacheKey = createCacheKey({})
15
+
11
16
  if (!Array.isArray(peek({}))) {
12
17
  // eslint-disable-next-line require-await
13
- preload(async () =>
14
- Array.isArray(props.languages)
15
- ? props.languages
16
- : props.languages(client, {})
17
- )
18
+ preloadWithKey(async () => {
19
+ if (Array.isArray(props.languages)) {
20
+ return props.languages
21
+ }
22
+ const result = await props.languages(client, {})
23
+ // Populate function cache for sharing with other components
24
+ // Use the same key structure as the main component
25
+ setFunctionCache(props.languages, {}, result)
26
+ return result
27
+ }, cacheKey)
18
28
  }
19
29
 
20
30
  return null
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import {defineField, type FieldDefinition, type Rule} from 'sanity'
3
3
 
4
- import {peek} from '../cache'
4
+ import {getFunctionCache, peek, setFunctionCache} from '../cache'
5
5
  import {createFieldName} from '../components/createFieldName'
6
6
  import {getSelectedValue} from '../components/getSelectedValue'
7
7
  import InternationalizedArray from '../components/InternationalizedArray'
@@ -50,7 +50,12 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
50
50
  // @ts-expect-error - fix typings
51
51
  validation: (rule: Rule) =>
52
52
  rule.custom<Value[]>(async (value, context) => {
53
- if (!value) {
53
+ if (!value || value.length === 0) {
54
+ return true
55
+ }
56
+
57
+ // Early return for simple cases to avoid expensive operations
58
+ if (value.length === 1 && !value[0]?._key) {
54
59
  return true
55
60
  }
56
61
 
@@ -65,7 +70,33 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
65
70
  } else if (Array.isArray(peek(selectedValue))) {
66
71
  contextLanguages = peek(selectedValue) || []
67
72
  } else if (typeof languagesFieldOption === 'function') {
68
- contextLanguages = await languagesFieldOption(client, selectedValue)
73
+ // Try to get from function cache first (if it's the same function as the component)
74
+ const cachedLanguages = getFunctionCache(
75
+ languagesFieldOption,
76
+ selectedValue
77
+ )
78
+
79
+ if (Array.isArray(cachedLanguages)) {
80
+ contextLanguages = cachedLanguages
81
+ } else {
82
+ // Try suspend cache as fallback
83
+ const suspendCachedLanguages = peek(selectedValue)
84
+ if (Array.isArray(suspendCachedLanguages)) {
85
+ contextLanguages = suspendCachedLanguages
86
+ } else {
87
+ // Only make the async call if we don't have cached data
88
+ contextLanguages = await languagesFieldOption(
89
+ client,
90
+ selectedValue
91
+ )
92
+ // Cache the result for future validation calls
93
+ setFunctionCache(
94
+ languagesFieldOption,
95
+ selectedValue,
96
+ contextLanguages
97
+ )
98
+ }
99
+ }
69
100
  }
70
101
 
71
102
  if (value && value.length > contextLanguages.length) {
@@ -76,12 +107,13 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
76
107
  }`
77
108
  }
78
109
 
79
- const nonLanguageKeys = value?.length
80
- ? value.filter(
81
- (item) =>
82
- !contextLanguages.find((language) => item._key === language.id)
83
- )
84
- : []
110
+ // Create a Set for faster language ID lookups
111
+ const languageIds = new Set(contextLanguages.map((lang) => lang.id))
112
+
113
+ // Check for invalid language keys
114
+ const nonLanguageKeys = value.filter(
115
+ (item) => item?._key && !languageIds.has(item._key)
116
+ )
85
117
  if (nonLanguageKeys.length) {
86
118
  return {
87
119
  message: `Array item keys must be valid languages registered to the field type`,
@@ -89,27 +121,20 @@ export default (config: ArrayFactoryConfig): FieldDefinition<'array'> => {
89
121
  }
90
122
  }
91
123
 
92
- // Ensure there's no duplicate `language` fields
93
- type KeyedValues = {
94
- [key: string]: Value[]
124
+ // Check for duplicate language keys (more efficient)
125
+ const seenKeys = new Set<string>()
126
+ const duplicateValues: Value[] = []
127
+
128
+ for (const item of value) {
129
+ if (item?._key) {
130
+ if (seenKeys.has(item._key)) {
131
+ duplicateValues.push(item)
132
+ } else {
133
+ seenKeys.add(item._key)
134
+ }
135
+ }
95
136
  }
96
137
 
97
- const valuesByLanguage = value?.length
98
- ? value
99
- .filter((item) => Boolean(item?._key))
100
- .reduce((acc, cur) => {
101
- if (acc[cur._key]) {
102
- return {...acc, [cur._key]: [...acc[cur._key], cur]}
103
- }
104
- return {
105
- ...acc,
106
- [cur._key]: [cur],
107
- }
108
- }, {} as KeyedValues)
109
- : {}
110
- const duplicateValues = Object.values(valuesByLanguage)
111
- .filter((item) => item?.length > 1)
112
- .flat()
113
138
  if (duplicateValues.length) {
114
139
  return {
115
140
  message: 'There can only be one field per language',