2018-02-11 23:37:21 +00:00
|
|
|
/**
|
|
|
|
* @flow
|
|
|
|
* Firestore Transaction representation wrapper
|
|
|
|
*/
|
|
|
|
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
|
|
|
|
import { getLogger } from '../../utils/log';
|
|
|
|
import { getNativeModule } from '../../utils/native';
|
|
|
|
import Transaction from './Transaction';
|
|
|
|
import type Firestore from './';
|
|
|
|
|
|
|
|
let transactionId = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uses the push id generator to create a transaction id
|
|
|
|
* @returns {number}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
const generateTransactionId = (): number => transactionId++;
|
|
|
|
|
|
|
|
export type TransactionMeta = {
|
|
|
|
id: number,
|
|
|
|
stack: Array<string>,
|
|
|
|
reject: null | Function,
|
|
|
|
resolve: null | Function,
|
|
|
|
transaction: Transaction,
|
|
|
|
updateFunction: (transaction: Transaction) => Promise<any>,
|
|
|
|
};
|
|
|
|
|
|
|
|
type TransactionEvent = {
|
|
|
|
id: number,
|
|
|
|
type: 'update' | 'error' | 'complete',
|
|
|
|
error: ?{ code: string, message: string },
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @class TransactionHandler
|
|
|
|
*/
|
|
|
|
export default class TransactionHandler {
|
|
|
|
_firestore: Firestore;
|
|
|
|
_pending: { [number]: TransactionMeta };
|
|
|
|
|
|
|
|
constructor(firestore: Firestore) {
|
|
|
|
this._pending = {};
|
|
|
|
this._firestore = firestore;
|
2018-02-13 13:58:23 +00:00
|
|
|
SharedEventEmitter.addListener(
|
2018-02-11 23:37:21 +00:00
|
|
|
getAppEventName(this._firestore, 'firestore_transaction_event'),
|
|
|
|
this._handleTransactionEvent.bind(this)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* -------------
|
|
|
|
* INTERNAL API
|
|
|
|
* -------------
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a new transaction and start it natively.
|
|
|
|
* @param updateFunction
|
|
|
|
*/
|
|
|
|
_add(
|
|
|
|
updateFunction: (transaction: Transaction) => Promise<any>
|
|
|
|
): Promise<any> {
|
|
|
|
const id = generateTransactionId();
|
|
|
|
const meta = {
|
|
|
|
id,
|
|
|
|
reject: null,
|
|
|
|
resolve: null,
|
|
|
|
updateFunction,
|
2018-02-23 03:02:17 +00:00
|
|
|
stack: new Error().stack
|
|
|
|
.split('\n')
|
|
|
|
.slice(4)
|
|
|
|
.join('\n'),
|
2018-02-11 23:37:21 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
meta.transaction = new Transaction(this._firestore, meta);
|
|
|
|
this._pending[id] = meta;
|
|
|
|
|
|
|
|
// deferred promise
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
getNativeModule(this._firestore).transactionBegin(id);
|
|
|
|
meta.resolve = r => {
|
|
|
|
resolve(r);
|
|
|
|
this._remove(id);
|
|
|
|
};
|
|
|
|
meta.reject = e => {
|
|
|
|
reject(e);
|
|
|
|
this._remove(id);
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroys a local instance of a transaction meta
|
|
|
|
*
|
|
|
|
* @param id
|
|
|
|
* @private
|
|
|
|
*/
|
2018-02-23 03:02:17 +00:00
|
|
|
_remove(id) {
|
2018-02-11 23:37:21 +00:00
|
|
|
// todo confirm pending arg no longer needed
|
2018-02-23 03:02:17 +00:00
|
|
|
getNativeModule(this._firestore).transactionDispose(id);
|
2018-02-11 23:37:21 +00:00
|
|
|
// TODO may need delaying to next event loop
|
|
|
|
delete this._pending[id];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* -------------
|
|
|
|
* EVENTS
|
|
|
|
* -------------
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles incoming native transaction events and distributes to correct
|
|
|
|
* internal handler by event.type
|
|
|
|
*
|
|
|
|
* @param event
|
|
|
|
* @returns {*}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_handleTransactionEvent(event: TransactionEvent) {
|
|
|
|
switch (event.type) {
|
|
|
|
case 'update':
|
|
|
|
return this._handleUpdate(event);
|
|
|
|
case 'error':
|
|
|
|
return this._handleError(event);
|
|
|
|
case 'complete':
|
|
|
|
return this._handleComplete(event);
|
|
|
|
default:
|
|
|
|
getLogger(this._firestore).warn(
|
|
|
|
`Unknown transaction event type: '${event.type}'`,
|
|
|
|
event
|
|
|
|
);
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles incoming native transaction update events
|
|
|
|
*
|
|
|
|
* @param event
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
async _handleUpdate(event: TransactionEvent) {
|
|
|
|
const { id } = event;
|
|
|
|
// abort if no longer exists js side
|
|
|
|
if (!this._pending[id]) return this._remove(id);
|
|
|
|
|
|
|
|
const { updateFunction, transaction, reject } = this._pending[id];
|
|
|
|
|
|
|
|
// clear any saved state from previous transaction runs
|
|
|
|
transaction._prepare();
|
|
|
|
|
|
|
|
let finalError;
|
|
|
|
let updateFailed;
|
|
|
|
let pendingResult;
|
|
|
|
|
|
|
|
// run the users custom update functionality
|
|
|
|
try {
|
|
|
|
const possiblePromise = updateFunction(transaction);
|
|
|
|
|
|
|
|
// validate user has returned a promise in their update function
|
|
|
|
// TODO must it actually return a promise? Can't find any usages of it without one...
|
|
|
|
if (!possiblePromise || !possiblePromise.then) {
|
|
|
|
finalError = new Error(
|
|
|
|
'Update function for `firestore.runTransaction(updateFunction)` must return a Promise.'
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
pendingResult = await possiblePromise;
|
|
|
|
}
|
|
|
|
} catch (exception) {
|
2018-02-23 03:02:17 +00:00
|
|
|
// exception can still be falsey if user `Promise.reject();` 's with no args
|
|
|
|
// so we track the exception with a updateFailed boolean to ensure no fall-through
|
|
|
|
updateFailed = true;
|
2018-02-11 23:37:21 +00:00
|
|
|
finalError = exception;
|
|
|
|
}
|
|
|
|
|
|
|
|
// reject the final promise and remove from native
|
2018-02-23 03:02:17 +00:00
|
|
|
// update is failed when either the users updateFunction
|
|
|
|
// throws an error or rejects a promise
|
2018-02-11 23:37:21 +00:00
|
|
|
if (updateFailed) {
|
|
|
|
return reject(finalError);
|
|
|
|
}
|
|
|
|
|
|
|
|
// capture the resolved result as we'll need this
|
|
|
|
// to resolve the runTransaction() promise when
|
|
|
|
// native emits that the transaction is final
|
|
|
|
transaction._pendingResult = pendingResult;
|
|
|
|
|
|
|
|
// send the buffered update/set/delete commands for native to process
|
2018-02-23 03:02:17 +00:00
|
|
|
return getNativeModule(this._firestore).transactionApplyBuffer(
|
2018-02-11 23:37:21 +00:00
|
|
|
id,
|
|
|
|
transaction._commandBuffer
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles incoming native transaction error events
|
|
|
|
*
|
|
|
|
* @param event
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_handleError(event: TransactionEvent) {
|
|
|
|
const { id, error } = event;
|
|
|
|
const meta = this._pending[id];
|
|
|
|
|
|
|
|
if (meta) {
|
|
|
|
const { code, message } = error;
|
|
|
|
// build a JS error and replace its stack
|
|
|
|
// with the captured one at start of transaction
|
|
|
|
// so it's actually relevant to the user
|
|
|
|
const errorWithStack = new Error(message);
|
|
|
|
errorWithStack.code = code;
|
|
|
|
errorWithStack.stack = meta.stack;
|
|
|
|
|
|
|
|
meta.reject(errorWithStack);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles incoming native transaction complete events
|
|
|
|
*
|
|
|
|
* @param event
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_handleComplete(event: TransactionEvent) {
|
|
|
|
const { id } = event;
|
|
|
|
const meta = this._pending[id];
|
|
|
|
|
|
|
|
if (meta) {
|
|
|
|
const pendingResult = meta.transaction._pendingResult;
|
|
|
|
meta.resolve(pendingResult);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|