diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 38f1eb16..b84b2eda 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -24,6 +24,8 @@ class AnkiNoteBuilder { constructor() { this._markerPattern = AnkiUtil.cloneFieldMarkerPattern(true); this._templateRenderer = new TemplateRendererProxy(); + this._batchedRequests = []; + this._batchedRequestsQueued = false; } async createNote({ @@ -113,7 +115,7 @@ class AnkiNoteBuilder { async _formatField(field, commonData, template, errors=null) { return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => { try { - return await this._renderTemplate(template, marker, commonData); + return await this._renderTemplateBatched(template, commonData, marker); } catch (e) { if (Array.isArray(errors)) { const error = new Error(`Template render error for {${marker}}`); @@ -143,4 +145,88 @@ class AnkiNoteBuilder { async _renderTemplate(template, marker, commonData) { return await this._templateRenderer.render(template, {marker, commonData}, 'ankiNote'); } + + _getBatchedTemplateGroup(template) { + for (const item of this._batchedRequests) { + if (item.template === template) { + return item; + } + } + + const result = {template, commonDataRequestsMap: new Map()}; + this._batchedRequests.push(result); + return result; + } + + _renderTemplateBatched(template, commonData, marker) { + const {promise, resolve, reject} = deferPromise(); + const {commonDataRequestsMap} = this._getBatchedTemplateGroup(template); + let requests = commonDataRequestsMap.get(commonData); + if (typeof requests === 'undefined') { + requests = []; + commonDataRequestsMap.set(commonData, requests); + } + requests.push({resolve, reject, marker}); + this._runBatchedRequestsDelayed(); + return promise; + } + + _runBatchedRequestsDelayed() { + if (this._batchedRequestsQueued) { return; } + this._batchedRequestsQueued = true; + Promise.resolve().then(() => { + this._batchedRequestsQueued = false; + this._runBatchedRequests(); + }); + } + + _runBatchedRequests() { + if (this._batchedRequests.length === 0) { return; } + + const allRequests = []; + const items = []; + for (const {template, commonDataRequestsMap} of this._batchedRequests) { + const templateItems = []; + for (const [commonData, requests] of commonDataRequestsMap.entries()) { + const datas = []; + for (const {marker} of requests) { + datas.push(marker); + } + allRequests.push(...requests); + templateItems.push({type: 'ankiNote', commonData, datas}); + } + items.push({template, templateItems}); + } + + this._batchedRequests.length = 0; + + this._resolveBatchedRequests(items, allRequests); + } + + async _resolveBatchedRequests(items, requests) { + let responses; + try { + responses = await this._templateRenderer.renderMulti(items); + } catch (e) { + for (const {reject} of requests) { + reject(e); + } + return; + } + + for (let i = 0, ii = requests.length; i < ii; ++i) { + const request = requests[i]; + try { + const response = responses[i]; + const {error} = response; + if (typeof error !== 'undefined') { + throw deserializeError(error); + } else { + request.resolve(response.result); + } + } catch (e) { + request.reject(e); + } + } + } } diff --git a/ext/js/templates/template-renderer-frame-api.js b/ext/js/templates/template-renderer-frame-api.js index 104e357b..dd6be517 100644 --- a/ext/js/templates/template-renderer-frame-api.js +++ b/ext/js/templates/template-renderer-frame-api.js @@ -20,6 +20,7 @@ class TemplateRendererFrameApi { this._templateRenderer = templateRenderer; this._windowMessageHandlers = new Map([ ['render', {async: false, handler: this._onRender.bind(this)}], + ['renderMulti', {async: false, handler: this._onRenderMulti.bind(this)}], ['getModifiedData', {async: false, handler: this._onGetModifiedData.bind(this)}] ]); } @@ -58,6 +59,10 @@ class TemplateRendererFrameApi { return this._templateRenderer.render(template, data, type); } + _onRenderMulti({items}) { + return this._serializeMulti(this._templateRenderer.renderMulti(items)); + } + _onGetModifiedData({data, type}) { const result = this._templateRenderer.getModifiedData(data, type); return this._clone(result); @@ -82,6 +87,17 @@ class TemplateRendererFrameApi { }; } + _serializeMulti(array) { + for (let i = 0, ii = array.length; i < ii; ++i) { + const value = array[i]; + const {error} = value; + if (typeof error !== 'undefined') { + value.error = this._serializeError(error); + } + } + return array; + } + _clone(value) { return JSON.parse(JSON.stringify(value)); } diff --git a/ext/js/templates/template-renderer-frame-main.js b/ext/js/templates/template-renderer-frame-main.js index 40ecf308..3d61295d 100644 --- a/ext/js/templates/template-renderer-frame-main.js +++ b/ext/js/templates/template-renderer-frame-main.js @@ -27,7 +27,8 @@ const templateRenderer = new TemplateRenderer(japaneseUtil); const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil); templateRenderer.registerDataType('ankiNote', { - modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData) + modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData), + composeData: (marker, commonData) => ({marker, commonData}) }); const templateRendererFrameApi = new TemplateRendererFrameApi(templateRenderer); templateRendererFrameApi.prepare(); diff --git a/ext/js/templates/template-renderer-proxy.js b/ext/js/templates/template-renderer-proxy.js index aba45e6c..f35239cb 100644 --- a/ext/js/templates/template-renderer-proxy.js +++ b/ext/js/templates/template-renderer-proxy.js @@ -30,6 +30,11 @@ class TemplateRendererProxy { return await this._invoke('render', {template, data, type}); } + async renderMulti(items) { + await this._prepareFrame(); + return await this._invoke('renderMulti', {items}); + } + async getModifiedData(data, type) { await this._prepareFrame(); return await this._invoke('getModifiedData', {data, type}); diff --git a/ext/js/templates/template-renderer.js b/ext/js/templates/template-renderer.js index 5441528c..ed9cc41c 100644 --- a/ext/js/templates/template-renderer.js +++ b/ext/js/templates/template-renderer.js @@ -29,18 +29,39 @@ class TemplateRenderer { this._dataTypes = new Map(); } - registerDataType(name, {modifier=null}) { - this._dataTypes.set(name, {modifier}); + registerDataType(name, {modifier=null, composeData=null}) { + this._dataTypes.set(name, {modifier, composeData}); } render(template, data, type) { const instance = this._getTemplateInstance(template); - data = this._getModifiedData(data, type); + data = this._getModifiedData(data, void 0, type); return this._renderTemplate(instance, data); } + renderMulti(items) { + const results = []; + for (const {template, templateItems} of items) { + const instance = this._getTemplateInstance(template); + for (const {type, commonData, datas} of templateItems) { + for (let data of datas) { + let result; + try { + data = this._getModifiedData(data, commonData, type); + result = this._renderTemplate(instance, data); + result = {result}; + } catch (error) { + result = {error}; + } + results.push(result); + } + } + } + return results; + } + getModifiedData(data, type) { - return this._getModifiedData(data, type); + return this._getModifiedData(data, void 0, type); } // Private @@ -70,10 +91,16 @@ class TemplateRenderer { } } - _getModifiedData(data, type) { + _getModifiedData(data, commonData, type) { if (typeof type === 'string') { const typeInfo = this._dataTypes.get(type); if (typeof typeInfo !== 'undefined') { + if (typeof commonData !== 'undefined') { + const {composeData} = typeInfo; + if (typeof composeData === 'function') { + data = composeData(data, commonData); + } + } const {modifier} = typeInfo; if (typeof modifier === 'function') { data = modifier(data); diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js index b1fffbcd..152a8739 100644 --- a/test/test-anki-note-builder.js +++ b/test/test-anki-note-builder.js @@ -55,12 +55,28 @@ async function createVM() { const japaneseUtil = new JapaneseUtil(null); this._templateRenderer = new TemplateRenderer(japaneseUtil); this._templateRenderer.registerDataType('ankiNote', { - modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData) + modifier: ({marker, commonData}) => ankiNoteDataCreator.create(marker, commonData), + composeData: (marker, commonData) => ({marker, commonData}) }); } async render(template, data, type) { - return await this._templateRenderer.render(template, data, type); + return this._templateRenderer.render(template, data, type); + } + + async renderMulti(items) { + return this._serializeMulti(this._templateRenderer.renderMulti(items)); + } + + _serializeMulti(array) { + for (let i = 0, ii = array.length; i < ii; ++i) { + const value = array[i]; + const {error} = value; + if (typeof error !== 'undefined') { + value.error = this._serializeError(error); + } + } + return array; } } vm.set({TemplateRendererProxy});