ComponentKit/Core/ComponentContext/CKComponentContextHelper.mm (162 lines of code) (raw):
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKComponentContextHelper.h"
#import <RenderCore/RCAssert.h>
#import <ComponentKit/CKComponentScopeRoot.h>
#import <ComponentKit/CKThreadLocalComponentScope.h>
#import <ComponentKit/CKRootTreeNode.h>
#import <stack>
static NSString *const kThreadDictionaryKey = @"CKComponentContext";
struct CKComponentContextStackItem {
NSMutableDictionary *dictionary;
BOOL itemWasAdded;
};
@interface CKComponentContextValue : NSObject
{
@public
// The main store.
NSMutableDictionary *_dictionary;
// A map between render component to its dictionary.
NSMapTable<id, NSMutableDictionary *> *_renderToDictionaryCache;
// Stack of previous store dictionaries.
std::stack<CKComponentContextStackItem> _stack;
// Dirty flag for the current store in use.
BOOL _itemWasAdded;
}
@end
@implementation CKComponentContextValue @end
static CKComponentContextValue *contextValue(BOOL create)
{
NSMutableDictionary *const threadDictionary = [[NSThread currentThread] threadDictionary];
CKComponentContextValue *contextValue = threadDictionary[kThreadDictionaryKey];
if (contextValue == nil && create) {
contextValue = [CKComponentContextValue new];
contextValue->_dictionary = [NSMutableDictionary dictionary];
contextValue->_renderToDictionaryCache = [NSMapTable weakToStrongObjectsMapTable];
threadDictionary[kThreadDictionaryKey] = contextValue;
}
return contextValue;
}
bool CKComponentContextContents::operator==(const CKComponentContextContents &other) const
{
return ((other.objects == nil && objects == nil) || [other.objects isEqualToDictionary:objects]);
}
bool CKComponentContextContents::operator!=(const CKComponentContextContents &other) const
{
return !(*this == other);
}
static void clearContextValueIfEmpty(CKComponentContextValue *const currentValue)
{
if ([currentValue->_dictionary count] == 0 && currentValue->_renderToDictionaryCache.count == 0) {
[[[NSThread currentThread] threadDictionary] removeObjectForKey:kThreadDictionaryKey];
}
}
CKComponentContextPreviousState CKComponentContextHelper::store(id key, id object) noexcept
{
CKComponentContextValue *v = contextValue(YES);
NSMutableDictionary *const c = v->_dictionary;
id originalValue = c[key];
c[key] = object;
v->_itemWasAdded = YES;
CKComponentContextPreviousState state = {.key = key, .originalValue = originalValue, .newValue = object};
return state;
}
void CKComponentContextHelper::restore(const CKComponentContextPreviousState &storeResult) noexcept
{
// We want to create the context dictionary if it doesn't exist already, because we need to restore the original
// value. In practice it should always exist already except for an obscure edge case; see the unit test
// testTriplyNestedComponentContextWithNilMiddleValueCorrectlyRestoresOuterValue for an example.
CKComponentContextValue *const v = contextValue(YES);
NSMutableDictionary *const c = v->_dictionary;
id<NSCopying> storeResultKey = (id<NSCopying>)storeResult.key;
RCCAssert(c[storeResultKey] == storeResult.newValue, @"Context value for %@ unexpectedly mutated", storeResult.key);
c[storeResultKey] = storeResult.originalValue;
clearContextValueIfEmpty(v);
}
void CKComponentContextHelper::didCreateRenderComponent(id component) noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (!v) {
return;
}
// Make a backup dictionary if needed and store it in the _renderToDictionaryCache map.
if (v->_itemWasAdded) {
NSMutableDictionary *renderDictionary = [v->_dictionary mutableCopy];
[v->_renderToDictionaryCache setObject:renderDictionary forKey:component];
}
}
void CKComponentContextHelper::willBuildComponentTree(id component) noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (!v) {
return;
}
NSMutableDictionary *renderDictionary = [v->_renderToDictionaryCache objectForKey:component];
if (renderDictionary) {
// Push the current store into the stack.
v->_stack.push({
.dictionary = v->_dictionary,
.itemWasAdded = v->_itemWasAdded,
});
// Update the pointer to the latest render dictionary
v->_dictionary = renderDictionary;
v->_itemWasAdded = NO;
}
}
void CKComponentContextHelper::didBuildComponentTree(id component) noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (!v) {
return;
}
NSMutableDictionary *renderDictionary = [v->_renderToDictionaryCache objectForKey:component];
if (renderDictionary) {
RCCAssert(!v->_stack.empty(), @"The stack cannot be empty if there is a render dictionary in the cache");
RCCAssert(v->_dictionary == renderDictionary, @"The current store is different than the renderDictionary");
// Update the pointer to the latest render dictionary
if (!v->_stack.empty()) {
// Retrieve the previous value from the stack.
auto const &topItem = v->_stack.top();
v->_dictionary = topItem.dictionary;
v->_itemWasAdded = topItem.itemWasAdded;
// Pop the top backup from the stack
v->_stack.pop();
// Remove the dictionary from the map
[v->_renderToDictionaryCache removeObjectForKey:component];
}
clearContextValueIfEmpty(v);
}
}
id CKComponentContextHelper::fetchMutable(id key) noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (v) {
// Props updates support.
CKThreadLocalComponentScope *currentScope = CKThreadLocalComponentScope::currentScope();
if (currentScope != nullptr) {
[currentScope->newScopeRoot rootNode].markTopRenderComponentAsDirtyForPropsUpdates();
}
return v->_dictionary[key];
}
return nil;
}
id CKComponentContextHelper::fetch(id key) noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (v) {
return v->_dictionary[key];
}
return nil;
}
CKComponentContextContents CKComponentContextHelper::fetchAll() noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (!v) {
return {};
}
return {
.objects = [v->_dictionary copy],
};
}
NSMutableDictionary<Class, id>* CKComponentInitialValuesContext::setInitialValues(NSDictionary<Class, id> *objects) noexcept
{
CKComponentContextValue *const v = contextValue(YES);
// Save the old values.
auto const oldObjects = v->_dictionary;
// Copy the new values.
v->_dictionary = [objects mutableCopy];
// Move the old values back to the main storage.
for (id key in oldObjects) {
v->_dictionary[key] = oldObjects[key];
}
return oldObjects;
}
void CKComponentInitialValuesContext::cleanInitialValues(NSMutableDictionary<Class, id> *oldObjects) noexcept
{
CKComponentContextValue *const v = contextValue(NO);
if (!v) {
return;
}
v->_dictionary = oldObjects;
clearContextValueIfEmpty(v);
}