@tamagui/web 3.0.3 → 3.0.4
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/dist/cjs/_withNativeStyle.cjs +85 -46
- package/dist/cjs/_withNativeStyle.native.cjs +91 -41
- package/dist/cjs/_withNativeStyle.native.js +91 -41
- package/dist/cjs/_withNativeStyle.native.js.map +1 -1
- package/dist/cjs/helpers/nativeStyleEngine.cjs +140 -69
- package/dist/cjs/helpers/nativeStyleEngine.native.cjs +177 -118
- package/dist/cjs/helpers/nativeStyleEngine.native.js +177 -118
- package/dist/cjs/helpers/nativeStyleEngine.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/_withNativeStyle.mjs +85 -40
- package/dist/esm/_withNativeStyle.mjs.map +1 -1
- package/dist/esm/_withNativeStyle.native.js +92 -42
- package/dist/esm/_withNativeStyle.native.js.map +1 -1
- package/dist/esm/helpers/nativeStyleEngine.mjs +141 -71
- package/dist/esm/helpers/nativeStyleEngine.mjs.map +1 -1
- package/dist/esm/helpers/nativeStyleEngine.native.js +177 -119
- package/dist/esm/helpers/nativeStyleEngine.native.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.native.js +2 -2
- package/package.json +17 -17
- package/src/_withNativeStyle.tsx +141 -55
- package/src/helpers/nativeStyleEngine.ts +230 -101
- package/src/index.ts +1 -0
- package/src/setupHooks.ts +1 -0
- package/types/_withNativeStyle.d.ts +22 -2
- package/types/_withNativeStyle.d.ts.map +1 -1
- package/types/helpers/nativeStyleEngine.d.ts +24 -9
- package/types/helpers/nativeStyleEngine.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/setupHooks.d.ts +1 -0
- package/types/setupHooks.d.ts.map +1 -1
package/src/_withNativeStyle.tsx
CHANGED
|
@@ -1,20 +1,47 @@
|
|
|
1
1
|
import { setRef } from '@tamagui/compose-refs'
|
|
2
|
-
import
|
|
2
|
+
import { isAndroid, isIos } from '@tamagui/constants'
|
|
3
|
+
import React, { use } from 'react'
|
|
3
4
|
import { _withStableStyle } from './_withStableStyle'
|
|
4
5
|
import {
|
|
5
6
|
getNativeStyleEngine,
|
|
6
7
|
linkNativeStyleMapping,
|
|
7
8
|
resolveNativeStyleMapping,
|
|
8
|
-
type NativeStyleEngineLinkHandle,
|
|
9
9
|
type NativeStyleThemeMapping,
|
|
10
10
|
} from './helpers/nativeStyleEngine'
|
|
11
11
|
import { getThemeState, ThemeStateContext } from './hooks/useThemeState'
|
|
12
|
+
import { hooks } from './setupHooks'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
// the host props RN's <Text> passes for a non-pressable text outside a text
|
|
15
|
+
// ancestor, given none of the props it remaps (the compiler guarantees that)
|
|
16
|
+
const hostTextProps = {
|
|
17
|
+
accessible: isAndroid ? false : isIos ? true : undefined,
|
|
18
|
+
allowFontScaling: true,
|
|
19
|
+
ellipsizeMode: 'tail',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* internal: emitted by the native compiler when experimental.nativeFastPath is on.
|
|
24
|
+
*
|
|
25
|
+
* One component over the host: the theme scope is the only context read, the
|
|
26
|
+
* host ref links straight to the engine and unlinks through React's ref
|
|
27
|
+
* cleanup, so a themed view costs one component and one engine call to mount.
|
|
28
|
+
*
|
|
29
|
+
* `host: 'view'`: `Component` is RN's host view (`unstable_NativeView`).
|
|
30
|
+
* <View> resets TextAncestorContext for its children when it sits inside a
|
|
31
|
+
* <Text>; this wrapper does that reset itself, so the host view can carry
|
|
32
|
+
* children without changing what a nested <Text> renders.
|
|
33
|
+
*
|
|
34
|
+
* `host: 'text'`: `Component` is RN's <Text>, and outside a text ancestor this
|
|
35
|
+
* renders its host (`unstable_NativeText`) with the exact props <Text> would
|
|
36
|
+
* pass: the defaults above, `overflow: 'hidden'` first in the style, a string
|
|
37
|
+
* fontWeight, and TextAncestorContext only around element children. Inside a
|
|
38
|
+
* text ancestor it renders <Text>, which owns the virtual text path.
|
|
39
|
+
*/
|
|
14
40
|
export const _withNativeStyle = (
|
|
15
41
|
Component: any,
|
|
16
42
|
baseStyle: Record<string, unknown>,
|
|
17
|
-
themeStyleKeys: NativeStyleThemeMapping
|
|
43
|
+
themeStyleKeys: NativeStyleThemeMapping,
|
|
44
|
+
host?: 'view' | 'text'
|
|
18
45
|
) => {
|
|
19
46
|
const Fallback = _withStableStyle(
|
|
20
47
|
Component,
|
|
@@ -28,62 +55,121 @@ export const _withNativeStyle = (
|
|
|
28
55
|
true,
|
|
29
56
|
false
|
|
30
57
|
)
|
|
58
|
+
const baseViews = host ? hooks.getBaseViews?.() : undefined
|
|
59
|
+
const TextAncestor: React.Context<boolean> | undefined = baseViews?.TextAncestor
|
|
60
|
+
const TextAncestorProvider = TextAncestor?.Provider as React.Provider<boolean>
|
|
61
|
+
// <Text> rewrites userSelect and verticalAlign into other props
|
|
62
|
+
const HostText =
|
|
63
|
+
host === 'text' && !('userSelect' in baseStyle) && !('verticalAlign' in baseStyle)
|
|
64
|
+
? baseViews?.NativeText
|
|
65
|
+
: undefined
|
|
66
|
+
const hostTextBase = HostText
|
|
67
|
+
? {
|
|
68
|
+
overflow: 'hidden',
|
|
69
|
+
...baseStyle,
|
|
70
|
+
...(typeof baseStyle.fontWeight === 'number' && {
|
|
71
|
+
fontWeight: String(baseStyle.fontWeight),
|
|
72
|
+
}),
|
|
73
|
+
}
|
|
74
|
+
: baseStyle
|
|
75
|
+
// one style array per scope and host mode, its theme slot set to the scope's
|
|
76
|
+
// current state on every render. the engine owns a linked view's theme
|
|
77
|
+
// values, so a re-render after a switch hands React the same array, its diff
|
|
78
|
+
// skips the style, and the host is not re-committed with values the engine
|
|
79
|
+
// already applied. a new host reads the array's current state at creation
|
|
80
|
+
const styles = new Map<string, (object | null)[]>()
|
|
81
|
+
const hostTextStyles = new Map<string, (object | null)[]>()
|
|
82
|
+
// bundlers inline imports as a module lookup at every use; bind them once
|
|
83
|
+
// here, outside the per-mount path
|
|
84
|
+
const ScopeContext = ThemeStateContext
|
|
85
|
+
const engineOf = getNativeStyleEngine
|
|
86
|
+
const scopeState = getThemeState
|
|
87
|
+
const resolve = resolveNativeStyleMapping
|
|
88
|
+
const linkMapping = linkNativeStyleMapping
|
|
89
|
+
const assignRef = setRef
|
|
31
90
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { ref, _expressions: _ignoredExpressions, ...rest } = props
|
|
40
|
-
const link = useRef<NativeStyleEngineLinkHandle | null>(null)
|
|
41
|
-
// a host the engine refuses (a nitro view) never gets theme pushes, so
|
|
42
|
-
// this view renders through the theme-subscribed fallback instead
|
|
43
|
-
const [refused, setRefused] = useState(false)
|
|
44
|
-
const themeState = getThemeState(_scopeId)
|
|
45
|
-
const stateName = themeState?.name
|
|
46
|
-
const theme = themeState?.theme
|
|
47
|
-
const hostRef = useCallback(
|
|
48
|
-
(host: unknown) => {
|
|
49
|
-
link.current?.unlink()
|
|
50
|
-
link.current = null
|
|
51
|
-
setRef(ref, host)
|
|
52
|
-
if (host && stateName && theme) {
|
|
53
|
-
link.current = linkNativeStyleMapping(
|
|
54
|
-
host,
|
|
55
|
-
baseStyle,
|
|
56
|
-
themeStyleKeys,
|
|
57
|
-
_scopeId,
|
|
58
|
-
stateName,
|
|
59
|
-
theme
|
|
60
|
-
)
|
|
61
|
-
if (!link.current) setRefused(true)
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
[ref, _scopeId, stateName, theme]
|
|
65
|
-
)
|
|
66
|
-
if (refused) return <Fallback {...props} />
|
|
67
|
-
const stateStyle =
|
|
68
|
-
stateName && theme
|
|
69
|
-
? resolveNativeStyleMapping(themeStyleKeys, stateName, theme)
|
|
70
|
-
: undefined
|
|
91
|
+
// `use` reads context from whichever render calls it, so the compiler can
|
|
92
|
+
// call this straight from the parent component's render (see `inline`)
|
|
93
|
+
const render = (props: any, key?: React.Key | null) => {
|
|
94
|
+
const scopeId = use(ScopeContext)
|
|
95
|
+
const inText = TextAncestor ? use(TextAncestor) : false
|
|
96
|
+
const themeState = scopeId && engineOf() ? scopeState(scopeId) : undefined
|
|
97
|
+
const resetsText = inText && host === 'view'
|
|
71
98
|
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
if (!themeState) {
|
|
100
|
+
return resetsText ? (
|
|
101
|
+
<TextAncestorProvider key={key} value={false}>
|
|
102
|
+
<Fallback {...props} />
|
|
103
|
+
</TextAncestorProvider>
|
|
104
|
+
) : (
|
|
105
|
+
<Fallback key={key} {...props} />
|
|
75
106
|
)
|
|
76
107
|
}
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
// compiled createElement calls carry an `_expressions` prop for the
|
|
110
|
+
// fallback; jsx never does, so it forwards its props without a copy
|
|
111
|
+
let rest = props
|
|
112
|
+
if (props._expressions !== undefined) {
|
|
113
|
+
const { _expressions: _ignoredExpressions, ...withoutExpressions } = props
|
|
114
|
+
rest = withoutExpressions
|
|
115
|
+
}
|
|
116
|
+
const ref = props.ref
|
|
117
|
+
const { name, theme } = themeState
|
|
118
|
+
const asHostText = HostText && !inText
|
|
119
|
+
const Rendered = asHostText ? HostText : Component
|
|
120
|
+
const cache = asHostText ? hostTextStyles : styles
|
|
121
|
+
let style = cache.get(scopeId)
|
|
122
|
+
if (!style) {
|
|
123
|
+
style = [asHostText ? hostTextBase : baseStyle, null]
|
|
124
|
+
cache.set(scopeId, style)
|
|
125
|
+
}
|
|
126
|
+
style[1] = resolve(themeStyleKeys, name, theme)
|
|
127
|
+
const wrapped = resetsText || (asHostText && mayRenderText(rest.children))
|
|
128
|
+
const element = (
|
|
129
|
+
<Rendered
|
|
130
|
+
key={wrapped ? undefined : key}
|
|
131
|
+
{...(asHostText && hostTextProps)}
|
|
132
|
+
{...rest}
|
|
133
|
+
ref={(node: unknown) => {
|
|
134
|
+
// a callback ref may return its own cleanup (React 19), which then
|
|
135
|
+
// replaces the null call
|
|
136
|
+
let refCleanup: unknown
|
|
137
|
+
if (typeof ref === 'function') refCleanup = ref(node)
|
|
138
|
+
else assignRef(ref, node)
|
|
139
|
+
const link = linkMapping(node, themeStyleKeys, scopeId, name, theme)
|
|
140
|
+
return () => {
|
|
141
|
+
link?.unlink()
|
|
142
|
+
if (typeof refCleanup === 'function') refCleanup()
|
|
143
|
+
else assignRef(ref, null)
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
style={style}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
80
149
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<Fallback {...props} />
|
|
150
|
+
if (!wrapped) return element
|
|
151
|
+
return (
|
|
152
|
+
<TextAncestorProvider key={key} value={!resetsText}>
|
|
153
|
+
{element}
|
|
154
|
+
</TextAncestorProvider>
|
|
87
155
|
)
|
|
88
|
-
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const NativeStyleView = (props: any) => render(props)
|
|
159
|
+
// the compiler emits `View.inline(<View ... />)` where the calling
|
|
160
|
+
// component's theme scope is provably the element's own: the element is
|
|
161
|
+
// only built, never mounted, and its host renders in the caller's place
|
|
162
|
+
NativeStyleView.inline = (element: React.ReactElement<any>) =>
|
|
163
|
+
render(element.props, element.key)
|
|
164
|
+
return NativeStyleView
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// <Text>'s own check for children that could render a nested <Text>: arrays
|
|
168
|
+
// longer than three are assumed to
|
|
169
|
+
function mayRenderText(children: unknown): boolean {
|
|
170
|
+
if (children == null) return false
|
|
171
|
+
if (Array.isArray(children) && children.length <= 3) {
|
|
172
|
+
return children.some((child) => child != null && typeof child === 'object')
|
|
173
|
+
}
|
|
174
|
+
return typeof children === 'object'
|
|
89
175
|
}
|
|
@@ -86,27 +86,45 @@ export interface NativeViewStateUpdate {
|
|
|
86
86
|
props?: Record<string, unknown>
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export interface NativeViewStateTableUpdate {
|
|
90
|
-
id: number
|
|
91
|
-
state: string
|
|
92
|
-
props: Record<string, unknown>
|
|
93
|
-
}
|
|
94
|
-
|
|
95
89
|
export interface NativeStyleEngineSlots {
|
|
96
|
-
base?: Record<string, unknown>
|
|
97
90
|
state?: Record<string, Record<string, unknown>>
|
|
98
91
|
}
|
|
99
92
|
|
|
100
93
|
export type NativeStyleThemeMapping = Record<string, string>
|
|
101
94
|
|
|
102
95
|
export interface NativeStyleEngine {
|
|
96
|
+
/**
|
|
97
|
+
* links a mounted host. `table` is a shared table id from setTableState
|
|
98
|
+
* (compiler mappings) or per-view slots (runtime views driven through
|
|
99
|
+
* applyViewStates). `commitNow` commits the table's active state for the
|
|
100
|
+
* view once, for a host whose render raced a scope change.
|
|
101
|
+
*/
|
|
103
102
|
link(
|
|
104
103
|
ref: unknown,
|
|
105
|
-
|
|
106
|
-
scopeId?: string
|
|
104
|
+
table: NativeStyleEngineSlots | number,
|
|
105
|
+
scopeId?: string,
|
|
106
|
+
commitNow?: boolean
|
|
107
107
|
): NativeStyleEngineLinkHandle | null
|
|
108
|
+
/**
|
|
109
|
+
* links many mounted hosts to shared tables in one call: entry i links
|
|
110
|
+
* refs[i] to tableIds[i] under scopes[scopeIndexes[i]]. returns the id of
|
|
111
|
+
* entry 0; ids are consecutive, and an entry whose host is gone consumes
|
|
112
|
+
* its id unlinked
|
|
113
|
+
*/
|
|
114
|
+
linkTables(
|
|
115
|
+
refs: unknown[],
|
|
116
|
+
tableIds: number[],
|
|
117
|
+
scopeIndexes: number[],
|
|
118
|
+
scopes: string[]
|
|
119
|
+
): number
|
|
120
|
+
/** unlinks an id from linkTables */
|
|
121
|
+
unlink(id: number): void
|
|
122
|
+
/**
|
|
123
|
+
* fills one state of a shared table, read by every view linked to it;
|
|
124
|
+
* views already showing that state commit the new props
|
|
125
|
+
*/
|
|
126
|
+
setTableState(tableId: number, state: string, props: Record<string, unknown>): void
|
|
108
127
|
applyViewStates(entries: NativeViewStateUpdate[]): void
|
|
109
|
-
updateViewStateTables(entries: NativeViewStateTableUpdate[]): void
|
|
110
128
|
processStyleColors(props: Record<string, unknown>): Record<string, unknown>
|
|
111
129
|
setStateName(stateName: string, scopeId?: string): void
|
|
112
130
|
removeScope(scopeId: string): void
|
|
@@ -115,12 +133,6 @@ export interface NativeStyleEngine {
|
|
|
115
133
|
let engine: NativeStyleEngine | null = null
|
|
116
134
|
const scopeStates = new Map<string, { name: string; theme: object }>()
|
|
117
135
|
|
|
118
|
-
interface CompiledMappingLink {
|
|
119
|
-
handle: NativeStyleEngineLinkHandle
|
|
120
|
-
scopeId: string
|
|
121
|
-
stateThemes: Map<string, object>
|
|
122
|
-
}
|
|
123
|
-
|
|
124
136
|
interface ResolvedMappingState {
|
|
125
137
|
theme: object
|
|
126
138
|
/** raw resolved values, what React renders through RN's own style normalizer */
|
|
@@ -132,17 +144,19 @@ interface ResolvedMappingState {
|
|
|
132
144
|
interface CompiledMapping {
|
|
133
145
|
mapping: NativeStyleThemeMapping
|
|
134
146
|
states: Map<string, ResolvedMappingState>
|
|
135
|
-
links
|
|
147
|
+
/** the engine table every view of this mapping links to */
|
|
148
|
+
tableId: number
|
|
149
|
+
/** the resolved state last sent to the engine table, per state name */
|
|
150
|
+
sent: Map<string, ResolvedMappingState>
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
const mappingKeys = new WeakMap<object, string>()
|
|
139
154
|
const compiledMappings = new Map<string, CompiledMapping>()
|
|
140
|
-
// linked mappings
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
>()
|
|
155
|
+
// linked mappings per scope, with their live link counts. they remain update
|
|
156
|
+
// owners across memo-generation rollovers, and a scope change only syncs the
|
|
157
|
+
// mappings linked under that scope
|
|
158
|
+
const scopeMappings = new Map<string, Map<CompiledMapping, number>>()
|
|
159
|
+
let nextTableId = 1
|
|
146
160
|
|
|
147
161
|
function getCompiledMapping(mapping: NativeStyleThemeMapping): CompiledMapping {
|
|
148
162
|
let key = mappingKeys.get(mapping)
|
|
@@ -156,7 +170,12 @@ function getCompiledMapping(mapping: NativeStyleThemeMapping): CompiledMapping {
|
|
|
156
170
|
}
|
|
157
171
|
let compiled = compiledMappings.get(key)
|
|
158
172
|
if (!compiled) {
|
|
159
|
-
compiled = {
|
|
173
|
+
compiled = {
|
|
174
|
+
mapping,
|
|
175
|
+
states: new Map(),
|
|
176
|
+
tableId: nextTableId++,
|
|
177
|
+
sent: new Map(),
|
|
178
|
+
}
|
|
160
179
|
if (compiledMappings.size >= 10_000) {
|
|
161
180
|
compiledMappings.clear()
|
|
162
181
|
}
|
|
@@ -184,19 +203,31 @@ function resolveCompiledMapping(
|
|
|
184
203
|
const nativeProps = engine?.processStyleColors(renderProps) ?? renderProps
|
|
185
204
|
if (compiled.states.size >= 10_000) {
|
|
186
205
|
compiled.states.clear()
|
|
187
|
-
for (const link of compiled.links) {
|
|
188
|
-
link.stateThemes.clear()
|
|
189
|
-
}
|
|
190
206
|
}
|
|
191
207
|
const resolved: ResolvedMappingState = { theme, renderProps, nativeProps }
|
|
192
208
|
compiled.states.set(stateName, resolved)
|
|
193
209
|
return resolved
|
|
194
210
|
}
|
|
195
211
|
|
|
212
|
+
/** resolves one state of a mapping and sends it to the engine table once */
|
|
213
|
+
function syncTableState(
|
|
214
|
+
compiled: CompiledMapping,
|
|
215
|
+
stateName: string,
|
|
216
|
+
theme: Record<string, unknown>
|
|
217
|
+
): void {
|
|
218
|
+
const resolved = resolveCompiledMapping(compiled, stateName, theme)
|
|
219
|
+
if (compiled.sent.get(stateName) === resolved) return
|
|
220
|
+
compiled.sent.set(stateName, resolved)
|
|
221
|
+
engine!.setTableState(compiled.tableId, stateName, resolved.nativeProps)
|
|
222
|
+
}
|
|
223
|
+
|
|
196
224
|
export function setNativeStyleEngine(next: NativeStyleEngine | null): void {
|
|
197
225
|
if (engine !== next) {
|
|
226
|
+
settleReleases()
|
|
198
227
|
compiledMappings.clear()
|
|
199
|
-
|
|
228
|
+
scopeMappings.clear()
|
|
229
|
+
pendingLinks = []
|
|
230
|
+
unlinkedPending = 0
|
|
200
231
|
}
|
|
201
232
|
engine = next
|
|
202
233
|
if (next) {
|
|
@@ -217,26 +248,19 @@ export function updateNativeStyleScope(
|
|
|
217
248
|
): void {
|
|
218
249
|
const previous = scopeStates.get(scopeId)
|
|
219
250
|
if (previous?.name === stateName && previous.theme === theme) return
|
|
251
|
+
// pending views rendered the outgoing state: link them before it changes
|
|
252
|
+
flushNativeStyleLinks()
|
|
220
253
|
scopeStates.set(scopeId, { name: stateName, theme })
|
|
221
254
|
|
|
222
255
|
if (engine) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (linkedTheme === theme) continue
|
|
230
|
-
props ||= resolveCompiledMapping(
|
|
231
|
-
compiled,
|
|
232
|
-
stateName,
|
|
233
|
-
theme as Record<string, unknown>
|
|
234
|
-
).nativeProps
|
|
235
|
-
entries.push({ id: link.handle.id, state: stateName, props })
|
|
236
|
-
link.stateThemes.set(stateName, theme)
|
|
256
|
+
// one table entry per mapping, never per view: a state name resolves to
|
|
257
|
+
// one theme, so every scope showing it reads the same table entry
|
|
258
|
+
const linked = scopeMappings.get(scopeId)
|
|
259
|
+
if (linked) {
|
|
260
|
+
for (const compiled of linked.keys()) {
|
|
261
|
+
syncTableState(compiled, stateName, theme as Record<string, unknown>)
|
|
237
262
|
}
|
|
238
263
|
}
|
|
239
|
-
if (entries.length) engine.updateViewStateTables(entries)
|
|
240
264
|
}
|
|
241
265
|
engine?.setStateName(stateName, scopeId)
|
|
242
266
|
}
|
|
@@ -254,9 +278,112 @@ export function resolveNativeStyleMapping(
|
|
|
254
278
|
return resolveCompiledMapping(getCompiledMapping(mapping), stateName, theme).renderProps
|
|
255
279
|
}
|
|
256
280
|
|
|
281
|
+
interface MappingLink {
|
|
282
|
+
ref: unknown
|
|
283
|
+
compiled: CompiledMapping
|
|
284
|
+
scopeId: string
|
|
285
|
+
/** 0 while pending, -1 once unlinked */
|
|
286
|
+
id: number
|
|
287
|
+
/** the engine that assigned `id`: a later engine swap must not receive its unlink */
|
|
288
|
+
owner: NativeStyleEngine | null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// views link lazily: a mount only records its host here, and the engine
|
|
292
|
+
// links every pending view in one call before a scope next changes. linking
|
|
293
|
+
// exists for theme switches, so a mount pays nothing native, and a view that
|
|
294
|
+
// unmounts before any switch never reaches the engine
|
|
295
|
+
let pendingLinks: MappingLink[] = []
|
|
296
|
+
// pending entries unlinked before any flush; compacted once they dominate
|
|
297
|
+
let unlinkedPending = 0
|
|
298
|
+
|
|
299
|
+
function trackScopeMapping(link: MappingLink, delta: 1 | -1) {
|
|
300
|
+
let linked = scopeMappings.get(link.scopeId)
|
|
301
|
+
if (!linked) scopeMappings.set(link.scopeId, (linked = new Map()))
|
|
302
|
+
const count = (linked.get(link.compiled) ?? 0) + delta
|
|
303
|
+
if (count) {
|
|
304
|
+
linked.set(link.compiled, count)
|
|
305
|
+
} else {
|
|
306
|
+
linked.delete(link.compiled)
|
|
307
|
+
if (!linked.size) scopeMappings.delete(link.scopeId)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** links every pending view in one engine call */
|
|
312
|
+
export function flushNativeStyleLinks(): void {
|
|
313
|
+
settleReleases()
|
|
314
|
+
if (!pendingLinks.length) return
|
|
315
|
+
const links = pendingLinks
|
|
316
|
+
pendingLinks = []
|
|
317
|
+
unlinkedPending = 0
|
|
318
|
+
if (!engine) return
|
|
319
|
+
const live = links.filter((link) => link.id === 0)
|
|
320
|
+
if (!live.length) return
|
|
321
|
+
const refs: unknown[] = []
|
|
322
|
+
const tableIds: number[] = []
|
|
323
|
+
const scopeIndexes: number[] = []
|
|
324
|
+
const scopes: string[] = []
|
|
325
|
+
for (const link of live) {
|
|
326
|
+
let scopeIndex = scopes.indexOf(link.scopeId)
|
|
327
|
+
if (scopeIndex === -1) scopeIndex = scopes.push(link.scopeId) - 1
|
|
328
|
+
refs.push(link.ref)
|
|
329
|
+
tableIds.push(link.compiled.tableId)
|
|
330
|
+
scopeIndexes.push(scopeIndex)
|
|
331
|
+
}
|
|
332
|
+
const firstId = engine.linkTables(refs, tableIds, scopeIndexes, scopes)
|
|
333
|
+
for (let i = 0; i < live.length; i++) {
|
|
334
|
+
const link = live[i]!
|
|
335
|
+
link.id = firstId + i
|
|
336
|
+
link.owner = engine
|
|
337
|
+
link.ref = null
|
|
338
|
+
trackScopeMapping(link, 1)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// React detaches and re-attaches a callback ref whenever the callback changes,
|
|
343
|
+
// which a parent re-render does to every child. a release settles after the
|
|
344
|
+
// commit, so the same host attaching again keeps its link and the engine keeps
|
|
345
|
+
// its per-view caches instead of unlinking and relinking every view
|
|
346
|
+
interface Release {
|
|
347
|
+
compiled: CompiledMapping
|
|
348
|
+
scopeId: string
|
|
349
|
+
handle: NativeStyleEngineLinkHandle
|
|
350
|
+
settle: () => void
|
|
351
|
+
}
|
|
352
|
+
const releases = new Map<unknown, Release>()
|
|
353
|
+
let settleQueued = false
|
|
354
|
+
|
|
355
|
+
function settleReleases() {
|
|
356
|
+
settleQueued = false
|
|
357
|
+
if (!releases.size) return
|
|
358
|
+
const settling = [...releases.values()]
|
|
359
|
+
releases.clear()
|
|
360
|
+
for (const release of settling) release.settle()
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function releasable(
|
|
364
|
+
ref: unknown,
|
|
365
|
+
compiled: CompiledMapping,
|
|
366
|
+
scopeId: string,
|
|
367
|
+
id: number,
|
|
368
|
+
settle: () => void
|
|
369
|
+
): NativeStyleEngineLinkHandle {
|
|
370
|
+
const handle: NativeStyleEngineLinkHandle = {
|
|
371
|
+
id,
|
|
372
|
+
unlink: () => {
|
|
373
|
+
const prior = releases.get(ref)
|
|
374
|
+
if (prior && prior.handle !== handle) prior.settle()
|
|
375
|
+
releases.set(ref, { compiled, scopeId, handle, settle })
|
|
376
|
+
if (!settleQueued) {
|
|
377
|
+
settleQueued = true
|
|
378
|
+
queueMicrotask(settleReleases)
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
return handle
|
|
383
|
+
}
|
|
384
|
+
|
|
257
385
|
export function linkNativeStyleMapping(
|
|
258
386
|
ref: unknown,
|
|
259
|
-
baseStyle: Record<string, unknown>,
|
|
260
387
|
mapping: NativeStyleThemeMapping,
|
|
261
388
|
scopeId: string,
|
|
262
389
|
stateName: string,
|
|
@@ -266,71 +393,73 @@ export function linkNativeStyleMapping(
|
|
|
266
393
|
|
|
267
394
|
const compiled = getCompiledMapping(mapping)
|
|
268
395
|
// a scope can advance (layout effect) after a concurrent render captured its
|
|
269
|
-
// theme but before that render's host ref links.
|
|
270
|
-
//
|
|
271
|
-
//
|
|
396
|
+
// theme but before that render's host ref links. link that host now against
|
|
397
|
+
// the live scope state and have the engine commit it once to correct the
|
|
398
|
+
// stale React commit.
|
|
272
399
|
const scopeState = scopeStates.get(scopeId)
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
let base = processedBases.get(baseStyle)
|
|
282
|
-
if (!base || base.engine !== engine) {
|
|
283
|
-
base = {
|
|
284
|
-
engine,
|
|
285
|
-
props: engine.processStyleColors(normalizeNativeStyle(baseStyle)),
|
|
286
|
-
}
|
|
287
|
-
processedBases.set(baseStyle, base)
|
|
288
|
-
}
|
|
289
|
-
const state: Record<string, Record<string, unknown>> = {}
|
|
290
|
-
const stateThemes = new Map<string, object>()
|
|
291
|
-
for (const [name, cached] of compiled.states) {
|
|
292
|
-
state[name] = cached.nativeProps
|
|
293
|
-
stateThemes.set(name, cached.theme)
|
|
400
|
+
const stale =
|
|
401
|
+
scopeState && (scopeState.name !== stateName || scopeState.theme !== theme)
|
|
402
|
+
const kept = releases.get(ref)
|
|
403
|
+
if (kept) {
|
|
404
|
+
releases.delete(ref)
|
|
405
|
+
if (!stale && kept.compiled === compiled && kept.scopeId === scopeId)
|
|
406
|
+
return kept.handle
|
|
407
|
+
kept.settle()
|
|
294
408
|
}
|
|
295
|
-
state[currentStateName] = stateProps
|
|
296
|
-
stateThemes.set(currentStateName, currentTheme)
|
|
297
|
-
const handle = engine.link(ref, { base: base.props, state }, scopeId)
|
|
298
|
-
if (!handle) return null
|
|
299
|
-
|
|
300
409
|
if (stale) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
compiled.links.delete(link)
|
|
317
|
-
if (!compiled.links.size) {
|
|
318
|
-
activeCompiledMappings.delete(compiled)
|
|
319
|
-
}
|
|
410
|
+
syncTableState(compiled, scopeState.name, scopeState.theme as Record<string, unknown>)
|
|
411
|
+
const handle = engine.link(ref, compiled.tableId, scopeId, true)
|
|
412
|
+
if (!handle) return null
|
|
413
|
+
const link: MappingLink = {
|
|
414
|
+
ref: null,
|
|
415
|
+
compiled,
|
|
416
|
+
scopeId,
|
|
417
|
+
id: handle.id,
|
|
418
|
+
owner: engine,
|
|
419
|
+
}
|
|
420
|
+
trackScopeMapping(link, 1)
|
|
421
|
+
return releasable(ref, compiled, scopeId, handle.id, () => {
|
|
422
|
+
if (link.id === -1) return
|
|
423
|
+
link.id = -1
|
|
424
|
+
if (link.owner === engine) trackScopeMapping(link, -1)
|
|
320
425
|
handle.unlink()
|
|
321
|
-
}
|
|
426
|
+
})
|
|
322
427
|
}
|
|
428
|
+
|
|
429
|
+
// the table must hold the state this view rendered before a switch links it
|
|
430
|
+
syncTableState(compiled, stateName, theme)
|
|
431
|
+
const link: MappingLink = { ref, compiled, scopeId, id: 0, owner: null }
|
|
432
|
+
pendingLinks.push(link)
|
|
433
|
+
return releasable(ref, compiled, scopeId, 0, () => {
|
|
434
|
+
const id = link.id
|
|
435
|
+
if (id === -1) return
|
|
436
|
+
link.id = -1
|
|
437
|
+
link.ref = null
|
|
438
|
+
if (id > 0) {
|
|
439
|
+
// scope accounting was reset when the engine changed
|
|
440
|
+
if (link.owner === engine) trackScopeMapping(link, -1)
|
|
441
|
+
link.owner!.unlink(id)
|
|
442
|
+
} else if (++unlinkedPending > 512 && unlinkedPending * 2 > pendingLinks.length) {
|
|
443
|
+
pendingLinks = pendingLinks.filter((pending) => pending.id === 0)
|
|
444
|
+
unlinkedPending = 0
|
|
445
|
+
}
|
|
446
|
+
})
|
|
323
447
|
}
|
|
324
448
|
|
|
325
449
|
/** memo-generation and live-owner sizes for development diagnostics and probes */
|
|
326
|
-
export const getNativeStyleEngineCacheStats = () =>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
450
|
+
export const getNativeStyleEngineCacheStats = () => {
|
|
451
|
+
const active = new Set(
|
|
452
|
+
[...scopeMappings.values()].flatMap((linked) => [...linked.keys()])
|
|
453
|
+
)
|
|
454
|
+
return {
|
|
455
|
+
mappings: compiledMappings.size,
|
|
456
|
+
activeMappings: active.size,
|
|
457
|
+
states: [...new Set([...compiledMappings.values(), ...active])].reduce(
|
|
458
|
+
(total, compiled) => total + compiled.states.size,
|
|
459
|
+
0
|
|
460
|
+
),
|
|
461
|
+
}
|
|
462
|
+
}
|
|
334
463
|
|
|
335
464
|
let pending: NativeViewStateUpdate[] | null = null
|
|
336
465
|
let flushListener: ((entries: NativeViewStateUpdate[]) => void) | null = null
|
package/src/index.ts
CHANGED