Add experimental support for caching the creation of HBS template functions

This commit is contained in:
Andrew Kingston 2022-01-29 18:53:21 +00:00
parent bfb2d637fc
commit e7b02aec04
3 changed files with 88 additions and 46 deletions

View File

@ -33,9 +33,12 @@
// need to be enriched at this level. // need to be enriched at this level.
// Nested settings are the un-enriched block settings that are to be passed on // Nested settings are the un-enriched block settings that are to be passed on
// and enriched at a deeper level. // and enriched at a deeper level.
let componentSettings let dynamicSettings
let staticSettings
let nestedSettings let nestedSettings
// The context keys that
// The enriched component settings // The enriched component settings
let enrichedSettings let enrichedSettings
@ -108,15 +111,23 @@
$: rawSettings = getRawSettings(instance) $: rawSettings = getRawSettings(instance)
$: instanceKey = Helpers.hashString(JSON.stringify(rawSettings)) $: instanceKey = Helpers.hashString(JSON.stringify(rawSettings))
// Update and enrich component settings // Parse and split component settings into categories
$: updateSettings(rawSettings, instanceKey, settingsDefinition, $context) $: updateSettings(rawSettings, instanceKey, settingsDefinition)
// Enrich component settings
$: enrichComponentSettings(dynamicSettings, $context)
// Evaluate conditional UI settings and store any component setting changes // Evaluate conditional UI settings and store any component setting changes
// which need to be made // which need to be made
$: evaluateConditions(enrichedSettings?._conditions) $: evaluateConditions(enrichedSettings?._conditions)
// Build up the final settings object to be passed to the component // Build up the final settings object to be passed to the component
$: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings) $: cacheSettings(
staticSettings,
enrichedSettings,
nestedSettings,
conditionalSettings
)
// Update component context // Update component context
$: componentStore.set({ $: componentStore.set({
@ -185,48 +196,55 @@
} }
// Updates and enriches component settings when raw settings change // Updates and enriches component settings when raw settings change
const updateSettings = (settings, key, settingsDefinition, context) => { const updateSettings = (settings, key, settingsDefinition) => {
const instanceChanged = key !== lastInstanceKey const instanceChanged = key !== lastInstanceKey
if (!instanceChanged) {
// Derive component and nested settings if the instance changed return
if (instanceChanged) { } else {
splitRawSettings(settings, settingsDefinition)
}
// Enrich component settings
enrichComponentSettings(componentSettings, context, instanceChanged)
// Update instance key
if (instanceChanged) {
lastInstanceKey = key lastInstanceKey = key
} }
}
// Splits the raw settings into those destined for the component itself // Derive static, dynamic and nested settings if the instance changed
// and nexted settings for child components inside blocks let newStaticSettings = { ...settings }
const splitRawSettings = (rawSettings, settingsDefinition) => { let newDynamicSettings = { ...settings }
let newComponentSettings = { ...rawSettings } let newNestedSettings = { ...settings }
let newNestedSettings = { ...rawSettings }
settingsDefinition?.forEach(setting => { settingsDefinition?.forEach(setting => {
if (setting.nested) { if (setting.nested) {
delete newComponentSettings[setting.key] delete newStaticSettings[setting.key]
delete newDynamicSettings[setting.key]
} else { } else {
delete newNestedSettings[setting.key] delete newNestedSettings[setting.key]
// This is a non-nested setting, but we need to find out if it is
// static or dynamic
const value = rawSettings[setting.key]
if (value == null) {
delete newDynamicSettings[setting.key]
} else if (typeof value === "string" && value.includes("{{")) {
delete newStaticSettings[setting.key]
} else if (typeof value === "object") {
const stringified = JSON.stringify(value)
if (stringified.includes("{{")) {
delete newStaticSettings[setting.key]
} else {
delete newDynamicSettings[setting.key]
}
} else {
delete newDynamicSettings[setting.key]
}
} }
}) })
componentSettings = newComponentSettings
staticSettings = newStaticSettings
dynamicSettings = newDynamicSettings
nestedSettings = newNestedSettings nestedSettings = newNestedSettings
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const enrichComponentSettings = (rawSettings, context, instanceChanged) => { const enrichComponentSettings = (settings, context) => {
const contextChanged = context.key !== lastContextKey const contextChanged = context.key !== lastContextKey
// Skip enrichment if the context and instance are unchanged
if (!contextChanged) { if (!contextChanged) {
if (!instanceChanged) {
return return
}
} else { } else {
lastContextKey = context.key lastContextKey = context.key
} }
@ -236,7 +254,7 @@
const enrichmentTime = latestUpdateTime const enrichmentTime = latestUpdateTime
// Enrich settings with context // Enrich settings with context
const newEnrichedSettings = enrichProps(rawSettings, context) const newEnrichedSettings = enrichProps(settings, context)
// Abandon this update if a newer update has started // Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) { if (enrichmentTime !== latestUpdateTime) {
@ -271,8 +289,13 @@
// Combines and caches all settings which will be passed to the component // Combines and caches all settings which will be passed to the component
// instance. Settings are aggressively memoized to avoid triggering svelte // instance. Settings are aggressively memoized to avoid triggering svelte
// reactive statements as much as possible. // reactive statements as much as possible.
const cacheSettings = (enriched, nested, conditional) => { const cacheSettings = (staticSettings, enriched, nested, conditional) => {
const allSettings = { ...enriched, ...nested, ...conditional } const allSettings = {
...staticSettings,
...enriched,
...nested,
...conditional,
}
if (!cachedSettings) { if (!cachedSettings) {
cachedSettings = { ...allSettings } cachedSettings = { ...allSettings }
initialSettings = cachedSettings initialSettings = cachedSettings

View File

@ -24,5 +24,5 @@ export const enrichDataBinding = async (input, context) => {
* Props are deeply cloned so that no mutation is done to the source object. * Props are deeply cloned so that no mutation is done to the source object.
*/ */
export const enrichDataBindings = (props, context) => { export const enrichDataBindings = (props, context) => {
return processObjectSync(cloneDeep(props), context) return processObjectSync(cloneDeep(props), context, { cache: true })
} }

View File

@ -7,10 +7,10 @@ const manifest = require("../manifest.json")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
registerAll(hbsInstance) registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create() const hbsInstanceNoHelpers = handlebars.create()
const defaultOpts = { noHelpers: false } const defaultOpts = { noHelpers: false, cacheTemplates: false }
/** /**
* utility function to check if the object is valid * Utility function to check if the object is valid.
*/ */
function testObject(object) { function testObject(object) {
// JSON stringify will fail if there are any cycles, stops infinite recursion // JSON stringify will fail if there are any cycles, stops infinite recursion
@ -21,6 +21,32 @@ function testObject(object) {
} }
} }
/**
* Creates a HBS template function for a given string, and optionally caches it.
*/
let templateCache = {}
function createTemplate(string, noHelpers, cache) {
// Finalising adds a helper, can't do this with no helpers
const shouldFinalise = !noHelpers
const key = `${string}${shouldFinalise}`
// Reuse the cached template is possible
if (cache && templateCache[key]) {
return templateCache[key]
}
string = processors.preprocess(string, shouldFinalise)
// This does not throw an error when template can't be fulfilled,
// have to try correct beforehand
const instance = noHelpers ? hbsInstanceNoHelpers : hbsInstance
const template = instance.compile(string, {
strict: false,
})
templateCache[key] = template
return template
}
/** /**
* Given an input object this will recurse through all props to try and update any handlebars statements within. * Given an input object this will recurse through all props to try and update any handlebars statements within.
* @param {object|array} object The input structure which is to be recursed, it is important to note that * @param {object|array} object The input structure which is to be recursed, it is important to note that
@ -104,14 +130,7 @@ module.exports.processStringSync = (string, context, opts) => {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
try { try {
// finalising adds a helper, can't do this with no helpers const template = createTemplate(string, opts.noHelpers, opts.cacheTemplates)
const shouldFinalise = !opts.noHelpers
string = processors.preprocess(string, shouldFinalise)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const template = instance.compile(string, {
strict: false,
})
const now = Math.floor(Date.now() / 1000) * 1000 const now = Math.floor(Date.now() / 1000) * 1000
return processors.postprocess( return processors.postprocess(
template({ template({
@ -154,8 +173,8 @@ module.exports.isValid = (string, opts) => {
// don't really need a real context to check if its valid // don't really need a real context to check if its valid
const context = {} const context = {}
try { try {
const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance const template = createTemplate(string, opts.noHelpers, opts.cache)
instance.compile(processors.preprocess(string, false))(context) template(context)
return true return true
} catch (err) { } catch (err) {
const msg = err && err.message ? err.message : err const msg = err && err.message ? err.message : err