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/lib/index.esm.js +93 -15
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +93 -15
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +93 -15
- package/lib/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/cache.ts +129 -1
- package/src/components/InternationalizedArrayContext.tsx +35 -3
- package/src/components/Preload.tsx +16 -6
- package/src/schema/array.ts +53 -28
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
14
|
-
Array.isArray(props.languages)
|
|
15
|
-
|
|
16
|
-
|
|
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
|
package/src/schema/array.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
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',
|