mirror of
https://github.com/hcengineering/platform.git
synced 2025-03-11 15:45:43 +00:00
254 lines
7.6 KiB
TypeScript
254 lines
7.6 KiB
TypeScript
![]() |
import core, {
|
||
|
AttachedDoc,
|
||
|
Class,
|
||
|
Client,
|
||
|
Doc,
|
||
|
DocumentUpdate,
|
||
|
Hierarchy,
|
||
|
Ref,
|
||
|
SortingOrder,
|
||
|
Tx,
|
||
|
TxCollectionCUD,
|
||
|
TxCreateDoc,
|
||
|
TxCUD,
|
||
|
TxProcessor,
|
||
|
TxUpdateDoc
|
||
|
} from '@anticrm/core'
|
||
|
import { createQuery, LiveQuery } from '@anticrm/presentation'
|
||
|
|
||
|
/**
|
||
|
* @public
|
||
|
*/
|
||
|
export type ActivityKey = string
|
||
|
|
||
|
/**
|
||
|
* @public
|
||
|
*/
|
||
|
export function activityKey (objectClass: Ref<Class<Doc>>, txClass: Ref<Class<Tx>>): ActivityKey {
|
||
|
return objectClass + ':' + txClass
|
||
|
}
|
||
|
|
||
|
function isEqualOps (op1: DocumentUpdate<Doc>, op2: DocumentUpdate<Doc>): boolean {
|
||
|
const o1 = Object.keys(op1).sort().join('-')
|
||
|
const o2 = Object.keys(op2).sort().join('-')
|
||
|
return o1 === o2
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transaction being displayed.
|
||
|
* @public
|
||
|
*/
|
||
|
export interface DisplayTx {
|
||
|
// Source tx
|
||
|
tx: TxCUD<Doc>
|
||
|
|
||
|
// A set of collapsed transactions.
|
||
|
txes: Array<TxCUD<Doc>>
|
||
|
|
||
|
// type check for createTx
|
||
|
createTx?: TxCreateDoc<Doc>
|
||
|
|
||
|
// Type check for updateTx
|
||
|
updateTx?: TxUpdateDoc<Doc>
|
||
|
|
||
|
// Document in case it is required.
|
||
|
doc?: Doc
|
||
|
|
||
|
removed: boolean
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @public
|
||
|
*/
|
||
|
export type DisplayTxListener = (txes: DisplayTx[]) => void
|
||
|
|
||
|
// Use 5 minutes to combine similar transactions.
|
||
|
const combineThreshold = 5 * 60 * 1000
|
||
|
/**
|
||
|
* Define activity.
|
||
|
*
|
||
|
* Allow to recieve a list of transactions and notify client about it.
|
||
|
*/
|
||
|
export interface Activity {
|
||
|
update: (object: Doc, listener: DisplayTxListener, sort: SortingOrder) => void
|
||
|
}
|
||
|
|
||
|
class ActivityImpl implements Activity {
|
||
|
private readonly txQuery1: LiveQuery
|
||
|
private readonly txQuery2: LiveQuery
|
||
|
|
||
|
private txes1: Array<TxCUD<Doc>> = []
|
||
|
private txes2: Array<TxCUD<Doc>> = []
|
||
|
constructor (readonly client: Client) {
|
||
|
this.txQuery1 = createQuery()
|
||
|
this.txQuery2 = createQuery()
|
||
|
}
|
||
|
|
||
|
private notify (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void {
|
||
|
this.combineTransactions(object, this.txes1, this.txes2).then(
|
||
|
(result) => {
|
||
|
const sorted = result.sort((a, b) => (a.tx.modifiedOn - b.tx.modifiedOn) * sort)
|
||
|
listener(sorted)
|
||
|
},
|
||
|
(err) => {
|
||
|
console.error(err)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
|
||
|
update (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void {
|
||
|
let isAttached = false
|
||
|
|
||
|
isAttached = this.client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)
|
||
|
|
||
|
this.txQuery1.query<TxCollectionCUD<Doc, AttachedDoc>>(
|
||
|
isAttached ? core.class.TxCollectionCUD : core.class.TxCUD,
|
||
|
isAttached
|
||
|
? { 'tx.objectId': object._id as Ref<AttachedDoc> }
|
||
|
: {
|
||
|
objectId: object._id,
|
||
|
_class: { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
|
||
|
},
|
||
|
(result) => {
|
||
|
this.txes1 = result
|
||
|
this.notify(object, listener, sort)
|
||
|
},
|
||
|
{ sort: { modifiedOn: SortingOrder.Descending } }
|
||
|
)
|
||
|
|
||
|
this.txQuery2.query<TxCUD<Doc>>(
|
||
|
core.class.TxCollectionCUD,
|
||
|
{
|
||
|
objectId: object._id,
|
||
|
'tx._class': { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
|
||
|
},
|
||
|
(result) => {
|
||
|
this.txes2 = result
|
||
|
this.notify(object, listener, sort)
|
||
|
},
|
||
|
{ sort: { modifiedOn: SortingOrder.Descending } }
|
||
|
)
|
||
|
}
|
||
|
|
||
|
async combineTransactions (object: Doc, txes1: Array<TxCUD<Doc>>, txes2: Array<TxCUD<Doc>>): Promise<DisplayTx[]> {
|
||
|
const hierarchy = this.client.getHierarchy()
|
||
|
|
||
|
// We need to sort with with natural order, to build a proper doc values.
|
||
|
const allTx = Array.from(txes1).concat(txes2).sort(this.sortByLastModified)
|
||
|
const txCUD: Array<TxCUD<Doc>> = this.filterTxCUD(allTx, hierarchy)
|
||
|
const parents = new Map<Ref<Doc>, DisplayTx>()
|
||
|
|
||
|
const results: DisplayTx[] = []
|
||
|
|
||
|
for (const tx of txCUD) {
|
||
|
const { collectionCUD, updateCUD, result, tx: ntx } = this.createDisplayTx(tx, parents)
|
||
|
|
||
|
// We do not need collection object updates, in main list of displayed transactions.
|
||
|
if (this.isDisplayTxRequired(collectionCUD, updateCUD, ntx, object)) {
|
||
|
// Combine previous update transaction for same field and if same operation and time treshold is ok
|
||
|
this.checkIntegratePreviousTx(results, result)
|
||
|
results.push(result)
|
||
|
}
|
||
|
}
|
||
|
return Array.from(results)
|
||
|
}
|
||
|
|
||
|
sortByLastModified (a: TxCUD<Doc>, b: TxCUD<Doc>): number {
|
||
|
return a.modifiedOn - b.modifiedOn
|
||
|
}
|
||
|
|
||
|
isDisplayTxRequired (collectionCUD: boolean, updateCUD: boolean, ntx: TxCUD<Doc>, object: Doc): boolean {
|
||
|
return !(collectionCUD && updateCUD) || ntx.objectId === object._id
|
||
|
}
|
||
|
|
||
|
filterTxCUD (allTx: Array<TxCUD<Doc>>, hierarchy: Hierarchy): Array<TxCUD<Doc>> {
|
||
|
return allTx.filter((tx) => hierarchy.isDerived(tx._class, core.class.TxCUD))
|
||
|
}
|
||
|
|
||
|
createDisplayTx (
|
||
|
tx: TxCUD<Doc>,
|
||
|
parents: Map<Ref<Doc>, DisplayTx>
|
||
|
): { collectionCUD: boolean, updateCUD: boolean, result: DisplayTx, tx: TxCUD<Doc> } {
|
||
|
let collectionCUD = false
|
||
|
let updateCUD = false
|
||
|
const hierarchy = this.client.getHierarchy()
|
||
|
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||
|
tx = (tx as TxCollectionCUD<Doc, AttachedDoc>).tx
|
||
|
collectionCUD = true
|
||
|
}
|
||
|
let firstTx = parents.get(tx.objectId)
|
||
|
const result: DisplayTx = this.newDisplayTx(tx)
|
||
|
|
||
|
result.doc = firstTx?.doc ?? result.doc
|
||
|
|
||
|
firstTx = firstTx ?? result
|
||
|
parents.set(tx.objectId, firstTx)
|
||
|
|
||
|
// If we have updates also apply them all.
|
||
|
updateCUD = this.updateFirstTx(result, firstTx)
|
||
|
|
||
|
if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc) && result.doc !== undefined) {
|
||
|
firstTx.removed = true
|
||
|
}
|
||
|
return { collectionCUD, updateCUD, result, tx }
|
||
|
}
|
||
|
|
||
|
updateFirstTx (result: DisplayTx, firstTx: DisplayTx): boolean {
|
||
|
if (this.client.getHierarchy().isDerived(result.tx._class, core.class.TxUpdateDoc) && result.doc !== undefined) {
|
||
|
firstTx.doc = TxProcessor.updateDoc2Doc(result.doc, result.tx as TxUpdateDoc<Doc>)
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
newDisplayTx (tx: TxCUD<Doc>): DisplayTx {
|
||
|
const hierarchy = this.client.getHierarchy()
|
||
|
const createTx = hierarchy.isDerived(tx._class, core.class.TxCreateDoc) ? (tx as TxCreateDoc<Doc>) : undefined
|
||
|
return {
|
||
|
tx,
|
||
|
txes: [],
|
||
|
createTx,
|
||
|
updateTx: hierarchy.isDerived(tx._class, core.class.TxUpdateDoc) ? (tx as TxUpdateDoc<Doc>) : undefined,
|
||
|
removed: false,
|
||
|
doc: createTx !== undefined ? TxProcessor.createDoc2Doc(createTx) : undefined
|
||
|
}
|
||
|
}
|
||
|
|
||
|
checkIntegratePreviousTx (results: DisplayTx[], result: DisplayTx): void {
|
||
|
if (results.length > 0) {
|
||
|
const prevTx = results[results.length - 1]
|
||
|
if (this.isSameKindTx(prevTx, result)) {
|
||
|
const prevUpdate = prevTx.tx as unknown as TxUpdateDoc<Doc>
|
||
|
const curUpdate = result.tx as unknown as TxUpdateDoc<Doc>
|
||
|
if (
|
||
|
isEqualOps(prevUpdate.operations, curUpdate.operations) &&
|
||
|
result.tx.modifiedOn - prevUpdate.modifiedOn < combineThreshold
|
||
|
) {
|
||
|
// we have same keys, l
|
||
|
// Remember previous transactions
|
||
|
result.txes.push(...prevTx.txes, prevTx.tx)
|
||
|
// Remove last item
|
||
|
results.splice(results.length - 1, 1)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
isSameKindTx (prevTx: DisplayTx, result: DisplayTx): boolean {
|
||
|
return (
|
||
|
prevTx.tx.objectId === result.tx.objectId && // Same document id
|
||
|
prevTx.tx._class === result.tx._class && // Same transaction class
|
||
|
result.tx._class === core.class.TxUpdateDoc &&
|
||
|
prevTx.tx.modifiedBy === result.tx.modifiedBy // Same user
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Construct an new activity, to listend for displayed transactions in UI.
|
||
|
* @param client
|
||
|
*/
|
||
|
export function newActivity (client: Client): Activity {
|
||
|
return new ActivityImpl(client)
|
||
|
}
|