Skip to content

Commit

Permalink
uberf-7389: instant transactions (#5941)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexey Zinoviev <[email protected]>
  • Loading branch information
lexiv0re authored Jun 28, 2024
1 parent 51383ca commit 6af082e
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 30 deletions.
1 change: 1 addition & 0 deletions models/chunter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions models/chunter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
})
Expand Down
17 changes: 12 additions & 5 deletions models/presentation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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'
Expand Down Expand Up @@ -94,13 +95,19 @@ 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,
TPresentationMiddlewareFactory,
TComponentPointExtension,
TDocCreateExtension,
TDocRules,
TFilePreviewExtension
TFilePreviewExtension,
TInstantTransactions
)
}
8 changes: 6 additions & 2 deletions packages/presentation/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,7 +24,8 @@ import {
type DocRules,
type DocCreateExtension,
type FilePreviewExtension,
type ObjectSearchCategory
type ObjectSearchCategory,
type InstantTransactions
} from './types'
import type { PreviewConfig } from './preview'

Expand All @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions packages/presentation/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type Tx,
type Blob,
type Class,
type Client,
Expand Down Expand Up @@ -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>>>
}
110 changes: 95 additions & 15 deletions packages/presentation/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import { Analytics } from '@hcengineering/analytics'
import core, {
TxOperations,
TxProcessor,
concatLink,
getCurrentAccount,
reduceCalls,
type TxApplyIf,
type AnyAttribute,
type ArrOf,
type AttachedDoc,
Expand All @@ -46,22 +48,23 @@ 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'
import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteComponent } from '@hcengineering/ui'
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> = []
Expand All @@ -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
Expand All @@ -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)

// Only CUD tx can be pending now
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>

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)
}

await liveQuery.tx(...tx)
// 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)
}
}

txListeners.forEach((it) => {
it(...tx)
})
} catch (err: any) {
Analytics.handleError(err)
console.log(err)
}
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)
}
}

Expand All @@ -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)
}
Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -158,6 +160,7 @@
class:actionsOpened={isActionsOpened}
class:borderedHover={hoverStyles === 'borderedHover'}
class:filledHover={hoverStyles === 'filledHover'}
class:stale
on:click={onClick}
on:contextmenu={handleContextMenu}
>
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -324,6 +329,10 @@
}
}
}
&.stale {
opacity: 0.5;
}
}
.header {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit 6af082e

Please sign in to comment.