Skip to content

Commit dfd8f22

Browse files
committed
Swap encoding of key safe bytes from number[] to sortable string. This improves encoding efficency by 4x.
1 parent e6860ac commit dfd8f22

File tree

5 files changed

+105
-31
lines changed

5 files changed

+105
-31
lines changed

packages/firestore/src/index/index_entry.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@
1818
import { isSafariOrWebkit } from '@firebase/util';
1919

2020
import { DbIndexEntry } from '../local/indexeddb_schema';
21-
import { DbIndexEntryKey } from '../local/indexeddb_sentinels';
21+
import { DbIndexEntryKey, KeySafeBytes } from '../local/indexeddb_sentinels';
2222
import { DocumentKey } from '../model/document_key';
2323

2424
/** Represents an index entry saved by the SDK in persisted storage. */
2525
export class IndexEntry {
2626
constructor(
2727
readonly _indexId: number,
2828
readonly _documentKey: DocumentKey,
29-
readonly _arrayValue: Uint8Array | number[],
30-
readonly _directionalValue: Uint8Array | number[]
29+
readonly _arrayValue: Uint8Array,
30+
readonly _directionalValue: Uint8Array
3131
) {}
3232

3333
/**
@@ -66,9 +66,9 @@ export class IndexEntry {
6666
return {
6767
indexId: this._indexId,
6868
uid,
69-
arrayValue: indexSafeUint8Array(this._arrayValue),
70-
directionalValue: indexSafeUint8Array(this._directionalValue),
71-
orderedDocumentKey: indexSafeUint8Array(orderedDocumentKey),
69+
arrayValue: encodeKeySafeBytes(this._arrayValue),
70+
directionalValue: encodeKeySafeBytes(this._directionalValue),
71+
orderedDocumentKey: encodeKeySafeBytes(orderedDocumentKey),
7272
documentKey: documentKey.path.toArray()
7373
};
7474
}
@@ -82,9 +82,9 @@ export class IndexEntry {
8282
return [
8383
this._indexId,
8484
uid,
85-
indexSafeUint8Array(this._arrayValue),
86-
indexSafeUint8Array(this._directionalValue),
87-
indexSafeUint8Array(orderedDocumentKey),
85+
encodeKeySafeBytes(this._arrayValue),
86+
encodeKeySafeBytes(this._directionalValue),
87+
encodeKeySafeBytes(orderedDocumentKey),
8888
documentKey.path.toArray()
8989
];
9090
}
@@ -125,16 +125,56 @@ export function compareByteArrays(
125125
return left.length - right.length;
126126
}
127127

128-
// Create an safe representation of Uint8Array values
129-
// If the browser is detected as Safari or WebKit, then
130-
// the input array will be converted to `number[]`.
131-
// Otherwise, the input array will be returned in its
132-
// original type.
133-
export function indexSafeUint8Array(
134-
array: Uint8Array | number[]
135-
): Uint8Array | number[] {
128+
/**
129+
* Workaround for WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=292721
130+
* Create a key safe representation of Uint8Array values.
131+
* If the browser is detected as Safari or WebKit, then
132+
* the input array will be converted to "sortable byte string".
133+
* Otherwise, the input array will be returned in its original type.
134+
*/
135+
export function encodeKeySafeBytes(array: Uint8Array): KeySafeBytes {
136136
if (isSafariOrWebkit() && !Array.isArray(array)) {
137-
return Array.from(array);
137+
return encodeUint8ArrayToSortableString(array);
138138
}
139139
return array;
140140
}
141+
142+
/**
143+
* Reverts the key safe representation of Uint8Array (created by
144+
* indexSafeUint8Array) to a normal Uint8Array.
145+
*/
146+
export function decodeKeySafeBytes(input: KeySafeBytes): Uint8Array {
147+
if (typeof input !== 'string') {
148+
return input;
149+
}
150+
return decodeSortableStringToUint8Array(input);
151+
}
152+
153+
/**
154+
* Encodes a Uint8Array into a "sortable byte string".
155+
* A "sortable byte string" sorts in the same order as the Uint8Array.
156+
* This works because JS string comparison sorts strings based on code points.
157+
*/
158+
function encodeUint8ArrayToSortableString(array: Uint8Array): string {
159+
let byteString = '';
160+
for (let i = 0; i < array.length; i++) {
161+
byteString += String.fromCharCode(array[i]);
162+
}
163+
164+
return byteString;
165+
}
166+
167+
/**
168+
* Decodes a "sortable byte string" back into a Uint8Array.
169+
* A "sortable byte string" is assumed to be created where each character's
170+
* Unicode code point directly corresponds to a single byte value (0-255).
171+
*/
172+
function decodeSortableStringToUint8Array(byteString: string): Uint8Array {
173+
const uint8array = new Uint8Array(byteString.length);
174+
175+
for (let i = 0; i < byteString.length; i++) {
176+
uint8array[i] = byteString.charCodeAt(i);
177+
}
178+
179+
return uint8array;
180+
}

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ import { IndexByteEncoder } from '../index/index_byte_encoder';
4242
import {
4343
IndexEntry,
4444
indexEntryComparator,
45-
indexSafeUint8Array
45+
encodeKeySafeBytes,
46+
decodeKeySafeBytes
4647
} from '../index/index_entry';
4748
import { documentKeySet, DocumentMap } from '../model/collections';
4849
import { Document } from '../model/document';
@@ -860,7 +861,7 @@ export class IndexedDbIndexManager implements IndexManager {
860861
range: IDBKeyRange.only([
861862
fieldIndex.indexId,
862863
this.uid,
863-
indexSafeUint8Array(
864+
encodeKeySafeBytes(
864865
this.encodeDirectionalKey(fieldIndex, documentKey)
865866
)
866867
])
@@ -870,8 +871,8 @@ export class IndexedDbIndexManager implements IndexManager {
870871
new IndexEntry(
871872
fieldIndex.indexId,
872873
documentKey,
873-
entry.arrayValue,
874-
entry.directionalValue
874+
decodeKeySafeBytes(entry.arrayValue),
875+
decodeKeySafeBytes(entry.directionalValue)
875876
)
876877
);
877878
}

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import { BatchId, ListenSequenceNumber, TargetId } from '../core/types';
19+
import { KeySafeBytes } from '../index/index_entry';
1920
import { IndexKind } from '../model/field_index';
2021
import { BundledQuery } from '../protos/firestore_bundle_proto';
2122
import {
@@ -52,9 +53,11 @@ import { DbTimestampKey } from './indexeddb_sentinels';
5253
* 14. Add overlays.
5354
* 15. Add indexing support.
5455
* 16. Parse timestamp strings before creating index entries.
56+
* 17. TODO(tomandersen)
57+
* 18. Encode key safe representations of IndexEntry in DbIndexEntryStore.
5558
*/
5659

57-
export const SCHEMA_VERSION = 17;
60+
export const SCHEMA_VERSION = 18;
5861

5962
/**
6063
* Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects.
@@ -507,14 +510,14 @@ export interface DbIndexEntry {
507510
/** The user id for this entry. */
508511
uid: string;
509512
/** The encoded array index value for this entry. */
510-
arrayValue: Uint8Array | number[];
513+
arrayValue: KeySafeBytes;
511514
/** The encoded directional value for equality and inequality filters. */
512-
directionalValue: Uint8Array | number[];
515+
directionalValue: KeySafeBytes;
513516
/**
514517
* The document key this entry points to. This entry is encoded by an ordered
515518
* encoder to match the key order of the index.
516519
*/
517-
orderedDocumentKey: Uint8Array | number[];
520+
orderedDocumentKey: KeySafeBytes;
518521
/** The segments of the document key this entry points to. */
519522
documentKey: string[];
520523
}

packages/firestore/src/local/indexeddb_schema_converter.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { isSafariOrWebkit } from '@firebase/util';
19+
1820
import { User } from '../auth/user';
1921
import { ListenSequence } from '../core/listen_sequence';
2022
import { SnapshotVersion } from '../core/snapshot_version';
@@ -277,6 +279,22 @@ export class SchemaConverter implements SimpleDbSchemaConverter {
277279
});
278280
}
279281

282+
if (fromVersion < 18 && toVersion >= 18) {
283+
// Clear the IndexEntryStores on WebKit and Safari to remove possibly
284+
// corrupted index entries
285+
if (isSafariOrWebkit()) {
286+
p = p
287+
.next(() => {
288+
const indexStateStore = txn.objectStore(DbIndexStateStore);
289+
indexStateStore.clear();
290+
})
291+
.next(() => {
292+
const indexEntryStore = txn.objectStore(DbIndexEntryStore);
293+
indexEntryStore.clear();
294+
});
295+
}
296+
}
297+
280298
return p;
281299
}
282300

packages/firestore/src/local/indexeddb_sentinels.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,15 @@ export const DbIndexStateSequenceNumberIndex = 'sequenceNumberIndex';
305305

306306
export const DbIndexStateSequenceNumberIndexPath = ['uid', 'sequenceNumber'];
307307

308+
/**
309+
* Representation of a byte array that is safe for
310+
* use in an IndexedDb key. The value is either
311+
* a "sortable byte string", which is key safe in
312+
* Safari/WebKit, or the value is a Uint8Array,
313+
* which is key safe in other browsers.
314+
*/
315+
export type KeySafeBytes = Uint8Array | string;
316+
308317
/**
309318
* The key for each index entry consists of the index id and its user id,
310319
* the encoded array and directional value for the indexed fields as well as
@@ -313,9 +322,9 @@ export const DbIndexStateSequenceNumberIndexPath = ['uid', 'sequenceNumber'];
313322
export type DbIndexEntryKey = [
314323
number,
315324
string,
316-
Uint8Array | number[],
317-
Uint8Array | number[],
318-
Uint8Array | number[],
325+
KeySafeBytes,
326+
KeySafeBytes,
327+
KeySafeBytes,
319328
string[]
320329
];
321330

@@ -425,6 +434,7 @@ export const V15_STORES = [
425434
];
426435
export const V16_STORES = V15_STORES;
427436
export const V17_STORES = [...V15_STORES, DbGlobalsStore];
437+
export const V18_STORES = V17_STORES;
428438

429439
/**
430440
* The list of all default IndexedDB stores used throughout the SDK. This is
@@ -435,7 +445,9 @@ export const ALL_STORES = V12_STORES;
435445

436446
/** Returns the object stores for the provided schema. */
437447
export function getObjectStores(schemaVersion: number): string[] {
438-
if (schemaVersion === 17) {
448+
if (schemaVersion === 18) {
449+
return V18_STORES;
450+
} else if (schemaVersion === 17) {
439451
return V17_STORES;
440452
} else if (schemaVersion === 16) {
441453
return V16_STORES;
@@ -450,6 +462,6 @@ export function getObjectStores(schemaVersion: number): string[] {
450462
} else if (schemaVersion === 11) {
451463
return V11_STORES;
452464
} else {
453-
fail(0xeb55, 'Only schema version 11 and 12 and 13 are supported');
465+
fail(0xeb55, 'Only schema versions >11 are supported');
454466
}
455467
}

0 commit comments

Comments
 (0)