Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-open IndexedDB if closed #3535

Merged
merged 6 commits into from
Aug 5, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Re-open IndexedDB if closed
  • Loading branch information
schmidt-sebastian committed Aug 4, 2020
commit e8c09bb40f5088c1cf3bb26f864a42219cfd699a
87 changes: 40 additions & 47 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 @@ -288,54 +290,45 @@ export class IndexedDbPersistence implements Persistence {
*
* @return {Promise<void>} Whether persistence was enabled.
*/
start(): Promise<void> {
async start(): Promise<void> {
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();
})
.then(() => {
if (!this.isPrimary && !this.allowTabSynchronization) {
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
// obtain the primary lease.
throw new FirestoreError(
Code.FAILED_PRECONDITION,
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
);
}
this.attachVisibilityHandler();
this.attachWindowUnloadHook();
try {
await this.simpleDb.ensureDb();

// 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.
await this.updateClientMetadataAndTryBecomePrimary();

if (!this.isPrimary && !this.allowTabSynchronization) {
// Fail `start()` if `synchronizeTabs` is disabled and we cannot
// obtain the primary lease.
throw new FirestoreError(
Code.FAILED_PRECONDITION,
PRIMARY_LEASE_EXCLUSIVE_ERROR_MSG
);
}
this.attachVisibilityHandler();
this.attachWindowUnloadHook();

this.scheduleClientMetadataAndPrimaryLeaseRefreshes();
this.scheduleClientMetadataAndPrimaryLeaseRefreshes();

return this.runTransaction(
'getHighestListenSequenceNumber',
'readonly',
txn => this.targetCache.getHighestSequenceNumber(txn)
);
})
.then(highestListenSequenceNumber => {
this.listenSequence = new ListenSequence(
highestListenSequenceNumber,
this.sequenceNumberSyncer
);
})
.then(() => {
this._started = true;
})
.catch(reason => {
this.simpleDb && this.simpleDb.close();
return Promise.reject(reason);
});
const highestListenSequenceNumber = await this.runTransaction(
'getHighestListenSequenceNumber',
'readonly',
txn => this.targetCache.getHighestSequenceNumber(txn)
);

this.listenSequence = new ListenSequence(
highestListenSequenceNumber,
this.sequenceNumberSyncer
);

this._started = true;
} finally {
this.simpleDb.close();
}
}

/**
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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly a copy of the code above, but this line changed from FirestoreError to 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
Loading