From f7067e2d032362ed5b6a9fc1222661e2f5d94d10 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Fri, 2 May 2025 15:32:16 -0600 Subject: [PATCH 01/20] Enable only unicode / utf8 string tests --- packages/firestore/test/integration/api/database.test.ts | 2 +- packages/firestore/test/unit/util/misc.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 9675e02efeb..265f6d210be 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -2433,7 +2433,7 @@ apiDescribe('Database', persistence => { }); }); - describe('Sort unicode strings', () => { + describe.only('Sort unicode strings', () => { const expectedDocs = [ 'b', 'a', diff --git a/packages/firestore/test/unit/util/misc.test.ts b/packages/firestore/test/unit/util/misc.test.ts index 0829259a8e8..45dc083272e 100644 --- a/packages/firestore/test/unit/util/misc.test.ts +++ b/packages/firestore/test/unit/util/misc.test.ts @@ -54,7 +54,7 @@ describe('FieldMask', () => { }); }); -describe('CompareUtf8Strings', () => { +describe.only('CompareUtf8Strings', () => { it('compareUtf8Strings should return correct results', () => { const errors = []; const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); From 2c219e55344cbbf92f1606f564b91b0f0d5808c4 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 5 May 2025 09:40:34 -0600 Subject: [PATCH 02/20] Attempting to mitigate failures caused by browser disconnect timout --- config/karma.base.js | 1 + packages/firestore/test/integration/api/database.test.ts | 2 +- packages/firestore/test/unit/util/misc.test.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/karma.base.js b/config/karma.base.js index c49b1246ed6..9d92053115f 100644 --- a/config/karma.base.js +++ b/config/karma.base.js @@ -53,6 +53,7 @@ const config = { // Doing 65 seconds to allow for the 20 second firestore tests browserNoActivityTimeout: 65000, + browserDisconnectTimeout: 65000, // Preprocess matching files before serving them to the browser. // Available preprocessors: diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 265f6d210be..9675e02efeb 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -2433,7 +2433,7 @@ apiDescribe('Database', persistence => { }); }); - describe.only('Sort unicode strings', () => { + describe('Sort unicode strings', () => { const expectedDocs = [ 'b', 'a', diff --git a/packages/firestore/test/unit/util/misc.test.ts b/packages/firestore/test/unit/util/misc.test.ts index 45dc083272e..0829259a8e8 100644 --- a/packages/firestore/test/unit/util/misc.test.ts +++ b/packages/firestore/test/unit/util/misc.test.ts @@ -54,7 +54,7 @@ describe('FieldMask', () => { }); }); -describe.only('CompareUtf8Strings', () => { +describe('CompareUtf8Strings', () => { it('compareUtf8Strings should return correct results', () => { const errors = []; const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); From d4b667eba01e53479e9529c62829c5c7a892f6e1 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 5 May 2025 11:46:42 -0600 Subject: [PATCH 03/20] Skip compareUtf8Strings should return correct results --- packages/firestore/test/unit/util/misc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/test/unit/util/misc.test.ts b/packages/firestore/test/unit/util/misc.test.ts index 0829259a8e8..982b8498611 100644 --- a/packages/firestore/test/unit/util/misc.test.ts +++ b/packages/firestore/test/unit/util/misc.test.ts @@ -55,7 +55,7 @@ describe('FieldMask', () => { }); describe('CompareUtf8Strings', () => { - it('compareUtf8Strings should return correct results', () => { + it.skip('compareUtf8Strings should return correct results', () => { const errors = []; const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); let passCount = 0; From a4b0f8a8c5f50506becf1fee9c2a6a923e48a9f3 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 5 May 2025 12:26:14 -0600 Subject: [PATCH 04/20] Skip sort unicode strings --- packages/firestore/test/integration/api/database.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 9675e02efeb..4661745985b 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -2433,7 +2433,7 @@ apiDescribe('Database', persistence => { }); }); - describe('Sort unicode strings', () => { + describe.skip('Sort unicode strings', () => { const expectedDocs = [ 'b', 'a', From f5e632ffdfa38a78e7ef75b03c03549534df0ac9 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 5 May 2025 12:53:52 -0600 Subject: [PATCH 05/20] browserDisconnectTimeout back to default --- config/karma.base.js | 1 - 1 file changed, 1 deletion(-) diff --git a/config/karma.base.js b/config/karma.base.js index 9d92053115f..c49b1246ed6 100644 --- a/config/karma.base.js +++ b/config/karma.base.js @@ -53,7 +53,6 @@ const config = { // Doing 65 seconds to allow for the 20 second firestore tests browserNoActivityTimeout: 65000, - browserDisconnectTimeout: 65000, // Preprocess matching files before serving them to the browser. // Available preprocessors: From 944feac7532079315f773889cc97968c26ff65ed Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 5 May 2025 15:58:43 -0600 Subject: [PATCH 06/20] reenable compareUtf8strings unit test" --- packages/firestore/test/unit/util/misc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/test/unit/util/misc.test.ts b/packages/firestore/test/unit/util/misc.test.ts index 982b8498611..0829259a8e8 100644 --- a/packages/firestore/test/unit/util/misc.test.ts +++ b/packages/firestore/test/unit/util/misc.test.ts @@ -55,7 +55,7 @@ describe('FieldMask', () => { }); describe('CompareUtf8Strings', () => { - it.skip('compareUtf8Strings should return correct results', () => { + it('compareUtf8Strings should return correct results', () => { const errors = []; const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); let passCount = 0; From baeb68e70a5d741f4866263cbef73b520873443c Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 5 May 2025 16:21:44 -0600 Subject: [PATCH 07/20] another test --- packages/firestore/test/unit/util/misc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/test/unit/util/misc.test.ts b/packages/firestore/test/unit/util/misc.test.ts index 0829259a8e8..d4dd1cb9326 100644 --- a/packages/firestore/test/unit/util/misc.test.ts +++ b/packages/firestore/test/unit/util/misc.test.ts @@ -55,7 +55,7 @@ describe('FieldMask', () => { }); describe('CompareUtf8Strings', () => { - it('compareUtf8Strings should return correct results', () => { + it.only('compareUtf8Strings should return correct results', () => { const errors = []; const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); let passCount = 0; From 538f47091a16da4401b04829a07621ed1863cabb Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 15:36:56 -0600 Subject: [PATCH 08/20] Reenable all tests and increase browserDisconnectTimeout to 65 seconds --- config/karma.base.js | 1 + packages/firestore/test/unit/util/misc.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/karma.base.js b/config/karma.base.js index c49b1246ed6..9d92053115f 100644 --- a/config/karma.base.js +++ b/config/karma.base.js @@ -53,6 +53,7 @@ const config = { // Doing 65 seconds to allow for the 20 second firestore tests browserNoActivityTimeout: 65000, + browserDisconnectTimeout: 65000, // Preprocess matching files before serving them to the browser. // Available preprocessors: diff --git a/packages/firestore/test/unit/util/misc.test.ts b/packages/firestore/test/unit/util/misc.test.ts index d4dd1cb9326..0829259a8e8 100644 --- a/packages/firestore/test/unit/util/misc.test.ts +++ b/packages/firestore/test/unit/util/misc.test.ts @@ -55,7 +55,7 @@ describe('FieldMask', () => { }); describe('CompareUtf8Strings', () => { - it.only('compareUtf8Strings should return correct results', () => { + it('compareUtf8Strings should return correct results', () => { const errors = []; const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); let passCount = 0; From aeec17a1b4306770f4da58b2c202b6318ab594ae Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 15:39:19 -0600 Subject: [PATCH 09/20] Fix IndexDbIndexManager encoding of Uint8Array in Safari --- common/api-review/util.api.md | 5 + packages/firestore/src/index/index_entry.ts | 85 +++++-- .../src/local/indexeddb_index_manager.ts | 223 +++++++++--------- .../firestore/src/local/indexeddb_schema.ts | 6 +- .../src/local/indexeddb_sentinels.ts | 6 +- packages/util/src/environment.ts | 10 + 6 files changed, 205 insertions(+), 130 deletions(-) diff --git a/common/api-review/util.api.md b/common/api-review/util.api.md index 0f8fc13cd3a..427cb5e174a 100644 --- a/common/api-review/util.api.md +++ b/common/api-review/util.api.md @@ -317,6 +317,11 @@ export function isReactNative(): boolean; // @public export function isSafari(): boolean; +// Warning: (ae-missing-release-tag) "isSafariOrWebkit" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function isSafariOrWebkit(): boolean; + // Warning: (ae-missing-release-tag) "issuedAtTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/firestore/src/index/index_entry.ts b/packages/firestore/src/index/index_entry.ts index ee80276f325..e3d9477077f 100644 --- a/packages/firestore/src/index/index_entry.ts +++ b/packages/firestore/src/index/index_entry.ts @@ -15,15 +15,19 @@ * limitations under the License. */ +import { isSafariOrWebkit } from '@firebase/util'; + +import { DbIndexEntry } from '../local/indexeddb_schema'; +import { DbIndexEntryKey } from '../local/indexeddb_sentinels'; import { DocumentKey } from '../model/document_key'; /** Represents an index entry saved by the SDK in persisted storage. */ export class IndexEntry { constructor( - readonly indexId: number, - readonly documentKey: DocumentKey, - readonly arrayValue: Uint8Array, - readonly directionalValue: Uint8Array + readonly _indexId: number, + readonly _documentKey: DocumentKey, + readonly _arrayValue: Uint8Array | number[], + readonly _directionalValue: Uint8Array | number[] ) {} /** @@ -31,52 +35,87 @@ export class IndexEntry { * directional value. */ successor(): IndexEntry { - const currentLength = this.directionalValue.length; + const currentLength = this._directionalValue.length; const newLength = - currentLength === 0 || this.directionalValue[currentLength - 1] === 255 + currentLength === 0 || this._directionalValue[currentLength - 1] === 255 ? currentLength + 1 : currentLength; const successor = new Uint8Array(newLength); - successor.set(this.directionalValue, 0); + successor.set(this._directionalValue, 0); if (newLength !== currentLength) { - successor.set([0], this.directionalValue.length); + successor.set([0], this._directionalValue.length); } else { ++successor[successor.length - 1]; } return new IndexEntry( - this.indexId, - this.documentKey, - this.arrayValue, + this._indexId, + this._documentKey, + this._arrayValue, successor ); } + + // Create a representation of the Index Entry as a DbIndexEntry + dbIndexEntry( + uid: string, + orderedDocumentKey: Uint8Array, + documentKey: DocumentKey + ): DbIndexEntry { + return { + indexId: this._indexId, + uid, // this.uid, + arrayValue: indexSafeUint8Array(this._arrayValue), + directionalValue: indexSafeUint8Array(this._directionalValue), + orderedDocumentKey: indexSafeUint8Array(orderedDocumentKey), // this.encodeDirectionalKey(fieldIndex, document.key), + documentKey: documentKey.path.toArray() + }; + } + + // Create a representation of the Index Entry as a DbIndexEntryKey + dbIndexEntryKey( + uid: string, + orderedDocumentKey: Uint8Array, + documentKey: DocumentKey + ): DbIndexEntryKey { + return [ + this._indexId, + uid, + indexSafeUint8Array(this._arrayValue), + indexSafeUint8Array(this._directionalValue), + indexSafeUint8Array(orderedDocumentKey), + documentKey.path.toArray() + ]; + } } export function indexEntryComparator( left: IndexEntry, right: IndexEntry ): number { - let cmp = left.indexId - right.indexId; + let cmp = left._indexId - right._indexId; if (cmp !== 0) { return cmp; } - cmp = compareByteArrays(left.arrayValue, right.arrayValue); + cmp = compareByteArrays(left._arrayValue, right._arrayValue); if (cmp !== 0) { return cmp; } - cmp = compareByteArrays(left.directionalValue, right.directionalValue); + cmp = compareByteArrays(left._directionalValue, right._directionalValue); if (cmp !== 0) { return cmp; } - return DocumentKey.comparator(left.documentKey, right.documentKey); + return DocumentKey.comparator(left._documentKey, right._documentKey); } -export function compareByteArrays(left: Uint8Array, right: Uint8Array): number { +export function compareByteArrays( + left: Uint8Array | number[], + right: Uint8Array | number[] +): number { for (let i = 0; i < left.length && i < right.length; ++i) { const compare = left[i] - right[i]; if (compare !== 0) { @@ -85,3 +124,17 @@ export function compareByteArrays(left: Uint8Array, right: Uint8Array): number { } return left.length - right.length; } + +// Create an safe representation of Uint8Array values +// If the browser is detected as Safari or WebKit, then +// the input array will be converted to `number[]`. +// Otherwise, the input array will be returned in its +// original type. +export function indexSafeUint8Array( + array: Uint8Array | number[] +): Uint8Array | number[] { + if (isSafariOrWebkit() && !Array.isArray(array)) { + return Array.from(array); + } + return array; +} diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index d2b8bc47163..4b497a268d6 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -39,7 +39,11 @@ import { } from '../core/target'; import { FirestoreIndexValueWriter } from '../index/firestore_index_value_writer'; import { IndexByteEncoder } from '../index/index_byte_encoder'; -import { IndexEntry, indexEntryComparator } from '../index/index_entry'; +import { + IndexEntry, + indexEntryComparator, + indexSafeUint8Array +} from '../index/index_entry'; import { documentKeySet, DocumentMap } from '../model/collections'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -294,79 +298,90 @@ export class IndexedDbIndexManager implements IndexManager { let canServeTarget = true; const indexes = new Map(); - return PersistencePromise.forEach( - this.getSubTargets(target), - (subTarget: Target) => { - return this.getFieldIndex(transaction, subTarget).next(index => { - canServeTarget &&= !!index; - indexes.set(subTarget, index); - }); - } - ).next(() => { - if (!canServeTarget) { - return PersistencePromise.resolve(null as DocumentKey[] | null); - } else { - let existingKeys = documentKeySet(); - const result: DocumentKey[] = []; - return PersistencePromise.forEach(indexes, (index, subTarget) => { - logDebug( - LOG_TAG, - `Using index ${fieldIndexToString( - index! - )} to execute ${canonifyTarget(target)}` - ); - - const arrayValues = targetGetArrayValues(subTarget, index!); - const notInValues = targetGetNotInValues(subTarget, index!); - const lowerBound = targetGetLowerBound(subTarget, index!); - const upperBound = targetGetUpperBound(subTarget, index!); - - const lowerBoundEncoded = this.encodeBound( - index!, - subTarget, - lowerBound - ); - const upperBoundEncoded = this.encodeBound( - index!, - subTarget, - upperBound - ); - const notInEncoded = this.encodeValues( - index!, - subTarget, - notInValues - ); - - const indexRanges = this.generateIndexRanges( - index!.indexId, - arrayValues, - lowerBoundEncoded, - lowerBound.inclusive, - upperBoundEncoded, - upperBound.inclusive, - notInEncoded - ); - return PersistencePromise.forEach( - indexRanges, - (indexRange: IDBKeyRange) => { - return indexEntries - .loadFirst(indexRange, target.limit) - .next(entries => { - entries.forEach(entry => { - const documentKey = DocumentKey.fromSegments( - entry.documentKey - ); - if (!existingKeys.has(documentKey)) { - existingKeys = existingKeys.add(documentKey); - result.push(documentKey); - } + return indexEntries + .count() + .next(count => { + //console.log(`INDEX ENTRY COUNT: ${count}`); + return PersistencePromise.forEach( + this.getSubTargets(target), + (subTarget: Target) => { + return this.getFieldIndex(transaction, subTarget).next(index => { + canServeTarget &&= !!index; + indexes.set(subTarget, index); + }); + } + ); + }) + .next(() => { + if (!canServeTarget) { + return PersistencePromise.resolve(null as DocumentKey[] | null); + } else { + let existingKeys = documentKeySet(); + const result: DocumentKey[] = []; + return PersistencePromise.forEach(indexes, (index, subTarget) => { + logDebug( + LOG_TAG, + `Using index ${fieldIndexToString( + index! + )} to execute ${canonifyTarget(target)}` + ); + + const arrayValues = targetGetArrayValues(subTarget, index!); + const notInValues = targetGetNotInValues(subTarget, index!); + const lowerBound = targetGetLowerBound(subTarget, index!); + const upperBound = targetGetUpperBound(subTarget, index!); + + const lowerBoundEncoded = this.encodeBound( + index!, + subTarget, + lowerBound + ); + const upperBoundEncoded = this.encodeBound( + index!, + subTarget, + upperBound + ); + const notInEncoded = this.encodeValues( + index!, + subTarget, + notInValues + ); + + const indexRanges = this.generateIndexRanges( + index!.indexId, + arrayValues, + lowerBoundEncoded, + lowerBound.inclusive, + upperBoundEncoded, + upperBound.inclusive, + notInEncoded + ); + return PersistencePromise.forEach( + indexRanges, + (indexRange: IDBKeyRange) => { + // console.log(`indexRange lower: ${JSON.stringify(indexRange.lower)}, upper: ${JSON.stringify(indexRange.upper)}`); + // console.log(`indexRange lower: ${JSON.stringify(indexRange.lower[2])}, upper: ${JSON.stringify(indexRange.upper[2])}`); + // console.log(`indexRange lower: ${JSON.stringify(indexRange.lowerOpen)}, upper: ${JSON.stringify(indexRange.upperOpen)}`); + // console.log(`target.limit: ${target.limit}`); + return indexEntries + .loadFirst(indexRange, target.limit) + .next(entries => { + // console.log(JSON.stringify(entries)); + entries.forEach(entry => { + const documentKey = DocumentKey.fromSegments( + entry.documentKey + ); + if (!existingKeys.has(documentKey)) { + existingKeys = existingKeys.add(documentKey); + result.push(documentKey); + } + }); }); - }); - } - ); - }).next(() => result as DocumentKey[] | null); - } - }); + } + ); + }).next(() => result as DocumentKey[] | null); + } + }); } private getSubTargets(target: Target): Target[] { @@ -460,8 +475,8 @@ export class IndexedDbIndexManager implements IndexManager { /** Generates the lower bound for `arrayValue` and `directionalValue`. */ private generateLowerBound( indexId: number, - arrayValue: Uint8Array, - directionalValue: Uint8Array, + arrayValue: Uint8Array | number[], + directionalValue: Uint8Array | number[], inclusive: boolean ): IndexEntry { const entry = new IndexEntry( @@ -476,8 +491,8 @@ export class IndexedDbIndexManager implements IndexManager { /** Generates the upper bound for `arrayValue` and `directionalValue`. */ private generateUpperBound( indexId: number, - arrayValue: Uint8Array, - directionalValue: Uint8Array, + arrayValue: Uint8Array | number[], + directionalValue: Uint8Array | number[], inclusive: boolean ): IndexEntry { const entry = new IndexEntry( @@ -817,14 +832,13 @@ export class IndexedDbIndexManager implements IndexManager { indexEntry: IndexEntry ): PersistencePromise { const indexEntries = indexEntriesStore(transaction); - return indexEntries.put({ - indexId: indexEntry.indexId, - uid: this.uid, - arrayValue: indexEntry.arrayValue, - directionalValue: indexEntry.directionalValue, - orderedDocumentKey: this.encodeDirectionalKey(fieldIndex, document.key), - documentKey: document.key.path.toArray() - }); + return indexEntries.put( + indexEntry.dbIndexEntry( + this.uid, + this.encodeDirectionalKey(fieldIndex, document.key), + document.key + ) + ); } private deleteIndexEntry( @@ -834,14 +848,13 @@ export class IndexedDbIndexManager implements IndexManager { indexEntry: IndexEntry ): PersistencePromise { const indexEntries = indexEntriesStore(transaction); - return indexEntries.delete([ - indexEntry.indexId, - this.uid, - indexEntry.arrayValue, - indexEntry.directionalValue, - this.encodeDirectionalKey(fieldIndex, document.key), - document.key.path.toArray() - ]); + return indexEntries.delete( + indexEntry.dbIndexEntryKey( + this.uid, + this.encodeDirectionalKey(fieldIndex, document.key), + document.key + ) + ); } private getExistingIndexEntries( @@ -858,7 +871,9 @@ export class IndexedDbIndexManager implements IndexManager { range: IDBKeyRange.only([ fieldIndex.indexId, this.uid, - this.encodeDirectionalKey(fieldIndex, documentKey) + indexSafeUint8Array( + this.encodeDirectionalKey(fieldIndex, documentKey) + ) ]) }, (_, entry) => { @@ -1020,24 +1035,16 @@ export class IndexedDbIndexManager implements IndexManager { return []; } - const lowerBound = [ - bounds[i].indexId, + const lowerBound = bounds[i].dbIndexEntryKey( this.uid, - bounds[i].arrayValue, - bounds[i].directionalValue, EMPTY_VALUE, - [] - ] as DbIndexEntryKey; - - const upperBound = [ - bounds[i + 1].indexId, + DocumentKey.empty() + ); + const upperBound = bounds[i + 1].dbIndexEntryKey( this.uid, - bounds[i + 1].arrayValue, - bounds[i + 1].directionalValue, EMPTY_VALUE, - [] - ] as DbIndexEntryKey; - + DocumentKey.empty() + ); ranges.push(IDBKeyRange.bound(lowerBound, upperBound)); } return ranges; diff --git a/packages/firestore/src/local/indexeddb_schema.ts b/packages/firestore/src/local/indexeddb_schema.ts index 0395756ab96..24bf7050c85 100644 --- a/packages/firestore/src/local/indexeddb_schema.ts +++ b/packages/firestore/src/local/indexeddb_schema.ts @@ -507,14 +507,14 @@ export interface DbIndexEntry { /** The user id for this entry. */ uid: string; /** The encoded array index value for this entry. */ - arrayValue: Uint8Array; + arrayValue: Uint8Array | number[]; /** The encoded directional value for equality and inequality filters. */ - directionalValue: Uint8Array; + directionalValue: Uint8Array | number[]; /** * The document key this entry points to. This entry is encoded by an ordered * encoder to match the key order of the index. */ - orderedDocumentKey: Uint8Array; + orderedDocumentKey: Uint8Array | number[]; /** The segments of the document key this entry points to. */ documentKey: string[]; } diff --git a/packages/firestore/src/local/indexeddb_sentinels.ts b/packages/firestore/src/local/indexeddb_sentinels.ts index cb6ebcb664a..c35f111df2e 100644 --- a/packages/firestore/src/local/indexeddb_sentinels.ts +++ b/packages/firestore/src/local/indexeddb_sentinels.ts @@ -313,9 +313,9 @@ export const DbIndexStateSequenceNumberIndexPath = ['uid', 'sequenceNumber']; export type DbIndexEntryKey = [ number, string, - Uint8Array, - Uint8Array, - Uint8Array, + Uint8Array | number[], + Uint8Array | number[], + Uint8Array | number[], string[] ]; diff --git a/packages/util/src/environment.ts b/packages/util/src/environment.ts index a0467b08c59..0824b2eceda 100644 --- a/packages/util/src/environment.ts +++ b/packages/util/src/environment.ts @@ -173,6 +173,16 @@ export function isSafari(): boolean { ); } +/** Returns true if we are running in Safari or WebKit */ +export function isSafariOrWebkit(): boolean { + return ( + !isNode() && + !!navigator.userAgent && + (navigator.userAgent.includes('Safari') || navigator.userAgent.includes('WebKit')) && + !navigator.userAgent.includes('Chrome') + ); +} + /** * This method checks if indexedDB is supported by current browser/service worker context * @return true if indexedDB is supported by current browser/service worker context From 385478504c6c210a8343e93fff12d3568996a1b2 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 15:40:03 -0600 Subject: [PATCH 10/20] Scripts for running tests in WebKit locally --- packages/firestore/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 8cc7e5e18f5..4fc4a9dc1ed 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -36,6 +36,8 @@ "test:browser:emulator": "karma start --targetBackend=emulator", "test:browser:nightly": "karma start --targetBackend=nightly", "test:browser:prod": "karma start --targetBackend=prod", + "test:webkit:prod": "BROWSERS=WebkitHeadless karma start --targetBackend=prod --browsers=WebkitHeadless", + "test:webkit:unit": "BROWSERS=WebkitHeadless karma start --unit --targetBackend=prod --browsers=WebkitHeadless", "test:browser:prod:nameddb": "karma start --targetBackend=prod --databaseId=test-db", "test:browser:unit": "karma start --unit", "test:browser:debug": "karma start --browsers=Chrome --auto-watch", From 53ecb6780c136c4667fe5f1da029332b9e2248ef Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 16:03:58 -0600 Subject: [PATCH 11/20] lint fix --- packages/firestore/test/integration/api/database.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 4661745985b..9675e02efeb 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -2433,7 +2433,7 @@ apiDescribe('Database', persistence => { }); }); - describe.skip('Sort unicode strings', () => { + describe('Sort unicode strings', () => { const expectedDocs = [ 'b', 'a', From 44cee475dcbe9cde274a74f65970dad784390f4f Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 17:39:36 -0600 Subject: [PATCH 12/20] formatting --- packages/util/src/environment.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/util/src/environment.ts b/packages/util/src/environment.ts index 0824b2eceda..85410494241 100644 --- a/packages/util/src/environment.ts +++ b/packages/util/src/environment.ts @@ -178,7 +178,8 @@ export function isSafariOrWebkit(): boolean { return ( !isNode() && !!navigator.userAgent && - (navigator.userAgent.includes('Safari') || navigator.userAgent.includes('WebKit')) && + (navigator.userAgent.includes('Safari') || + navigator.userAgent.includes('WebKit')) && !navigator.userAgent.includes('Chrome') ); } From 2380e61c1436f6d8060f38c63e18df9c9d5ac1f2 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 17:41:29 -0600 Subject: [PATCH 13/20] Create eighty-starfishes-listen.md --- .changeset/eighty-starfishes-listen.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/eighty-starfishes-listen.md diff --git a/.changeset/eighty-starfishes-listen.md b/.changeset/eighty-starfishes-listen.md new file mode 100644 index 00000000000..2ee9941c56e --- /dev/null +++ b/.changeset/eighty-starfishes-listen.md @@ -0,0 +1,6 @@ +--- +"@firebase/firestore": patch +"@firebase/util": patch +--- + +Fix encoding of CSI keys in Safari From e6860acafda834c4a04f346a5b3b0613665d1cff Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 7 May 2025 17:50:45 -0600 Subject: [PATCH 14/20] cleanup --- packages/firestore/src/index/index_entry.ts | 4 +- .../src/local/indexeddb_index_manager.ts | 163 ++++++++---------- 2 files changed, 78 insertions(+), 89 deletions(-) diff --git a/packages/firestore/src/index/index_entry.ts b/packages/firestore/src/index/index_entry.ts index e3d9477077f..907beb4f388 100644 --- a/packages/firestore/src/index/index_entry.ts +++ b/packages/firestore/src/index/index_entry.ts @@ -65,10 +65,10 @@ export class IndexEntry { ): DbIndexEntry { return { indexId: this._indexId, - uid, // this.uid, + uid, arrayValue: indexSafeUint8Array(this._arrayValue), directionalValue: indexSafeUint8Array(this._directionalValue), - orderedDocumentKey: indexSafeUint8Array(orderedDocumentKey), // this.encodeDirectionalKey(fieldIndex, document.key), + orderedDocumentKey: indexSafeUint8Array(orderedDocumentKey), documentKey: documentKey.path.toArray() }; } diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index 4b497a268d6..7d28649df2f 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -298,90 +298,79 @@ export class IndexedDbIndexManager implements IndexManager { let canServeTarget = true; const indexes = new Map(); - return indexEntries - .count() - .next(count => { - //console.log(`INDEX ENTRY COUNT: ${count}`); - return PersistencePromise.forEach( - this.getSubTargets(target), - (subTarget: Target) => { - return this.getFieldIndex(transaction, subTarget).next(index => { - canServeTarget &&= !!index; - indexes.set(subTarget, index); - }); - } - ); - }) - .next(() => { - if (!canServeTarget) { - return PersistencePromise.resolve(null as DocumentKey[] | null); - } else { - let existingKeys = documentKeySet(); - const result: DocumentKey[] = []; - return PersistencePromise.forEach(indexes, (index, subTarget) => { - logDebug( - LOG_TAG, - `Using index ${fieldIndexToString( - index! - )} to execute ${canonifyTarget(target)}` - ); - - const arrayValues = targetGetArrayValues(subTarget, index!); - const notInValues = targetGetNotInValues(subTarget, index!); - const lowerBound = targetGetLowerBound(subTarget, index!); - const upperBound = targetGetUpperBound(subTarget, index!); - - const lowerBoundEncoded = this.encodeBound( - index!, - subTarget, - lowerBound - ); - const upperBoundEncoded = this.encodeBound( - index!, - subTarget, - upperBound - ); - const notInEncoded = this.encodeValues( - index!, - subTarget, - notInValues - ); - - const indexRanges = this.generateIndexRanges( - index!.indexId, - arrayValues, - lowerBoundEncoded, - lowerBound.inclusive, - upperBoundEncoded, - upperBound.inclusive, - notInEncoded - ); - return PersistencePromise.forEach( - indexRanges, - (indexRange: IDBKeyRange) => { - // console.log(`indexRange lower: ${JSON.stringify(indexRange.lower)}, upper: ${JSON.stringify(indexRange.upper)}`); - // console.log(`indexRange lower: ${JSON.stringify(indexRange.lower[2])}, upper: ${JSON.stringify(indexRange.upper[2])}`); - // console.log(`indexRange lower: ${JSON.stringify(indexRange.lowerOpen)}, upper: ${JSON.stringify(indexRange.upperOpen)}`); - // console.log(`target.limit: ${target.limit}`); - return indexEntries - .loadFirst(indexRange, target.limit) - .next(entries => { - // console.log(JSON.stringify(entries)); - entries.forEach(entry => { - const documentKey = DocumentKey.fromSegments( - entry.documentKey - ); - if (!existingKeys.has(documentKey)) { - existingKeys = existingKeys.add(documentKey); - result.push(documentKey); - } - }); + return PersistencePromise.forEach( + this.getSubTargets(target), + (subTarget: Target) => { + return this.getFieldIndex(transaction, subTarget).next(index => { + canServeTarget &&= !!index; + indexes.set(subTarget, index); + }); + } + ).next(() => { + if (!canServeTarget) { + return PersistencePromise.resolve(null as DocumentKey[] | null); + } else { + let existingKeys = documentKeySet(); + const result: DocumentKey[] = []; + return PersistencePromise.forEach(indexes, (index, subTarget) => { + logDebug( + LOG_TAG, + `Using index ${fieldIndexToString( + index! + )} to execute ${canonifyTarget(target)}` + ); + + const arrayValues = targetGetArrayValues(subTarget, index!); + const notInValues = targetGetNotInValues(subTarget, index!); + const lowerBound = targetGetLowerBound(subTarget, index!); + const upperBound = targetGetUpperBound(subTarget, index!); + + const lowerBoundEncoded = this.encodeBound( + index!, + subTarget, + lowerBound + ); + const upperBoundEncoded = this.encodeBound( + index!, + subTarget, + upperBound + ); + const notInEncoded = this.encodeValues( + index!, + subTarget, + notInValues + ); + + const indexRanges = this.generateIndexRanges( + index!.indexId, + arrayValues, + lowerBoundEncoded, + lowerBound.inclusive, + upperBoundEncoded, + upperBound.inclusive, + notInEncoded + ); + return PersistencePromise.forEach( + indexRanges, + (indexRange: IDBKeyRange) => { + return indexEntries + .loadFirst(indexRange, target.limit) + .next(entries => { + entries.forEach(entry => { + const documentKey = DocumentKey.fromSegments( + entry.documentKey + ); + if (!existingKeys.has(documentKey)) { + existingKeys = existingKeys.add(documentKey); + result.push(documentKey); + } }); - } - ); - }).next(() => result as DocumentKey[] | null); - } - }); + }); + } + ); + }).next(() => result as DocumentKey[] | null); + } + }); } private getSubTargets(target: Target): Target[] { @@ -475,8 +464,8 @@ export class IndexedDbIndexManager implements IndexManager { /** Generates the lower bound for `arrayValue` and `directionalValue`. */ private generateLowerBound( indexId: number, - arrayValue: Uint8Array | number[], - directionalValue: Uint8Array | number[], + arrayValue: Uint8Array, + directionalValue: Uint8Array, inclusive: boolean ): IndexEntry { const entry = new IndexEntry( @@ -491,8 +480,8 @@ export class IndexedDbIndexManager implements IndexManager { /** Generates the upper bound for `arrayValue` and `directionalValue`. */ private generateUpperBound( indexId: number, - arrayValue: Uint8Array | number[], - directionalValue: Uint8Array | number[], + arrayValue: Uint8Array, + directionalValue: Uint8Array, inclusive: boolean ): IndexEntry { const entry = new IndexEntry( From dfd8f221459fb804c32e8030ff7a8d2a5d8002f5 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 8 May 2025 13:47:57 -0600 Subject: [PATCH 15/20] Swap encoding of key safe bytes from number[] to sortable string. This improves encoding efficency by 4x. --- packages/firestore/src/index/index_entry.ts | 76 ++++++++++++++----- .../src/local/indexeddb_index_manager.ts | 9 ++- .../firestore/src/local/indexeddb_schema.ts | 11 ++- .../src/local/indexeddb_schema_converter.ts | 18 +++++ .../src/local/indexeddb_sentinels.ts | 22 ++++-- 5 files changed, 105 insertions(+), 31 deletions(-) diff --git a/packages/firestore/src/index/index_entry.ts b/packages/firestore/src/index/index_entry.ts index 907beb4f388..abac408e65f 100644 --- a/packages/firestore/src/index/index_entry.ts +++ b/packages/firestore/src/index/index_entry.ts @@ -18,7 +18,7 @@ import { isSafariOrWebkit } from '@firebase/util'; import { DbIndexEntry } from '../local/indexeddb_schema'; -import { DbIndexEntryKey } from '../local/indexeddb_sentinels'; +import { DbIndexEntryKey, KeySafeBytes } from '../local/indexeddb_sentinels'; import { DocumentKey } from '../model/document_key'; /** Represents an index entry saved by the SDK in persisted storage. */ @@ -26,8 +26,8 @@ export class IndexEntry { constructor( readonly _indexId: number, readonly _documentKey: DocumentKey, - readonly _arrayValue: Uint8Array | number[], - readonly _directionalValue: Uint8Array | number[] + readonly _arrayValue: Uint8Array, + readonly _directionalValue: Uint8Array ) {} /** @@ -66,9 +66,9 @@ export class IndexEntry { return { indexId: this._indexId, uid, - arrayValue: indexSafeUint8Array(this._arrayValue), - directionalValue: indexSafeUint8Array(this._directionalValue), - orderedDocumentKey: indexSafeUint8Array(orderedDocumentKey), + arrayValue: encodeKeySafeBytes(this._arrayValue), + directionalValue: encodeKeySafeBytes(this._directionalValue), + orderedDocumentKey: encodeKeySafeBytes(orderedDocumentKey), documentKey: documentKey.path.toArray() }; } @@ -82,9 +82,9 @@ export class IndexEntry { return [ this._indexId, uid, - indexSafeUint8Array(this._arrayValue), - indexSafeUint8Array(this._directionalValue), - indexSafeUint8Array(orderedDocumentKey), + encodeKeySafeBytes(this._arrayValue), + encodeKeySafeBytes(this._directionalValue), + encodeKeySafeBytes(orderedDocumentKey), documentKey.path.toArray() ]; } @@ -125,16 +125,56 @@ export function compareByteArrays( return left.length - right.length; } -// Create an safe representation of Uint8Array values -// If the browser is detected as Safari or WebKit, then -// the input array will be converted to `number[]`. -// Otherwise, the input array will be returned in its -// original type. -export function indexSafeUint8Array( - array: Uint8Array | number[] -): Uint8Array | number[] { +/** + * Workaround for WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=292721 + * Create a key safe representation of Uint8Array values. + * If the browser is detected as Safari or WebKit, then + * the input array will be converted to "sortable byte string". + * Otherwise, the input array will be returned in its original type. + */ +export function encodeKeySafeBytes(array: Uint8Array): KeySafeBytes { if (isSafariOrWebkit() && !Array.isArray(array)) { - return Array.from(array); + return encodeUint8ArrayToSortableString(array); } return array; } + +/** + * Reverts the key safe representation of Uint8Array (created by + * indexSafeUint8Array) to a normal Uint8Array. + */ +export function decodeKeySafeBytes(input: KeySafeBytes): Uint8Array { + if (typeof input !== 'string') { + return input; + } + return decodeSortableStringToUint8Array(input); +} + +/** + * Encodes a Uint8Array into a "sortable byte string". + * A "sortable byte string" sorts in the same order as the Uint8Array. + * This works because JS string comparison sorts strings based on code points. + */ +function encodeUint8ArrayToSortableString(array: Uint8Array): string { + let byteString = ''; + for (let i = 0; i < array.length; i++) { + byteString += String.fromCharCode(array[i]); + } + + return byteString; +} + +/** + * Decodes a "sortable byte string" back into a Uint8Array. + * A "sortable byte string" is assumed to be created where each character's + * Unicode code point directly corresponds to a single byte value (0-255). + */ +function decodeSortableStringToUint8Array(byteString: string): Uint8Array { + const uint8array = new Uint8Array(byteString.length); + + for (let i = 0; i < byteString.length; i++) { + uint8array[i] = byteString.charCodeAt(i); + } + + return uint8array; +} diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index 7d28649df2f..2d707470b2f 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -42,7 +42,8 @@ import { IndexByteEncoder } from '../index/index_byte_encoder'; import { IndexEntry, indexEntryComparator, - indexSafeUint8Array + encodeKeySafeBytes, + decodeKeySafeBytes } from '../index/index_entry'; import { documentKeySet, DocumentMap } from '../model/collections'; import { Document } from '../model/document'; @@ -860,7 +861,7 @@ export class IndexedDbIndexManager implements IndexManager { range: IDBKeyRange.only([ fieldIndex.indexId, this.uid, - indexSafeUint8Array( + encodeKeySafeBytes( this.encodeDirectionalKey(fieldIndex, documentKey) ) ]) @@ -870,8 +871,8 @@ export class IndexedDbIndexManager implements IndexManager { new IndexEntry( fieldIndex.indexId, documentKey, - entry.arrayValue, - entry.directionalValue + decodeKeySafeBytes(entry.arrayValue), + decodeKeySafeBytes(entry.directionalValue) ) ); } diff --git a/packages/firestore/src/local/indexeddb_schema.ts b/packages/firestore/src/local/indexeddb_schema.ts index 24bf7050c85..e98ae888ae2 100644 --- a/packages/firestore/src/local/indexeddb_schema.ts +++ b/packages/firestore/src/local/indexeddb_schema.ts @@ -16,6 +16,7 @@ */ import { BatchId, ListenSequenceNumber, TargetId } from '../core/types'; +import { KeySafeBytes } from '../index/index_entry'; import { IndexKind } from '../model/field_index'; import { BundledQuery } from '../protos/firestore_bundle_proto'; import { @@ -52,9 +53,11 @@ import { DbTimestampKey } from './indexeddb_sentinels'; * 14. Add overlays. * 15. Add indexing support. * 16. Parse timestamp strings before creating index entries. + * 17. TODO(tomandersen) + * 18. Encode key safe representations of IndexEntry in DbIndexEntryStore. */ -export const SCHEMA_VERSION = 17; +export const SCHEMA_VERSION = 18; /** * Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects. @@ -507,14 +510,14 @@ export interface DbIndexEntry { /** The user id for this entry. */ uid: string; /** The encoded array index value for this entry. */ - arrayValue: Uint8Array | number[]; + arrayValue: KeySafeBytes; /** The encoded directional value for equality and inequality filters. */ - directionalValue: Uint8Array | number[]; + directionalValue: KeySafeBytes; /** * The document key this entry points to. This entry is encoded by an ordered * encoder to match the key order of the index. */ - orderedDocumentKey: Uint8Array | number[]; + orderedDocumentKey: KeySafeBytes; /** The segments of the document key this entry points to. */ documentKey: string[]; } diff --git a/packages/firestore/src/local/indexeddb_schema_converter.ts b/packages/firestore/src/local/indexeddb_schema_converter.ts index 7446ae7ae20..1b84fa4f2c1 100644 --- a/packages/firestore/src/local/indexeddb_schema_converter.ts +++ b/packages/firestore/src/local/indexeddb_schema_converter.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { isSafariOrWebkit } from '@firebase/util'; + import { User } from '../auth/user'; import { ListenSequence } from '../core/listen_sequence'; import { SnapshotVersion } from '../core/snapshot_version'; @@ -277,6 +279,22 @@ export class SchemaConverter implements SimpleDbSchemaConverter { }); } + if (fromVersion < 18 && toVersion >= 18) { + // Clear the IndexEntryStores on WebKit and Safari to remove possibly + // corrupted index entries + if (isSafariOrWebkit()) { + p = p + .next(() => { + const indexStateStore = txn.objectStore(DbIndexStateStore); + indexStateStore.clear(); + }) + .next(() => { + const indexEntryStore = txn.objectStore(DbIndexEntryStore); + indexEntryStore.clear(); + }); + } + } + return p; } diff --git a/packages/firestore/src/local/indexeddb_sentinels.ts b/packages/firestore/src/local/indexeddb_sentinels.ts index c35f111df2e..0b4f5ed8918 100644 --- a/packages/firestore/src/local/indexeddb_sentinels.ts +++ b/packages/firestore/src/local/indexeddb_sentinels.ts @@ -305,6 +305,15 @@ export const DbIndexStateSequenceNumberIndex = 'sequenceNumberIndex'; export const DbIndexStateSequenceNumberIndexPath = ['uid', 'sequenceNumber']; +/** + * Representation of a byte array that is safe for + * use in an IndexedDb key. The value is either + * a "sortable byte string", which is key safe in + * Safari/WebKit, or the value is a Uint8Array, + * which is key safe in other browsers. + */ +export type KeySafeBytes = Uint8Array | string; + /** * The key for each index entry consists of the index id and its user id, * the encoded array and directional value for the indexed fields as well as @@ -313,9 +322,9 @@ export const DbIndexStateSequenceNumberIndexPath = ['uid', 'sequenceNumber']; export type DbIndexEntryKey = [ number, string, - Uint8Array | number[], - Uint8Array | number[], - Uint8Array | number[], + KeySafeBytes, + KeySafeBytes, + KeySafeBytes, string[] ]; @@ -425,6 +434,7 @@ export const V15_STORES = [ ]; export const V16_STORES = V15_STORES; export const V17_STORES = [...V15_STORES, DbGlobalsStore]; +export const V18_STORES = V17_STORES; /** * The list of all default IndexedDB stores used throughout the SDK. This is @@ -435,7 +445,9 @@ export const ALL_STORES = V12_STORES; /** Returns the object stores for the provided schema. */ export function getObjectStores(schemaVersion: number): string[] { - if (schemaVersion === 17) { + if (schemaVersion === 18) { + return V18_STORES; + } else if (schemaVersion === 17) { return V17_STORES; } else if (schemaVersion === 16) { return V16_STORES; @@ -450,6 +462,6 @@ export function getObjectStores(schemaVersion: number): string[] { } else if (schemaVersion === 11) { return V11_STORES; } else { - fail(0xeb55, 'Only schema version 11 and 12 and 13 are supported'); + fail(0xeb55, 'Only schema versions >11 are supported'); } } From 022a55e0a169c05fda2cffc202a75be62f565e33 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 8 May 2025 13:49:40 -0600 Subject: [PATCH 16/20] Update eighty-starfishes-listen.md --- .changeset/eighty-starfishes-listen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-starfishes-listen.md b/.changeset/eighty-starfishes-listen.md index 2ee9941c56e..78801bd3c8c 100644 --- a/.changeset/eighty-starfishes-listen.md +++ b/.changeset/eighty-starfishes-listen.md @@ -1,6 +1,6 @@ --- "@firebase/firestore": patch -"@firebase/util": patch +"@firebase/util": minor --- Fix encoding of CSI keys in Safari From d4086bbcf4978516621e7c402b75c7e5ce3c8071 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 8 May 2025 13:51:36 -0600 Subject: [PATCH 17/20] cleanup --- packages/firestore/src/index/index_entry.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/firestore/src/index/index_entry.ts b/packages/firestore/src/index/index_entry.ts index abac408e65f..67e1db9ef2a 100644 --- a/packages/firestore/src/index/index_entry.ts +++ b/packages/firestore/src/index/index_entry.ts @@ -112,10 +112,7 @@ export function indexEntryComparator( return DocumentKey.comparator(left._documentKey, right._documentKey); } -export function compareByteArrays( - left: Uint8Array | number[], - right: Uint8Array | number[] -): number { +export function compareByteArrays(left: Uint8Array, right: Uint8Array): number { for (let i = 0; i < left.length && i < right.length; ++i) { const compare = left[i] - right[i]; if (compare !== 0) { From 26739c9afb3342ffaac7de936ff9927c5f53d3ea Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 8 May 2025 14:02:51 -0600 Subject: [PATCH 18/20] fix import --- packages/firestore/src/local/indexeddb_schema.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/firestore/src/local/indexeddb_schema.ts b/packages/firestore/src/local/indexeddb_schema.ts index e98ae888ae2..b8b6c1111d8 100644 --- a/packages/firestore/src/local/indexeddb_schema.ts +++ b/packages/firestore/src/local/indexeddb_schema.ts @@ -16,7 +16,6 @@ */ import { BatchId, ListenSequenceNumber, TargetId } from '../core/types'; -import { KeySafeBytes } from '../index/index_entry'; import { IndexKind } from '../model/field_index'; import { BundledQuery } from '../protos/firestore_bundle_proto'; import { @@ -27,7 +26,7 @@ import { } from '../protos/firestore_proto_api'; import { EncodedResourcePath } from './encoded_resource_path'; -import { DbTimestampKey } from './indexeddb_sentinels'; +import { DbTimestampKey, KeySafeBytes } from './indexeddb_sentinels'; /** * Schema Version for the Web client: From 4d5b5cc12c6a16cd57ada05fd55a14e0e6924443 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Fri, 9 May 2025 13:38:18 -0600 Subject: [PATCH 19/20] Address pr feedback --- packages/firestore/package.json | 4 ++-- packages/firestore/src/index/index_entry.ts | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/firestore/package.json b/packages/firestore/package.json index a35975e6128..fdf849b996e 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -36,8 +36,8 @@ "test:browser:emulator": "karma start --targetBackend=emulator", "test:browser:nightly": "karma start --targetBackend=nightly", "test:browser:prod": "karma start --targetBackend=prod", - "test:webkit:prod": "BROWSERS=WebkitHeadless karma start --targetBackend=prod --browsers=WebkitHeadless", - "test:webkit:unit": "BROWSERS=WebkitHeadless karma start --unit --targetBackend=prod --browsers=WebkitHeadless", + "test:webkit:prod": "BROWSERS=WebkitHeadless karma start --targetBackend=prod", + "test:webkit:unit": "BROWSERS=WebkitHeadless karma start --unit --targetBackend=prod", "test:browser:prod:nameddb": "karma start --targetBackend=prod --databaseId=test-db", "test:browser:unit": "karma start --unit", "test:browser:debug": "karma start --browsers=Chrome --auto-watch", diff --git a/packages/firestore/src/index/index_entry.ts b/packages/firestore/src/index/index_entry.ts index 67e1db9ef2a..c9f3218c65e 100644 --- a/packages/firestore/src/index/index_entry.ts +++ b/packages/firestore/src/index/index_entry.ts @@ -79,13 +79,14 @@ export class IndexEntry { orderedDocumentKey: Uint8Array, documentKey: DocumentKey ): DbIndexEntryKey { + const entry = this.dbIndexEntry(uid, orderedDocumentKey, documentKey); return [ - this._indexId, - uid, - encodeKeySafeBytes(this._arrayValue), - encodeKeySafeBytes(this._directionalValue), - encodeKeySafeBytes(orderedDocumentKey), - documentKey.path.toArray() + entry.indexId, + entry.uid, + entry.arrayValue, + entry.directionalValue, + entry.orderedDocumentKey, + entry.documentKey ]; } } @@ -130,7 +131,7 @@ export function compareByteArrays(left: Uint8Array, right: Uint8Array): number { * Otherwise, the input array will be returned in its original type. */ export function encodeKeySafeBytes(array: Uint8Array): KeySafeBytes { - if (isSafariOrWebkit() && !Array.isArray(array)) { + if (isSafariOrWebkit()) { return encodeUint8ArrayToSortableString(array); } return array; @@ -138,7 +139,7 @@ export function encodeKeySafeBytes(array: Uint8Array): KeySafeBytes { /** * Reverts the key safe representation of Uint8Array (created by - * indexSafeUint8Array) to a normal Uint8Array. + * encodeKeySafeBytes) to a normal Uint8Array. */ export function decodeKeySafeBytes(input: KeySafeBytes): Uint8Array { if (typeof input !== 'string') { From 0dc1ea617674ff116fa951d6d595e1fbee25c4c2 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Fri, 9 May 2025 14:11:13 -0600 Subject: [PATCH 20/20] fix changelog --- .changeset/eighty-starfishes-listen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eighty-starfishes-listen.md b/.changeset/eighty-starfishes-listen.md index 78801bd3c8c..4f53b0fb786 100644 --- a/.changeset/eighty-starfishes-listen.md +++ b/.changeset/eighty-starfishes-listen.md @@ -3,4 +3,4 @@ "@firebase/util": minor --- -Fix encoding of CSI keys in Safari +Fix Safari/WebKit cache issues when client-side indexing is used.