diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index ad46b8c13c..00b45037d2 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -35,6 +35,7 @@ import core, { SearchResult, SortingQuery, Space, + Timestamp, Tx, TxCollectionCUD, TxCreateDoc, @@ -62,7 +63,10 @@ const CACHE_SIZE = 100 type Callback = (result: FindResult) => void +type QueryId = number + interface Query { + id: QueryId // uniq query identifier. _class: Ref> query: DocumentQuery result: Doc[] | Promise @@ -73,6 +77,12 @@ interface Query { requested?: Promise } +interface DocumentRef { + doc: Doc + queries: QueryId[] + lastUsed: Timestamp +} + /** * @public */ @@ -80,6 +90,10 @@ export class LiveQuery implements WithTx, Client { private client: Client private readonly queries: Map>, Query[]> = new Map>, Query[]>() private readonly queue: Query[] = [] + private queryCounter: number = 0 + + // A map of _class to documents. + private readonly documentRefs = new Map, DocumentRef>>() constructor (client: Client) { this.client = client @@ -170,6 +184,36 @@ export class LiveQuery implements WithTx, Client { return q } + findFromDocs( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions + ): FindResult | null { + const classKey = _class + ':' + JSON.stringify(options?.lookup ?? {}) + if (typeof query._id === 'string') { + // One document query + const doc = this.documentRefs.get(classKey)?.get(query._id)?.doc + if (doc !== undefined) { + const q = matchQuery([doc], query, _class, this.getHierarchy()) + if (q.length > 0) { + return toFindResult(this.clone([doc]), 1) as FindResult + } + } + } + if (options?.limit === 1) { + const docs = this.documentRefs.get(classKey) + if (docs !== undefined) { + const _docs = Array.from(docs.values()).map((it) => it.doc) + + const q = matchQuery(_docs, query, _class, this.getHierarchy()) + if (q.length > 0) { + return toFindResult(this.clone([q[0]]), 1) as FindResult + } + } + } + return null + } + async findAll( _class: Ref>, query: DocumentQuery, @@ -186,11 +230,18 @@ export class LiveQuery implements WithTx, Client { modifiedOn: 1 } } + + // Perform one document queries if applicable. + const d = this.findFromDocs(_class, query, options) + if (d !== null) { + return d + } + const q = this.findQuery(_class, query, options) ?? this.createDumpQuery(_class, query, options) if (q.result instanceof Promise) { q.result = await q.result } - if (this.removeFromQueue(q)) { + if (this.removeFromQueue(q, false)) { this.queue.push(q) } return toFindResult(this.clone(q.result), q.total) as FindResult @@ -216,16 +267,22 @@ export class LiveQuery implements WithTx, Client { modifiedOn: 1 } } + if (options === undefined) { options = {} } options.limit = 1 + const d = this.findFromDocs(_class, query, options) + if (d !== null) { + return d[0] + } + const q = this.findQuery(_class, query, options) ?? this.createDumpQuery(_class, query, options) if (q.result instanceof Promise) { q.result = await q.result } - if (this.removeFromQueue(q)) { + if (this.removeFromQueue(q, false)) { this.queue.push(q) } return this.clone(q.result)[0] as WithLookup @@ -244,11 +301,16 @@ export class LiveQuery implements WithTx, Client { } } - private removeFromQueue (q: Query): boolean { + private removeFromQueue (q: Query, update = true): boolean { if (q.callbacks.size === 0) { const queueIndex = this.queue.indexOf(q) if (queueIndex !== -1) { this.queue.splice(queueIndex, 1) + if (update) { + if (!(q.result instanceof Promise)) { + this.updateDocuments(q, q.result, true) + } + } return true } } @@ -284,7 +346,7 @@ export class LiveQuery implements WithTx, Client { ): Query | undefined { const current = this.findQuery(_class, query, options) if (current !== undefined) { - this.removeFromQueue(current) + this.removeFromQueue(current, false) this.pushCallback(current, callback) return current @@ -298,8 +360,10 @@ export class LiveQuery implements WithTx, Client { options?: FindOptions ): Query { const queries = this.queries.get(_class) ?? [] - const result = this.client.findAll(_class, query, options) + const localResult = this.findFromDocs(_class, query, options) + const result = localResult != null ? Promise.resolve(localResult) : this.client.findAll(_class, query, options) const q: Query = { + id: ++this.queryCounter, _class, query, result, @@ -329,7 +393,13 @@ export class LiveQuery implements WithTx, Client { const q = this.queue.shift() if (q === undefined) return const queries = this.queries.get(q._class) - queries?.splice(queries.indexOf(q), 1) + const pos = queries?.indexOf(q) ?? -1 + if (pos >= 0 && queries !== undefined) { + queries.splice(pos, 1) + if (!(q.result instanceof Promise)) { + this.updateDocuments(q, q.result, true) + } + } if (queries?.length === 0) { this.queries.delete(q._class) } @@ -408,7 +478,8 @@ export class LiveQuery implements WithTx, Client { } else { const pos = q.result.findIndex((p) => p._id === _id) if (pos !== -1) { - q.result.splice(pos, 1) + const doc = q.result.splice(pos, 1) + this.updateDocuments(q, doc, true) if (q.options?.total === true) { q.total-- } @@ -418,6 +489,7 @@ export class LiveQuery implements WithTx, Client { const pos = q.result.findIndex((p) => p._id === _id) if (pos !== -1) { q.result[pos] = match + this.updateDocuments(q, [match]) } } return false @@ -432,9 +504,10 @@ export class LiveQuery implements WithTx, Client { ): Promise { const lookup = q.options?.lookup const docIdKey = _id + JSON.stringify(lookup ?? {}) + q._class + const current = docCache.get(docIdKey) ?? - (await this.client.findOne(q._class, { _id, space }, lookup !== undefined ? { lookup } : undefined)) + (await this.client.findOne(q._class, { _id, space }, lookup !== undefined ? { lookup } : undefined)) if (current !== undefined) { docCache.set(docIdKey, current) } else { @@ -457,12 +530,14 @@ export class LiveQuery implements WithTx, Client { const pos = q.result.findIndex((p) => p._id === _id) if (current !== undefined && this.match(q, current)) { q.result[pos] = current + this.updateDocuments(q, [current]) } else { if (q.options?.limit === q.result.length) { await this.refresh(q) return true } else if (pos !== -1) { - q.result.splice(pos, 1) + const doc = q.result.splice(pos, 1) + this.updateDocuments(q, doc, true) if (q.options?.total === true) { q.total-- } @@ -492,13 +567,15 @@ export class LiveQuery implements WithTx, Client { await this.refresh(q) return true } else { - q.result.splice(pos, 1) + const doc = q.result.splice(pos, 1) + this.updateDocuments(q, doc, true) if (q.options?.total === true) { q.total-- } } } else { q.result[pos] = updatedDoc + this.updateDocuments(q, [updatedDoc]) } return false } @@ -654,7 +731,7 @@ export class LiveQuery implements WithTx, Client { q.result = await q.result } let needCallback = false - needCallback = await this.proccesLookupUpdateDoc(q.result, lookup, tx) + needCallback = await this.processLookupUpdateDoc(q.result, lookup, tx) if (needCallback) { if (q.options?.sort !== undefined) { @@ -664,7 +741,7 @@ export class LiveQuery implements WithTx, Client { } } - private async proccesLookupUpdateDoc ( + private async processLookupUpdateDoc ( docs: Doc[], lookup: Lookup, tx: TxUpdateDoc | TxMixin @@ -691,9 +768,11 @@ export class LiveQuery implements WithTx, Client { index = -1 needCallback = true } else if (index === -1 && reverseLookupValue === obj._id) { - const doc = await this.client.findOne(tx.objectClass, { _id: tx.objectId }) - value.push(doc) - index = value.length - 1 + const doc = await this.findOne(tx.objectClass, { _id: tx.objectId }) + if (doc !== undefined) { + value.push(doc) + index = value.length - 1 + } needCallback = true } } @@ -813,6 +892,7 @@ export class LiveQuery implements WithTx, Client { const pos = q.result.findIndex((el) => el._id === doc._id) if (pos !== -1) { q.result[pos] = doc + this.updateDocuments(q, [doc]) } else { q.result.push(doc) if (q.options?.total === true) { @@ -840,8 +920,7 @@ export class LiveQuery implements WithTx, Client { const tkey = checkMixinKey(key, _class, this.client.getHierarchy()) if (Array.isArray(value)) { const [_class, nested] = value - const objects = await this.client.findAll(_class, { _id: getObjectValue(tkey, doc) }) - ;(result as any)[key] = objects[0] + ;(result as any)[key] = await this.findOne(_class, { _id: getObjectValue(tkey, doc) }) const nestedResult = {} const parent = (result as any)[key] if (parent !== undefined) { @@ -851,8 +930,7 @@ export class LiveQuery implements WithTx, Client { }) } } else { - const objects = await this.client.findAll(value, { _id: getObjectValue(tkey, doc) }) - ;(result as any)[key] = objects[0] + ;(result as any)[key] = await this.findOne(value, { _id: getObjectValue(tkey, doc) }) } } } @@ -874,8 +952,7 @@ export class LiveQuery implements WithTx, Client { } else { _class = value } - const objects = await this.client.findAll(_class, { [attr]: doc._id }) - ;(result as any)[key] = objects + ;(result as any)[key] = await this.findAll(_class, { [attr]: doc._id }) } } @@ -948,12 +1025,44 @@ export class LiveQuery implements WithTx, Client { if (q.result instanceof Promise) { q.result = await q.result } + + this.updateDocuments(q, q.result) + const result = q.result Array.from(q.callbacks.values()).forEach((callback) => { callback(toFindResult(this.clone(result), q.total)) }) } + private updateDocuments (q: Query, docs: Doc[], clean: boolean = false): void { + if (q.options?.projection !== undefined) { + return + } + for (const d of docs) { + const classKey = Hierarchy.mixinOrClass(d) + ':' + JSON.stringify(q.options?.lookup ?? {}) + let docMap = this.documentRefs.get(classKey) + if (docMap === undefined) { + if (clean) { + continue + } + docMap = new Map() + this.documentRefs.set(classKey, docMap) + } + const queries = (docMap.get(d._id)?.queries ?? []).filter((it) => it !== q.id) + if (!clean) { + queries.push(q.id) + } + if (queries.length === 0) { + docMap.delete(d._id) + } else { + const q = docMap.get(d._id) + if ((q?.lastUsed ?? 0) < d.modifiedOn) { + docMap.set(d._id, { ...(q ?? {}), doc: d, queries, lastUsed: d.modifiedOn }) + } + } + } + } + private async handleDocAddLookup (q: Query, doc: Doc): Promise { if (q.options?.lookup === undefined) return const lookup = q.options.lookup @@ -1030,7 +1139,9 @@ export class LiveQuery implements WithTx, Client { } const index = q.result.findIndex((p) => p._id === tx.objectId && h.isDerived(p._class, tx.objectClass)) if (index > -1) { - q.result.splice(index, 1) + const doc = q.result.splice(index, 1) + this.updateDocuments(q, doc, true) + if (q.options?.total === true) { q.total-- } @@ -1186,14 +1297,11 @@ export class LiveQuery implements WithTx, Client { for (const v of this.queries.values()) { for (const q of v) { if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { - console.log('Query update call', q) try { await this.refresh(q, true) } catch (err) { console.error(err) } - } else if (q.query.$search !== undefined) { - console.log('Query update mismatch class', q._class, indexingParam._class) } } } @@ -1261,13 +1369,13 @@ export class LiveQuery implements WithTx, Client { const lookupClass = getLookupClass(lookup) const nestedLookup = getNestedLookup(lookup) if (Array.isArray(ops[key])) { - ;(updatedDoc.$lookup as any)[key] = await this.client.findAll( + ;(updatedDoc.$lookup as any)[key] = await this.findAll( lookupClass, { _id: { $in: ops[key] } }, { lookup: nestedLookup } ) } else { - ;(updatedDoc.$lookup as any)[key] = await this.client.findOne( + ;(updatedDoc.$lookup as any)[key] = await this.findOne( lookupClass, { _id: ops[key] }, { lookup: nestedLookup } @@ -1289,14 +1397,17 @@ export class LiveQuery implements WithTx, Client { pp[pkey] = [] } if (Array.isArray(pops[pkey])) { - const pushData = await this.client.findAll( + const pushData = await this.findAll( lookupClass, { _id: { $in: pops[pkey] } }, { lookup: nestedLookup } ) pp[pkey].push(...pushData) } else { - pp[pkey].push(await this.client.findOne(lookupClass, { _id: pops[pkey] }, { lookup: nestedLookup })) + const d = await this.findOne(lookupClass, { _id: pops[pkey] }, { lookup: nestedLookup }) + if (d !== undefined) { + pp[pkey].push() + } } } }