From fd673a4ea0068adb97383eb7e64248d374f537e9 Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Wed, 16 Sep 2020 21:19:51 +0200 Subject: [PATCH] Implement asyncSearch (WIP) --- src/logic/operations.js | 2 - src/logic/search.js | 79 ++++++++++++++++++++++++++++++ src/logic/search.test.js | 101 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/logic/search.test.js diff --git a/src/logic/operations.js b/src/logic/operations.js index 6d81f58..bbc0931 100644 --- a/src/logic/operations.js +++ b/src/logic/operations.js @@ -284,8 +284,6 @@ export function createNewValue (doc, selection, type) { : '' }) - console.log('structure', jsonExample, structure) - return structure } else { // no example structure diff --git a/src/logic/search.js b/src/logic/search.js index a654c9b..6452ce4 100644 --- a/src/logic/search.js +++ b/src/logic/search.js @@ -130,6 +130,85 @@ function searchRecursive (key, doc, searchText) { return results } +async function tick () { + return new Promise(setTimeout) +} + +// TODO: comment +export function searchAsync (searchText, doc, { onPartlyResults, onDone }) { + const yieldAfterItemCount = 10000 // TODO: what is a good value? + const search = searchGenerator(searchText, doc, yieldAfterItemCount) + + // TODO: implement pause after having found x results (like 999) + + let cancelled = false + const results = [] + + async function executeSearch () { + let next + do { + next = search.next() + if (next.value) { + results.push(next.value) // TODO: make this immutable? + onPartlyResults(results) + } + await tick() // TODO: be able to wait longer than just one tick? So the UI stays fully responsive? + } while (!cancelled && !next.done) + + if (next.done) { + onDone(results) + } // else: cancelled + } + + // start searching on the next tick + setTimeout(executeSearch) + + return { + cancel: () => { + cancelled = true + } + } +} + +// TODO: comment +export function * searchGenerator (searchText, doc, yieldAfterItemCount = undefined) { + let count = 0 + + function * incrementCounter () { + count++ + if (typeof yieldAfterItemCount === 'number' && count % yieldAfterItemCount === 0) { + // pause every x items + yield null + } + } + + function * searchRecursiveAsync (searchText, doc, path) { + const type = valueType(doc) + + if (type === 'array') { + for (let i = 0; i < doc.length; i++) { + yield * searchRecursiveAsync(searchText, doc[i], path.concat([i])) + } + } else if (type === 'object') { + for (const prop of Object.keys(doc)) { + if (typeof prop === 'string' && containsCaseInsensitive(prop, searchText)) { + yield path.concat([prop, STATE_SEARCH_PROPERTY]) + } + yield * incrementCounter() + + yield * searchRecursiveAsync(searchText, doc[prop], path.concat([prop])) + } + } else { // type is a value + if (containsCaseInsensitive(doc, searchText)) { + yield path.concat([STATE_SEARCH_VALUE]) + } + yield * incrementCounter() + } + } + + return yield * searchRecursiveAsync(searchText, doc, []) +} + function flattenSearch (searchResult) { const resultArray = [] diff --git a/src/logic/search.test.js b/src/logic/search.test.js new file mode 100644 index 0000000..e7d087c --- /dev/null +++ b/src/logic/search.test.js @@ -0,0 +1,101 @@ +import assert from 'assert' +import { times } from 'lodash-es' +import { searchAsync, searchGenerator } from './search.js' +import { STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE } from '../constants.js' + +describe('search', () => { + it('should search with generator', () => { + const doc = { + b: { c: 'a' }, + a: [ + { a: 'b', c: 'a' }, + 'e', + 'a' + ] + } + + const search = searchGenerator('a', doc) + + assert.deepStrictEqual(search.next(), { done: false, value: ['b', 'c', STATE_SEARCH_VALUE] }) + assert.deepStrictEqual(search.next(), { done: false, value: ['a', STATE_SEARCH_PROPERTY] }) + assert.deepStrictEqual(search.next(), { done: false, value: ['a', 0, 'a', STATE_SEARCH_PROPERTY] }) + assert.deepStrictEqual(search.next(), { done: false, value: ['a', 0, 'c', STATE_SEARCH_VALUE] }) + assert.deepStrictEqual(search.next(), { done: false, value: ['a', 2, STATE_SEARCH_VALUE] }) + assert.deepStrictEqual(search.next(), { done: true, value: undefined }) + }) + + it('should yield every x items during search', () => { + const doc = times(30, index => String(index)) + + const search = searchGenerator('4', doc, 10) + assert.deepStrictEqual(search.next(), { done: false, value: [4, STATE_SEARCH_VALUE] }) + assert.deepStrictEqual(search.next(), { done: false, value: null }) // at 10 + assert.deepStrictEqual(search.next(), { done: false, value: [14, STATE_SEARCH_VALUE] }) + assert.deepStrictEqual(search.next(), { done: false, value: null }) // at 20 + assert.deepStrictEqual(search.next(), { done: false, value: [24, STATE_SEARCH_VALUE] }) + assert.deepStrictEqual(search.next(), { done: false, value: null }) // at 30 + assert.deepStrictEqual(search.next(), { done: true, value: undefined }) + }) + + it('should search async', (done) => { + const doc = times(30, index => String(index)) + + const callbacks = [] + + function onPartlyResults (results) { + callbacks.push(results.slice(0)) + } + + function onDone (results) { + assert.deepStrictEqual(results, [ + [4, STATE_SEARCH_VALUE], + [14, STATE_SEARCH_VALUE], + [24, STATE_SEARCH_VALUE] + ]) + + assert.deepStrictEqual(callbacks, [ + results.slice(0, 1), + results.slice(0, 2), + results.slice(0, 3) + ]) + + done() + } + + searchAsync('4', doc, { onPartlyResults, onDone }) + + // should not have results right after creation, but only on the first next tick + assert.deepStrictEqual(callbacks, []) + }) + + it('should cancel async search', (done) => { + const doc = times(30, index => String(index)) + + const callbacks = [] + + function onPartlyResults (results) { + callbacks.push(results.slice(0)) + } + + function onDone () { + throw new Error('onDone should not be invoked') + } + + const { cancel } = searchAsync('4', doc, { onPartlyResults, onDone }) + + // should not have results right after creation, but only on the first next tick + assert.deepStrictEqual(callbacks, []) + + setTimeout(() => { + cancel() + + assert.deepStrictEqual(callbacks, [ + [ + [4, STATE_SEARCH_VALUE] + ] + ]) + + done() + }) + }) +})