[firestore][js] initial draft of firestore transactions RNFB JS api
This commit is contained in:
parent
4c11dbfcce
commit
4c0b1639ab
|
@ -0,0 +1,179 @@
|
||||||
|
/**
|
||||||
|
* @flow
|
||||||
|
* Firestore Transaction representation wrapper
|
||||||
|
*/
|
||||||
|
import { mergeFieldPathData } from './utils';
|
||||||
|
import { buildNativeMap } from './utils/serialize';
|
||||||
|
|
||||||
|
import type Firestore from './';
|
||||||
|
import type { TransactionMeta } from './TransactionHandler';
|
||||||
|
import type DocumentReference from './DocumentReference';
|
||||||
|
import type DocumentSnapshot from './DocumentSnapshot';
|
||||||
|
import { isObject, isString } from '../../utils';
|
||||||
|
import FieldPath from './FieldPath';
|
||||||
|
import { getNativeModule } from '../../utils/native';
|
||||||
|
|
||||||
|
type Command = {
|
||||||
|
type: 'set' | 'update' | 'delete',
|
||||||
|
path: string,
|
||||||
|
data: ?{ [string]: any },
|
||||||
|
options: ?{ merge: boolean },
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetOptions = {
|
||||||
|
merge: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class Transaction
|
||||||
|
*/
|
||||||
|
export default class Transaction {
|
||||||
|
_pendingResult: ?any;
|
||||||
|
_firestore: Firestore;
|
||||||
|
_meta: TransactionMeta;
|
||||||
|
_commandBuffer: Array<Command>;
|
||||||
|
|
||||||
|
constructor(firestore: Firestore, meta: TransactionMeta) {
|
||||||
|
this._meta = meta;
|
||||||
|
this._commandBuffer = [];
|
||||||
|
this._firestore = firestore;
|
||||||
|
this._pendingResult = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -------------
|
||||||
|
* INTERNAL API
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the command buffer and any pending result in prep for
|
||||||
|
* the next transaction iteration attempt.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepare() {
|
||||||
|
this._commandBuffer = [];
|
||||||
|
this._pendingResult = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -------------
|
||||||
|
* PUBLIC API
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the document referenced by the provided DocumentReference.
|
||||||
|
*
|
||||||
|
* @param documentRef DocumentReference A reference to the document to be retrieved. Value must not be null.
|
||||||
|
*
|
||||||
|
* @returns Promise<DocumentSnapshot>
|
||||||
|
*/
|
||||||
|
get(documentRef: DocumentReference): Promise<DocumentSnapshot> {
|
||||||
|
// todo validate doc ref
|
||||||
|
return getNativeModule(this._firestore).transactionGetDocument(
|
||||||
|
this._meta.id,
|
||||||
|
documentRef.path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to the document referred to by the provided DocumentReference.
|
||||||
|
* If the document does not exist yet, it will be created. If you pass options,
|
||||||
|
* the provided data can be merged into the existing document.
|
||||||
|
*
|
||||||
|
* @param documentRef DocumentReference A reference to the document to be created. Value must not be null.
|
||||||
|
* @param data Object An object of the fields and values for the document.
|
||||||
|
* @param options SetOptions An object to configure the set behavior.
|
||||||
|
* Pass {merge: true} to only replace the values specified in the data argument.
|
||||||
|
* Fields omitted will remain untouched.
|
||||||
|
*
|
||||||
|
* @returns {Transaction}
|
||||||
|
*/
|
||||||
|
set(
|
||||||
|
documentRef: DocumentReference,
|
||||||
|
data: Object,
|
||||||
|
options?: SetOptions
|
||||||
|
): Transaction {
|
||||||
|
// todo validate doc ref
|
||||||
|
// todo validate data is object
|
||||||
|
this._commandBuffer.push({
|
||||||
|
type: 'set',
|
||||||
|
path: documentRef.path,
|
||||||
|
data: buildNativeMap(data),
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates fields in the document referred to by this DocumentReference.
|
||||||
|
* The update will fail if applied to a document that does not exist. Nested
|
||||||
|
* fields can be updated by providing dot-separated field path strings or by providing FieldPath objects.
|
||||||
|
*
|
||||||
|
* @param documentRef DocumentReference A reference to the document to be updated. Value must not be null.
|
||||||
|
* @param args any Either an object containing all of the fields and values to update,
|
||||||
|
* or a series of arguments alternating between fields (as string or FieldPath
|
||||||
|
* objects) and values.
|
||||||
|
*
|
||||||
|
* @returns {Transaction}
|
||||||
|
*/
|
||||||
|
update(documentRef: DocumentReference, ...args: Array<any>): Transaction {
|
||||||
|
// todo validate doc ref
|
||||||
|
let data = {};
|
||||||
|
if (args.length === 1) {
|
||||||
|
if (!isObject(args[0])) {
|
||||||
|
throw new Error(
|
||||||
|
'Transaction.update failed: If using a single data argument, it must be an object.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data] = args;
|
||||||
|
} else if (args.length % 2 === 1) {
|
||||||
|
throw new Error(
|
||||||
|
'Transaction.update failed: Must have either a single object data argument, or equal numbers of data key/value pairs.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < args.length; i += 2) {
|
||||||
|
const key = args[i];
|
||||||
|
const value = args[i + 1];
|
||||||
|
if (isString(key)) {
|
||||||
|
data[key] = value;
|
||||||
|
} else if (key instanceof FieldPath) {
|
||||||
|
data = mergeFieldPathData(data, key._segments, value);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Transaction.update failed: Argument at index ${i} must be a string or FieldPath`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._commandBuffer.push({
|
||||||
|
type: 'update',
|
||||||
|
path: documentRef.path,
|
||||||
|
data: buildNativeMap(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the document referred to by the provided DocumentReference.
|
||||||
|
*
|
||||||
|
* @param documentRef DocumentReference A reference to the document to be deleted. Value must not be null.
|
||||||
|
*
|
||||||
|
* @returns {Transaction}
|
||||||
|
*/
|
||||||
|
delete(documentRef: DocumentReference): Transaction {
|
||||||
|
// todo validate doc ref
|
||||||
|
this._commandBuffer.push({
|
||||||
|
type: 'delete',
|
||||||
|
path: documentRef.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
_transactionListener: Function;
|
||||||
|
_pending: { [number]: TransactionMeta };
|
||||||
|
|
||||||
|
constructor(firestore: Firestore) {
|
||||||
|
this._pending = {};
|
||||||
|
this._firestore = firestore;
|
||||||
|
this._transactionListener = SharedEventEmitter.addListener(
|
||||||
|
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,
|
||||||
|
stack: new Error().stack.slice(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
* @param pendingAbort Notify native that there's still an transaction in
|
||||||
|
* progress that needs aborting - this is to handle a JS side
|
||||||
|
* exception
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_remove(id, pendingAbort = false) {
|
||||||
|
// todo confirm pending arg no longer needed
|
||||||
|
getNativeModule(this._firestore).transactionDispose(id, pendingAbort);
|
||||||
|
// 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) {
|
||||||
|
updateFailed = true; // in case the user rejects with nothing
|
||||||
|
finalError = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reject the final promise and remove from native
|
||||||
|
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
|
||||||
|
return getNativeModule(this._firestore).transactionProcessUpdateResponse(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ import FieldValue from './FieldValue';
|
||||||
import GeoPoint from './GeoPoint';
|
import GeoPoint from './GeoPoint';
|
||||||
import Path from './Path';
|
import Path from './Path';
|
||||||
import WriteBatch from './WriteBatch';
|
import WriteBatch from './WriteBatch';
|
||||||
|
import TransactionHandler from './TransactionHandler';
|
||||||
|
import Transaction from './Transaction';
|
||||||
import INTERNALS from '../../utils/internals';
|
import INTERNALS from '../../utils/internals';
|
||||||
|
|
||||||
import type DocumentSnapshot from './DocumentSnapshot';
|
import type DocumentSnapshot from './DocumentSnapshot';
|
||||||
|
@ -36,8 +38,9 @@ type DocumentSyncEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const NATIVE_EVENTS = [
|
const NATIVE_EVENTS = [
|
||||||
'firestore_collection_sync_event',
|
'firestore_transaction_event',
|
||||||
'firestore_document_sync_event',
|
'firestore_document_sync_event',
|
||||||
|
'firestore_collection_sync_event',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MODULE_NAME = 'RNFirebaseFirestore';
|
export const MODULE_NAME = 'RNFirebaseFirestore';
|
||||||
|
@ -48,6 +51,7 @@ export const NAMESPACE = 'firestore';
|
||||||
*/
|
*/
|
||||||
export default class Firestore extends ModuleBase {
|
export default class Firestore extends ModuleBase {
|
||||||
_referencePath: Path;
|
_referencePath: Path;
|
||||||
|
_transactionHandler: TransactionHandler;
|
||||||
|
|
||||||
constructor(app: App) {
|
constructor(app: App) {
|
||||||
super(app, {
|
super(app, {
|
||||||
|
@ -56,7 +60,9 @@ export default class Firestore extends ModuleBase {
|
||||||
multiApp: true,
|
multiApp: true,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._referencePath = new Path([]);
|
this._referencePath = new Path([]);
|
||||||
|
this._transactionHandler = new TransactionHandler(this);
|
||||||
|
|
||||||
SharedEventEmitter.addListener(
|
SharedEventEmitter.addListener(
|
||||||
// sub to internal native event - this fans out to
|
// sub to internal native event - this fans out to
|
||||||
|
@ -73,11 +79,23 @@ export default class Firestore extends ModuleBase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -------------
|
||||||
|
* PUBLIC API
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a write batch, used for performing multiple writes as a single atomic operation.
|
||||||
|
*
|
||||||
|
* @returns {WriteBatch}
|
||||||
|
*/
|
||||||
batch(): WriteBatch {
|
batch(): WriteBatch {
|
||||||
return new WriteBatch(this);
|
return new WriteBatch(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Gets a CollectionReference instance that refers to the collection at the specified path.
|
||||||
*
|
*
|
||||||
* @param collectionPath
|
* @param collectionPath
|
||||||
* @returns {CollectionReference}
|
* @returns {CollectionReference}
|
||||||
|
@ -92,6 +110,7 @@ export default class Firestore extends ModuleBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Gets a DocumentReference instance that refers to the document at the specified path.
|
||||||
*
|
*
|
||||||
* @param documentPath
|
* @param documentPath
|
||||||
* @returns {DocumentReference}
|
* @returns {DocumentReference}
|
||||||
|
@ -105,13 +124,27 @@ export default class Firestore extends ModuleBase {
|
||||||
return new DocumentReference(this, path);
|
return new DocumentReference(this, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
enablePersistence(): Promise<void> {
|
/**
|
||||||
throw new Error('Persistence is enabled by default on the Firestore SDKs');
|
* Executes the given updateFunction and then attempts to commit the
|
||||||
|
* changes applied within the transaction. If any document read within
|
||||||
|
* the transaction has changed, Cloud Firestore retries the updateFunction.
|
||||||
|
*
|
||||||
|
* If it fails to commit after 5 attempts, the transaction fails.
|
||||||
|
*
|
||||||
|
* @param updateFunction
|
||||||
|
* @returns {void|Promise<any>}
|
||||||
|
*/
|
||||||
|
runTransaction(
|
||||||
|
updateFunction: (transaction: Transaction) => Promise<any>
|
||||||
|
): Promise<any> {
|
||||||
|
return this._transactionHandler._add(updateFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
runTransaction(): Promise<any> {
|
/**
|
||||||
throw new Error('firebase.firestore().runTransaction() coming soon');
|
* -------------
|
||||||
}
|
* UNSUPPORTED
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
setLogLevel(): void {
|
setLogLevel(): void {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -121,13 +154,45 @@ export default class Firestore extends ModuleBase {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
enableNetwork(): void {
|
||||||
|
throw new Error(
|
||||||
|
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
|
||||||
|
'firestore',
|
||||||
|
'enableNetwork'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
disableNetwork(): void {
|
||||||
|
throw new Error(
|
||||||
|
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
|
||||||
|
'firestore',
|
||||||
|
'disableNetwork'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -------------
|
||||||
|
* MISC
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
enablePersistence(): Promise<void> {
|
||||||
|
throw new Error('Persistence is enabled by default on the Firestore SDKs');
|
||||||
|
}
|
||||||
settings(): void {
|
settings(): void {
|
||||||
throw new Error('firebase.firestore().settings() coming soon');
|
throw new Error('firebase.firestore().settings() coming soon');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -------------
|
||||||
|
* INTERNALS
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal collection sync listener
|
* Internal collection sync listener
|
||||||
|
*
|
||||||
* @param event
|
* @param event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -147,6 +212,7 @@ export default class Firestore extends ModuleBase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal document sync listener
|
* Internal document sync listener
|
||||||
|
*
|
||||||
* @param event
|
* @param event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue