Skip to content

Commit

Permalink
Re-open IndexedDB if closed (#3535)
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian committed Aug 5, 2020
1 parent cf3401d commit 36be62a
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 223 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-drinks-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@firebase/firestore": patch
---

The SDK no longer crashes with the error "The database connection is closing". Instead, the individual operations that cause this error may be rejected.
25 changes: 10 additions & 15 deletions packages/firestore/src/local/indexeddb_persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,7 @@ export class IndexedDbPersistence implements Persistence {
}
}

// Technically `simpleDb` should be `| undefined` because it is
// initialized asynchronously by start(), but that would be more misleading
// than useful.
private simpleDb!: SimpleDb;
private simpleDb: SimpleDb;

private listenSequence: ListenSequence | null = null;

Expand Down Expand Up @@ -259,6 +256,11 @@ export class IndexedDbPersistence implements Persistence {
this.referenceDelegate = new IndexedDbLruDelegate(this, lruParams);
this.dbName = persistenceKey + MAIN_DATABASE;
this.serializer = new LocalSerializer(serializer);
this.simpleDb = new SimpleDb(
this.dbName,
SCHEMA_VERSION,
new SchemaConverter(this.serializer)
);
this.targetCache = new IndexedDbTargetCache(
this.referenceDelegate,
this.serializer
Expand Down Expand Up @@ -292,17 +294,10 @@ export class IndexedDbPersistence implements Persistence {
debugAssert(!this.started, 'IndexedDbPersistence double-started!');
debugAssert(this.window !== null, "Expected 'window' to be defined");

return SimpleDb.openOrCreate(
this.dbName,
SCHEMA_VERSION,
new SchemaConverter(this.serializer)
)
.then(db => {
this.simpleDb = db;
// NOTE: This is expected to fail sometimes (in the case of another tab already
// having the persistence lock), so it's the first thing we should do.
return this.updateClientMetadataAndTryBecomePrimary();
})
// NOTE: This is expected to fail sometimes (in the case of another tab
// already having the persistence lock), so it's the first thing we should
// do.
return this.updateClientMetadataAndTryBecomePrimary()
.then(() => {
if (!this.isPrimary && !this.allowTabSynchronization) {
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
Expand Down
217 changes: 122 additions & 95 deletions packages/firestore/src/local/simple_db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { debugAssert } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
import { logDebug, logError } from '../util/log';
import { Deferred } from '../util/promise';
import { SCHEMA_VERSION } from './indexeddb_schema';
import { PersistencePromise } from './persistence_promise';

// References to `window` are guarded by SimpleDb.isAvailable()
Expand Down Expand Up @@ -54,88 +53,8 @@ export interface SimpleDbSchemaConverter {
* See PersistencePromise for more details.
*/
export class SimpleDb {
/**
* Opens the specified database, creating or upgrading it if necessary.
*
* Note that `version` must not be a downgrade. IndexedDB does not support downgrading the schema
* version. We currently do not support any way to do versioning outside of IndexedDB's versioning
* mechanism, as only version-upgrade transactions are allowed to do things like create
* objectstores.
*/
static openOrCreate(
name: string,
version: number,
schemaConverter: SimpleDbSchemaConverter
): Promise<SimpleDb> {
debugAssert(
SimpleDb.isAvailable(),
'IndexedDB not supported in current environment.'
);
logDebug(LOG_TAG, 'Opening database:', name);
return new PersistencePromise<SimpleDb>((resolve, reject) => {
// TODO(mikelehen): Investigate browser compatibility.
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
// suggests IE9 and older WebKit browsers handle upgrade
// differently. They expect setVersion, as described here:
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
const request = indexedDB.open(name, version);

request.onsuccess = (event: Event) => {
const db = (event.target as IDBOpenDBRequest).result;
resolve(new SimpleDb(db));
};

request.onblocked = () => {
reject(
new FirestoreError(
Code.FAILED_PRECONDITION,
'Cannot upgrade IndexedDB schema while another tab is open. ' +
'Close all tabs that access Firestore and reload this page to proceed.'
)
);
};

request.onerror = (event: Event) => {
const error: DOMException = (event.target as IDBOpenDBRequest).error!;
if (error.name === 'VersionError') {
reject(
new FirestoreError(
Code.FAILED_PRECONDITION,
'A newer version of the Firestore SDK was previously used and so the persisted ' +
'data is not compatible with the version of the SDK you are now using. The SDK ' +
'will operate with persistence disabled. If you need persistence, please ' +
're-upgrade to a newer version of the SDK or else clear the persisted IndexedDB ' +
'data for your app to start fresh.'
)
);
} else {
reject(error);
}
};

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
logDebug(
LOG_TAG,
'Database "' + name + '" requires upgrade from version:',
event.oldVersion
);
const db = (event.target as IDBOpenDBRequest).result;
schemaConverter
.createOrUpgrade(
db,
request.transaction!,
event.oldVersion,
SCHEMA_VERSION
)
.next(() => {
logDebug(
LOG_TAG,
'Database upgrade to version ' + SCHEMA_VERSION + ' complete'
);
});
};
}).toPromise();
}
private db?: IDBDatabase;
private versionchangelistener?: (event: IDBVersionChangeEvent) => void;

/** Deletes the specified database. */
static delete(name: string): Promise<void> {
Expand Down Expand Up @@ -233,7 +152,25 @@ export class SimpleDb {
return Number(version);
}

constructor(private db: IDBDatabase) {
/*
* Creates a new SimpleDb wrapper for IndexedDb database `name`.
*
* Note that `version` must not be a downgrade. IndexedDB does not support
* downgrading the schema version. We currently do not support any way to do
* versioning outside of IndexedDB's versioning mechanism, as only
* version-upgrade transactions are allowed to do things like create
* objectstores.
*/
constructor(
private readonly name: string,
private readonly version: number,
private readonly schemaConverter: SimpleDbSchemaConverter
) {
debugAssert(
SimpleDb.isAvailable(),
'IndexedDB not supported in current environment.'
);

const iOSVersion = SimpleDb.getIOSVersion(getUA());
// NOTE: According to https://bugs.webkit.org/show_bug.cgi?id=197050, the
// bug we're checking for should exist in iOS >= 12.2 and < 13, but for
Expand All @@ -249,12 +186,91 @@ export class SimpleDb {
}
}

/**
* Opens the specified database, creating or upgrading it if necessary.
*/
async ensureDb(): Promise<IDBDatabase> {
if (!this.db) {
logDebug(LOG_TAG, 'Opening database:', this.name);
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
// TODO(mikelehen): Investigate browser compatibility.
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
// suggests IE9 and older WebKit browsers handle upgrade
// differently. They expect setVersion, as described here:
// https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeRequest/setVersion
const request = indexedDB.open(this.name, this.version);

request.onsuccess = (event: Event) => {
const db = (event.target as IDBOpenDBRequest).result;
resolve(db);
};

request.onblocked = () => {
reject(
new IndexedDbTransactionError(
'Cannot upgrade IndexedDB schema while another tab is open. ' +
'Close all tabs that access Firestore and reload this page to proceed.'
)
);
};

request.onerror = (event: Event) => {
const error: DOMException = (event.target as IDBOpenDBRequest).error!;
if (error.name === 'VersionError') {
reject(
new FirestoreError(
Code.FAILED_PRECONDITION,
'A newer version of the Firestore SDK was previously used and so the persisted ' +
'data is not compatible with the version of the SDK you are now using. The SDK ' +
'will operate with persistence disabled. If you need persistence, please ' +
're-upgrade to a newer version of the SDK or else clear the persisted IndexedDB ' +
'data for your app to start fresh.'
)
);
} else {
reject(new IndexedDbTransactionError(error));
}
};

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
logDebug(
LOG_TAG,
'Database "' + this.name + '" requires upgrade from version:',
event.oldVersion
);
const db = (event.target as IDBOpenDBRequest).result;
this.schemaConverter
.createOrUpgrade(
db,
request.transaction!,
event.oldVersion,
this.version
)
.next(() => {
logDebug(
LOG_TAG,
'Database upgrade to version ' + this.version + ' complete'
);
});
};
});
}

if (this.versionchangelistener) {
this.db.onversionchange = event => this.versionchangelistener!(event);
}
return this.db;
}

setVersionChangeListener(
versionChangeListener: (event: IDBVersionChangeEvent) => void
): void {
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
return versionChangeListener(event);
};
this.versionchangelistener = versionChangeListener;
if (this.db) {
this.db.onversionchange = (event: IDBVersionChangeEvent) => {
return versionChangeListener(event);
};
}
}

async runTransaction<T>(
Expand All @@ -268,12 +284,14 @@ export class SimpleDb {
while (true) {
++attemptNumber;

const transaction = SimpleDbTransaction.open(
this.db,
readonly ? 'readonly' : 'readwrite',
objectStores
);
try {
this.db = await this.ensureDb();

const transaction = SimpleDbTransaction.open(
this.db,
readonly ? 'readonly' : 'readwrite',
objectStores
);
const transactionFnResult = transactionFn(transaction)
.catch(error => {
// Abort the transaction if there was an error.
Expand Down Expand Up @@ -312,6 +330,8 @@ export class SimpleDb {
retryable
);

this.close();

if (!retryable) {
return Promise.reject(error);
}
Expand All @@ -320,7 +340,10 @@ export class SimpleDb {
}

close(): void {
this.db.close();
if (this.db) {
this.db.close();
}
this.db = undefined;
}
}

Expand Down Expand Up @@ -400,7 +423,7 @@ export interface IterateOptions {
export class IndexedDbTransactionError extends FirestoreError {
name = 'IndexedDbTransactionError';

constructor(cause: Error) {
constructor(cause: Error | string) {
super(Code.UNAVAILABLE, 'IndexedDB transaction failed: ' + cause);
}
}
Expand Down Expand Up @@ -429,7 +452,11 @@ export class SimpleDbTransaction {
mode: IDBTransactionMode,
objectStoreNames: string[]
): SimpleDbTransaction {
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
try {
return new SimpleDbTransaction(db.transaction(objectStoreNames, mode));
} catch (e) {
throw new IndexedDbTransactionError(e);
}
}

constructor(private readonly transaction: IDBTransaction) {
Expand Down
15 changes: 3 additions & 12 deletions packages/firestore/test/unit/local/encoded_resource_path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,9 @@ describe('EncodedResourcePath', () => {

const dbName = 'resource-path-tests';

beforeEach(() => {
return SimpleDb.delete(dbName)
.then(() => {
return SimpleDb.openOrCreate(
dbName,
1,
new EncodedResourcePathSchemaConverter()
);
})
.then(simpleDb => {
db = simpleDb;
});
beforeEach(async () => {
await SimpleDb.delete(dbName);
db = new SimpleDb(dbName, 1, new EncodedResourcePathSchemaConverter());
});

afterEach(() => {
Expand Down

0 comments on commit 36be62a

Please sign in to comment.