Skip to content

Commit 6af082e

Browse files
authored
uberf-7389: instant transactions (#5941)
Signed-off-by: Alexey Zinoviev <[email protected]>
1 parent 51383ca commit 6af082e

File tree

9 files changed

+165
-30
lines changed

9 files changed

+165
-30
lines changed

models/chunter/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@hcengineering/model-notification": "^0.6.0",
4242
"@hcengineering/model-view": "^0.6.0",
4343
"@hcengineering/model-workbench": "^0.6.1",
44+
"@hcengineering/model-presentation": "^0.6.0",
4445
"@hcengineering/notification": "^0.6.23",
4546
"@hcengineering/platform": "^0.6.11",
4647
"@hcengineering/ui": "^0.6.15",

models/chunter/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type ObjectChatPanel,
2525
type ThreadMessage
2626
} from '@hcengineering/chunter'
27+
import presentation from '@hcengineering/model-presentation'
2728
import contact from '@hcengineering/contact'
2829
import {
2930
type Class,
@@ -457,6 +458,10 @@ export function createModel (builder: Builder): void {
457458
presenter: chunter.component.ChatMessagePresenter
458459
})
459460

461+
builder.mixin(chunter.class.ChatMessage, core.class.Class, presentation.mixin.InstantTransactions, {
462+
txClasses: [core.class.TxCreateDoc]
463+
})
464+
460465
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
461466
presenter: chunter.component.ThreadMessagePresenter
462467
})

models/presentation/src/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
// limitations under the License.
1414
//
1515

16-
import { DOMAIN_MODEL, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
17-
import { Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
18-
import core, { TDoc } from '@hcengineering/model-core'
16+
import { DOMAIN_MODEL, type Tx, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
17+
import { Mixin, Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
18+
import core, { TClass, TDoc } from '@hcengineering/model-core'
1919
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
2020
// Import types to prevent .svelte components to being exposed to type typescript.
2121
import {
@@ -33,7 +33,8 @@ import {
3333
type FilePreviewExtension,
3434
type ObjectSearchCategory,
3535
type ObjectSearchContext,
36-
type ObjectSearchFactory
36+
type ObjectSearchFactory,
37+
type InstantTransactions
3738
} from '@hcengineering/presentation/src/types'
3839
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
3940
import presentation from './plugin'
@@ -94,13 +95,19 @@ export class TFilePreviewExtension extends TComponentPointExtension implements F
9495
availabilityChecker?: Resource<() => Promise<boolean>>
9596
}
9697

98+
@Mixin(presentation.mixin.InstantTransactions, core.class.Class)
99+
export class TInstantTransactions extends TClass implements InstantTransactions {
100+
txClasses!: Array<Ref<Class<Tx>>>
101+
}
102+
97103
export function createModel (builder: Builder): void {
98104
builder.createModel(
99105
TObjectSearchCategory,
100106
TPresentationMiddlewareFactory,
101107
TComponentPointExtension,
102108
TDocCreateExtension,
103109
TDocRules,
104-
TFilePreviewExtension
110+
TFilePreviewExtension,
111+
TInstantTransactions
105112
)
106113
}

packages/presentation/src/plugin.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// limitations under the License.
1515
//
1616

17-
import { type Class, type Ref } from '@hcengineering/core'
17+
import { type Mixin, type Class, type Ref } from '@hcengineering/core'
1818
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
1919
import { plugin } from '@hcengineering/platform'
2020
import { type ComponentExtensionId } from '@hcengineering/ui'
@@ -24,7 +24,8 @@ import {
2424
type DocRules,
2525
type DocCreateExtension,
2626
type FilePreviewExtension,
27-
type ObjectSearchCategory
27+
type ObjectSearchCategory,
28+
type InstantTransactions
2829
} from './types'
2930
import type { PreviewConfig } from './preview'
3031

@@ -42,6 +43,9 @@ export default plugin(presentationId, {
4243
DocRules: '' as Ref<Class<DocRules>>,
4344
FilePreviewExtension: '' as Ref<Class<FilePreviewExtension>>
4445
},
46+
mixin: {
47+
InstantTransactions: '' as Ref<Mixin<InstantTransactions>>
48+
},
4549
string: {
4650
Create: '' as IntlString,
4751
Cancel: '' as IntlString,

packages/presentation/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type Tx,
23
type Blob,
34
type Class,
45
type Client,
@@ -183,3 +184,10 @@ export interface FilePreviewExtension extends ComponentPointExtension {
183184
// Extension is only available if this checker returns true
184185
availabilityChecker?: Resource<() => Promise<boolean>>
185186
}
187+
188+
/**
189+
* @public
190+
*/
191+
export interface InstantTransactions extends Class<Doc> {
192+
txClasses: Array<Ref<Class<Tx>>>
193+
}

packages/presentation/src/utils.ts

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import { Analytics } from '@hcengineering/analytics'
1818
import core, {
1919
TxOperations,
20+
TxProcessor,
2021
concatLink,
2122
getCurrentAccount,
2223
reduceCalls,
24+
type TxApplyIf,
2325
type AnyAttribute,
2426
type ArrOf,
2527
type AttachedDoc,
@@ -46,22 +48,23 @@ import core, {
4648
type Tx,
4749
type TxResult,
4850
type TypeAny,
49-
type WithLookup
51+
type WithLookup,
52+
type TxCUD
5053
} from '@hcengineering/core'
5154
import { getMetadata, getResource } from '@hcengineering/platform'
5255
import { LiveQuery as LQ } from '@hcengineering/query'
5356
import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteComponent } from '@hcengineering/ui'
5457
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
5558
import { deepEqual } from 'fast-equals'
5659
import { onDestroy } from 'svelte'
57-
import { get } from 'svelte/store'
60+
import { type Writable, get, writable } from 'svelte/store'
5861
import { type KeyedAttribute } from '..'
5962
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
6063
import plugin from './plugin'
6164
export { reduceCalls } from '@hcengineering/core'
6265

6366
let liveQuery: LQ
64-
let client: TxOperations & MeasureClient
67+
let client: TxOperations & MeasureClient & OptimisticTxes
6568
let pipeline: PresentationPipeline
6669

6770
const txListeners: Array<(...tx: Tx[]) => void> = []
@@ -83,7 +86,11 @@ export function removeTxListener (l: (tx: Tx) => void): void {
8386
}
8487
}
8588

86-
class UIClient extends TxOperations implements Client, MeasureClient {
89+
export interface OptimisticTxes {
90+
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
91+
}
92+
93+
class UIClient extends TxOperations implements Client, MeasureClient, OptimisticTxes {
8794
constructor (
8895
client: MeasureClient,
8996
private readonly liveQuery: Client
@@ -93,23 +100,56 @@ class UIClient extends TxOperations implements Client, MeasureClient {
93100

94101
afterMeasure: Tx[] = []
95102
measureOp?: MeasureDoneOperation
103+
protected pendingTxes = new Set<Ref<Tx>>()
104+
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
105+
106+
get pendingCreatedDocs (): typeof this._pendingCreatedDocs {
107+
return this._pendingCreatedDocs
108+
}
96109

97110
async doNotify (...tx: Tx[]): Promise<void> {
98111
if (this.measureOp !== undefined) {
99112
this.afterMeasure.push(...tx)
100113
} else {
101-
try {
102-
await pipeline.notifyTx(...tx)
114+
const pending = get(this._pendingCreatedDocs)
115+
let pendingUpdated = false
116+
tx.forEach((t) => {
117+
if (this.pendingTxes.has(t._id)) {
118+
this.pendingTxes.delete(t._id)
119+
120+
// Only CUD tx can be pending now
121+
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
122+
123+
if (innerTx._class === core.class.TxCreateDoc) {
124+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
125+
delete pending[innerTx.objectId]
126+
pendingUpdated = true
127+
}
128+
}
129+
})
130+
if (pendingUpdated) {
131+
this._pendingCreatedDocs.set(pending)
132+
}
103133

104-
await liveQuery.tx(...tx)
134+
// We still want to notify about all transactions because there might be queries created after
135+
// the early applied transaction
136+
// For old queries there's a check anyway that prevents the same document from being added twice
137+
await this.provideNotify(...tx)
138+
}
139+
}
105140

106-
txListeners.forEach((it) => {
107-
it(...tx)
108-
})
109-
} catch (err: any) {
110-
Analytics.handleError(err)
111-
console.log(err)
112-
}
141+
private async provideNotify (...tx: Tx[]): Promise<void> {
142+
try {
143+
await pipeline.notifyTx(...tx)
144+
145+
await liveQuery.tx(...tx)
146+
147+
txListeners.forEach((it) => {
148+
it(...tx)
149+
})
150+
} catch (err: any) {
151+
Analytics.handleError(err)
152+
console.log(err)
113153
}
114154
}
115155

@@ -130,9 +170,49 @@ class UIClient extends TxOperations implements Client, MeasureClient {
130170
}
131171

132172
override async tx (tx: Tx): Promise<TxResult> {
173+
void this.notifyEarly(tx)
174+
133175
return await this.client.tx(tx)
134176
}
135177

178+
private async notifyEarly (tx: Tx): Promise<void> {
179+
if (tx._class === core.class.TxApplyIf) {
180+
const applyTx = tx as TxApplyIf
181+
182+
if (applyTx.match.length !== 0 || applyTx.notMatch.length !== 0) {
183+
// Cannot early apply conditional transactions
184+
return
185+
}
186+
187+
await Promise.all(
188+
applyTx.txes.map(async (atx) => {
189+
await this.notifyEarly(atx)
190+
})
191+
)
192+
return
193+
}
194+
195+
if (!this.getHierarchy().isDerived(tx._class, core.class.TxCUD)) {
196+
return
197+
}
198+
199+
const innerTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
200+
// Can pre-build some configuration later from the model if this will be too slow.
201+
const instantTxes = this.getHierarchy().classHierarchyMixin(innerTx.objectClass, plugin.mixin.InstantTransactions)
202+
if (instantTxes?.txClasses.includes(innerTx._class) !== true) {
203+
return
204+
}
205+
206+
if (innerTx._class === core.class.TxCreateDoc) {
207+
const pending = get(this._pendingCreatedDocs)
208+
pending[innerTx.objectId] = true
209+
this._pendingCreatedDocs.set(pending)
210+
}
211+
212+
this.pendingTxes.add(tx._id)
213+
await this.provideNotify(tx)
214+
}
215+
136216
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
137217
return await this.client.searchFulltext(query, options)
138218
}
@@ -159,7 +239,7 @@ class UIClient extends TxOperations implements Client, MeasureClient {
159239
/**
160240
* @public
161241
*/
162-
export function getClient (): TxOperations & MeasureClient {
242+
export function getClient (): TxOperations & MeasureClient & OptimisticTxes {
163243
return client
164244
}
165245

plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
export let hideFooter = false
5050
export let skipLabel = false
5151
export let hoverable = true
52+
export let pending = false
53+
export let stale = false
5254
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
5355
export let showDatePreposition = false
5456
export let type: ActivityMessageViewType = 'default'
@@ -158,6 +160,7 @@
158160
class:actionsOpened={isActionsOpened}
159161
class:borderedHover={hoverStyles === 'borderedHover'}
160162
class:filledHover={hoverStyles === 'filledHover'}
163+
class:stale
161164
on:click={onClick}
162165
on:contextmenu={handleContextMenu}
163166
>
@@ -226,7 +229,7 @@
226229
</div>
227230

228231
{#if withActions && !readonly}
229-
<div class="actions" class:opened={isActionsOpened}>
232+
<div class="actions" class:pending class:opened={isActionsOpened}>
230233
<ActivityMessageActions
231234
message={isReactionMessage(message) ? parentMessage : message}
232235
{actions}
@@ -282,13 +285,15 @@
282285
top: -0.75rem;
283286
right: 0.75rem;
284287
285-
&.opened {
288+
&.opened:not(.pending) {
286289
visibility: visible;
287290
}
288291
}
289292
290293
&:hover > .actions {
291-
visibility: visible;
294+
&:not(.pending) {
295+
visibility: visible;
296+
}
292297
}
293298
294299
&:hover > .time {
@@ -324,6 +329,10 @@
324329
}
325330
}
326331
}
332+
333+
&.stale {
334+
opacity: 0.5;
335+
}
327336
}
328337
329338
.header {

plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,11 @@
100100
}
101101
102102
async function onMessage (event: CustomEvent) {
103-
loading = true
104-
const doneOp = await getClient().measure(`chunter.create.${_class} ${object._class}`)
103+
if (chatMessage) {
104+
loading = true
105+
} // for new messages we use instant txes
106+
107+
const doneOp = getClient().measure(`chunter.create.${_class} ${object._class}`)
105108
try {
106109
draftController.remove()
107110
inputRef.removeDraft(false)
@@ -116,11 +119,11 @@
116119
currentMessage = getDefault()
117120
_id = currentMessage._id
118121
const d1 = Date.now()
119-
void doneOp().then((res) => {
122+
void (await doneOp)().then((res) => {
120123
console.log(`create.${_class} measure`, res, Date.now() - d1)
121124
})
122125
} catch (err: any) {
123-
void doneOp()
126+
void (await doneOp)()
124127
Analytics.handleError(err)
125128
console.error(err)
126129
}

0 commit comments

Comments
 (0)