mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-29 09:09:15 +03:00
uberf-7389: instant transactions (#5941)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
parent
51383caa92
commit
6af082e66f
@ -41,6 +41,7 @@
|
||||
"@hcengineering/model-notification": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/model-workbench": "^0.6.1",
|
||||
"@hcengineering/model-presentation": "^0.6.0",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
type ObjectChatPanel,
|
||||
type ThreadMessage
|
||||
} from '@hcengineering/chunter'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
import contact from '@hcengineering/contact'
|
||||
import {
|
||||
type Class,
|
||||
@ -457,6 +458,10 @@ export function createModel (builder: Builder): void {
|
||||
presenter: chunter.component.ChatMessagePresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ChatMessage, core.class.Class, presentation.mixin.InstantTransactions, {
|
||||
txClasses: [core.class.TxCreateDoc]
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: chunter.component.ThreadMessagePresenter
|
||||
})
|
||||
|
@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { DOMAIN_MODEL, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
|
||||
import core, { TDoc } from '@hcengineering/model-core'
|
||||
import { DOMAIN_MODEL, type Tx, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { Mixin, Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
|
||||
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
||||
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
// Import types to prevent .svelte components to being exposed to type typescript.
|
||||
import {
|
||||
@ -33,7 +33,8 @@ import {
|
||||
type FilePreviewExtension,
|
||||
type ObjectSearchCategory,
|
||||
type ObjectSearchContext,
|
||||
type ObjectSearchFactory
|
||||
type ObjectSearchFactory,
|
||||
type InstantTransactions
|
||||
} from '@hcengineering/presentation/src/types'
|
||||
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
|
||||
import presentation from './plugin'
|
||||
@ -94,6 +95,11 @@ export class TFilePreviewExtension extends TComponentPointExtension implements F
|
||||
availabilityChecker?: Resource<() => Promise<boolean>>
|
||||
}
|
||||
|
||||
@Mixin(presentation.mixin.InstantTransactions, core.class.Class)
|
||||
export class TInstantTransactions extends TClass implements InstantTransactions {
|
||||
txClasses!: Array<Ref<Class<Tx>>>
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TObjectSearchCategory,
|
||||
@ -101,6 +107,7 @@ export function createModel (builder: Builder): void {
|
||||
TComponentPointExtension,
|
||||
TDocCreateExtension,
|
||||
TDocRules,
|
||||
TFilePreviewExtension
|
||||
TFilePreviewExtension,
|
||||
TInstantTransactions
|
||||
)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Class, type Ref } from '@hcengineering/core'
|
||||
import { type Mixin, type Class, type Ref } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import { type ComponentExtensionId } from '@hcengineering/ui'
|
||||
@ -24,7 +24,8 @@ import {
|
||||
type DocRules,
|
||||
type DocCreateExtension,
|
||||
type FilePreviewExtension,
|
||||
type ObjectSearchCategory
|
||||
type ObjectSearchCategory,
|
||||
type InstantTransactions
|
||||
} from './types'
|
||||
import type { PreviewConfig } from './preview'
|
||||
|
||||
@ -42,6 +43,9 @@ export default plugin(presentationId, {
|
||||
DocRules: '' as Ref<Class<DocRules>>,
|
||||
FilePreviewExtension: '' as Ref<Class<FilePreviewExtension>>
|
||||
},
|
||||
mixin: {
|
||||
InstantTransactions: '' as Ref<Mixin<InstantTransactions>>
|
||||
},
|
||||
string: {
|
||||
Create: '' as IntlString,
|
||||
Cancel: '' as IntlString,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
type Tx,
|
||||
type Blob,
|
||||
type Class,
|
||||
type Client,
|
||||
@ -183,3 +184,10 @@ export interface FilePreviewExtension extends ComponentPointExtension {
|
||||
// Extension is only available if this checker returns true
|
||||
availabilityChecker?: Resource<() => Promise<boolean>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface InstantTransactions extends Class<Doc> {
|
||||
txClasses: Array<Ref<Class<Tx>>>
|
||||
}
|
||||
|
@ -17,9 +17,11 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import core, {
|
||||
TxOperations,
|
||||
TxProcessor,
|
||||
concatLink,
|
||||
getCurrentAccount,
|
||||
reduceCalls,
|
||||
type TxApplyIf,
|
||||
type AnyAttribute,
|
||||
type ArrOf,
|
||||
type AttachedDoc,
|
||||
@ -46,7 +48,8 @@ import core, {
|
||||
type Tx,
|
||||
type TxResult,
|
||||
type TypeAny,
|
||||
type WithLookup
|
||||
type WithLookup,
|
||||
type TxCUD
|
||||
} from '@hcengineering/core'
|
||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||
import { LiveQuery as LQ } from '@hcengineering/query'
|
||||
@ -54,14 +57,14 @@ import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteCo
|
||||
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { type Writable, get, writable } from 'svelte/store'
|
||||
import { type KeyedAttribute } from '..'
|
||||
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
|
||||
import plugin from './plugin'
|
||||
export { reduceCalls } from '@hcengineering/core'
|
||||
|
||||
let liveQuery: LQ
|
||||
let client: TxOperations & MeasureClient
|
||||
let client: TxOperations & MeasureClient & OptimisticTxes
|
||||
let pipeline: PresentationPipeline
|
||||
|
||||
const txListeners: Array<(...tx: Tx[]) => void> = []
|
||||
@ -83,7 +86,11 @@ export function removeTxListener (l: (tx: Tx) => void): void {
|
||||
}
|
||||
}
|
||||
|
||||
class UIClient extends TxOperations implements Client, MeasureClient {
|
||||
export interface OptimisticTxes {
|
||||
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
|
||||
}
|
||||
|
||||
class UIClient extends TxOperations implements Client, MeasureClient, OptimisticTxes {
|
||||
constructor (
|
||||
client: MeasureClient,
|
||||
private readonly liveQuery: Client
|
||||
@ -93,23 +100,56 @@ class UIClient extends TxOperations implements Client, MeasureClient {
|
||||
|
||||
afterMeasure: Tx[] = []
|
||||
measureOp?: MeasureDoneOperation
|
||||
protected pendingTxes = new Set<Ref<Tx>>()
|
||||
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
|
||||
|
||||
get pendingCreatedDocs (): typeof this._pendingCreatedDocs {
|
||||
return this._pendingCreatedDocs
|
||||
}
|
||||
|
||||
async doNotify (...tx: Tx[]): Promise<void> {
|
||||
if (this.measureOp !== undefined) {
|
||||
this.afterMeasure.push(...tx)
|
||||
} else {
|
||||
try {
|
||||
await pipeline.notifyTx(...tx)
|
||||
const pending = get(this._pendingCreatedDocs)
|
||||
let pendingUpdated = false
|
||||
tx.forEach((t) => {
|
||||
if (this.pendingTxes.has(t._id)) {
|
||||
this.pendingTxes.delete(t._id)
|
||||
|
||||
await liveQuery.tx(...tx)
|
||||
// Only CUD tx can be pending now
|
||||
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
|
||||
|
||||
txListeners.forEach((it) => {
|
||||
it(...tx)
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.log(err)
|
||||
if (innerTx._class === core.class.TxCreateDoc) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete pending[innerTx.objectId]
|
||||
pendingUpdated = true
|
||||
}
|
||||
}
|
||||
})
|
||||
if (pendingUpdated) {
|
||||
this._pendingCreatedDocs.set(pending)
|
||||
}
|
||||
|
||||
// We still want to notify about all transactions because there might be queries created after
|
||||
// the early applied transaction
|
||||
// For old queries there's a check anyway that prevents the same document from being added twice
|
||||
await this.provideNotify(...tx)
|
||||
}
|
||||
}
|
||||
|
||||
private async provideNotify (...tx: Tx[]): Promise<void> {
|
||||
try {
|
||||
await pipeline.notifyTx(...tx)
|
||||
|
||||
await liveQuery.tx(...tx)
|
||||
|
||||
txListeners.forEach((it) => {
|
||||
it(...tx)
|
||||
})
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,9 +170,49 @@ class UIClient extends TxOperations implements Client, MeasureClient {
|
||||
}
|
||||
|
||||
override async tx (tx: Tx): Promise<TxResult> {
|
||||
void this.notifyEarly(tx)
|
||||
|
||||
return await this.client.tx(tx)
|
||||
}
|
||||
|
||||
private async notifyEarly (tx: Tx): Promise<void> {
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
|
||||
if (applyTx.match.length !== 0 || applyTx.notMatch.length !== 0) {
|
||||
// Cannot early apply conditional transactions
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
applyTx.txes.map(async (atx) => {
|
||||
await this.notifyEarly(atx)
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.getHierarchy().isDerived(tx._class, core.class.TxCUD)) {
|
||||
return
|
||||
}
|
||||
|
||||
const innerTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
// Can pre-build some configuration later from the model if this will be too slow.
|
||||
const instantTxes = this.getHierarchy().classHierarchyMixin(innerTx.objectClass, plugin.mixin.InstantTransactions)
|
||||
if (instantTxes?.txClasses.includes(innerTx._class) !== true) {
|
||||
return
|
||||
}
|
||||
|
||||
if (innerTx._class === core.class.TxCreateDoc) {
|
||||
const pending = get(this._pendingCreatedDocs)
|
||||
pending[innerTx.objectId] = true
|
||||
this._pendingCreatedDocs.set(pending)
|
||||
}
|
||||
|
||||
this.pendingTxes.add(tx._id)
|
||||
await this.provideNotify(tx)
|
||||
}
|
||||
|
||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||
return await this.client.searchFulltext(query, options)
|
||||
}
|
||||
@ -159,7 +239,7 @@ class UIClient extends TxOperations implements Client, MeasureClient {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getClient (): TxOperations & MeasureClient {
|
||||
export function getClient (): TxOperations & MeasureClient & OptimisticTxes {
|
||||
return client
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,8 @@
|
||||
export let hideFooter = false
|
||||
export let skipLabel = false
|
||||
export let hoverable = true
|
||||
export let pending = false
|
||||
export let stale = false
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let showDatePreposition = false
|
||||
export let type: ActivityMessageViewType = 'default'
|
||||
@ -158,6 +160,7 @@
|
||||
class:actionsOpened={isActionsOpened}
|
||||
class:borderedHover={hoverStyles === 'borderedHover'}
|
||||
class:filledHover={hoverStyles === 'filledHover'}
|
||||
class:stale
|
||||
on:click={onClick}
|
||||
on:contextmenu={handleContextMenu}
|
||||
>
|
||||
@ -226,7 +229,7 @@
|
||||
</div>
|
||||
|
||||
{#if withActions && !readonly}
|
||||
<div class="actions" class:opened={isActionsOpened}>
|
||||
<div class="actions" class:pending class:opened={isActionsOpened}>
|
||||
<ActivityMessageActions
|
||||
message={isReactionMessage(message) ? parentMessage : message}
|
||||
{actions}
|
||||
@ -282,13 +285,15 @@
|
||||
top: -0.75rem;
|
||||
right: 0.75rem;
|
||||
|
||||
&.opened {
|
||||
&.opened:not(.pending) {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .actions {
|
||||
visibility: visible;
|
||||
&:not(.pending) {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .time {
|
||||
@ -324,6 +329,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.stale {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -100,8 +100,11 @@
|
||||
}
|
||||
|
||||
async function onMessage (event: CustomEvent) {
|
||||
loading = true
|
||||
const doneOp = await getClient().measure(`chunter.create.${_class} ${object._class}`)
|
||||
if (chatMessage) {
|
||||
loading = true
|
||||
} // for new messages we use instant txes
|
||||
|
||||
const doneOp = getClient().measure(`chunter.create.${_class} ${object._class}`)
|
||||
try {
|
||||
draftController.remove()
|
||||
inputRef.removeDraft(false)
|
||||
@ -116,11 +119,11 @@
|
||||
currentMessage = getDefault()
|
||||
_id = currentMessage._id
|
||||
const d1 = Date.now()
|
||||
void doneOp().then((res) => {
|
||||
void (await doneOp)().then((res) => {
|
||||
console.log(`create.${_class} measure`, res, Date.now() - d1)
|
||||
})
|
||||
} catch (err: any) {
|
||||
void doneOp()
|
||||
void (await doneOp)()
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
|
@ -54,8 +54,9 @@
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const { pendingCreatedDocs } = client
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const STALE_TIMEOUT_MS = 5000
|
||||
const userQuery = createQuery()
|
||||
const currentAccount = getCurrentAccount()
|
||||
|
||||
@ -103,6 +104,21 @@
|
||||
|
||||
$: links = showLinksPreview ? getLinks(value?.message) : []
|
||||
|
||||
let stale = false
|
||||
let markStaleId: NodeJS.Timeout | undefined
|
||||
$: pending = value?._id !== undefined && $pendingCreatedDocs[value._id]
|
||||
$: if (pending) {
|
||||
markStaleId = setTimeout(() => {
|
||||
stale = true
|
||||
}, STALE_TIMEOUT_MS)
|
||||
} else {
|
||||
if (markStaleId !== undefined) {
|
||||
clearTimeout(markStaleId)
|
||||
markStaleId = undefined
|
||||
}
|
||||
stale = false
|
||||
}
|
||||
|
||||
function getLinks (content?: string): HTMLLinkElement[] {
|
||||
if (!content) {
|
||||
return []
|
||||
@ -185,6 +201,8 @@
|
||||
{hoverable}
|
||||
{hoverStyles}
|
||||
{skipLabel}
|
||||
{pending}
|
||||
{stale}
|
||||
showDatePreposition={hideLink}
|
||||
{type}
|
||||
{onClick}
|
||||
|
Loading…
Reference in New Issue
Block a user