UBERF-7532: Bulk operations for triggers (#6023)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-07-09 00:04:05 +07:00 committed by GitHub
parent fa82ee1939
commit 4eac1927f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 656 additions and 693 deletions

View File

@ -13,11 +13,11 @@
// limitations under the License.
//
import { addLocation } from '@hcengineering/platform'
import { devModelId } from '@hcengineering/devmodel'
import { PresentationClientHook } from '@hcengineering/devmodel-resources'
import login from '@hcengineering/login'
import { setMetadata } from '@hcengineering/platform'
import devmodel, { devModelId } from '@hcengineering/devmodel'
import client from '@hcengineering/client'
import { addLocation, setMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
export function configurePlatformDevServer() {
console.log('Use Endpoint override:', process.env.LOGIN_ENDPOINT)
@ -28,6 +28,6 @@ export function configurePlatformDevServer() {
}
function enableDevModel() {
setMetadata(client.metadata.ClientHook, devmodel.hook.Hook)
setMetadata(presentation.metadata.ClientHook, new PresentationClientHook())
addLocation(devModelId, () => import(/* webpackChunkName: "devmodel" */ '@hcengineering/devmodel-resources'))
}

View File

@ -122,9 +122,6 @@ describe('client', () => {
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
loadModel: async (last: Timestamp) => clone(txes),
getAccount: async () => null as unknown as Account,
measure: async () => {
return async () => ({ time: 0, serverTime: 0 })
},
sendForceClose: async () => {}
}
}

View File

@ -73,7 +73,6 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
loadModel: async (last: Timestamp) => txes,
getAccount: async () => null as unknown as Account,
measure: async () => async () => ({ time: 0, serverTime: 0 }),
sendForceClose: async () => {}
}
}

View File

@ -47,17 +47,10 @@ export interface Client extends Storage, FulltextStorage {
close: () => Promise<void>
}
export type MeasureDoneOperation = () => Promise<{ time: number, serverTime: number }>
export interface MeasureClient extends Client {
// Will perform on server operation measure and will return a local client time and on server time
measure: (operationName: string) => Promise<MeasureDoneOperation>
}
/**
* @public
*/
export interface AccountClient extends MeasureClient {
export interface AccountClient extends Client {
getAccount: () => Promise<Account>
}
@ -97,11 +90,9 @@ export interface ClientConnection extends Storage, FulltextStorage, BackupClient
// If hash is passed, will return LoadModelResponse
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
getAccount: () => Promise<Account>
measure: (operationName: string) => Promise<MeasureDoneOperation>
}
class ClientImpl implements AccountClient, BackupClient, MeasureClient {
class ClientImpl implements AccountClient, BackupClient {
notify?: (...tx: Tx[]) => void
hierarchy!: Hierarchy
model!: ModelDb
@ -163,10 +154,6 @@ class ClientImpl implements AccountClient, BackupClient, MeasureClient {
return result
}
async measure (operationName: string): Promise<MeasureDoneOperation> {
return await this.conn.measure(operationName)
}
async updateFromRemote (...tx: Tx[]): Promise<void> {
for (const t of tx) {
try {

View File

@ -312,8 +312,8 @@ export class TxOperations implements Omit<Client, 'notify'> {
return this.removeDoc(doc._class, doc.space, doc._id)
}
apply (scope: string): ApplyOperations {
return new ApplyOperations(this, scope)
apply (scope: string, measure?: string): ApplyOperations {
return new ApplyOperations(this, scope, measure)
}
async diffUpdate<T extends Doc = Doc>(
@ -423,6 +423,12 @@ export class TxOperations implements Omit<Client, 'notify'> {
}
}
export interface CommitResult {
result: boolean
time: number
serverTime: number
}
/**
* @public
*
@ -436,7 +442,8 @@ export class ApplyOperations extends TxOperations {
notMatches: DocumentClassQuery<Doc>[] = []
constructor (
readonly ops: TxOperations,
readonly scope: string
readonly scope: string,
readonly measureName?: string
) {
const txClient: Client = {
getHierarchy: () => ops.client.getHierarchy(),
@ -465,23 +472,28 @@ export class ApplyOperations extends TxOperations {
return this
}
async commit (notify: boolean = true, extraNotify: Ref<Class<Doc>>[] = []): Promise<boolean> {
async commit (notify: boolean = true, extraNotify: Ref<Class<Doc>>[] = []): Promise<CommitResult> {
if (this.txes.length > 0) {
return (
await ((await this.ops.tx(
this.ops.txFactory.createTxApplyIf(
core.space.Tx,
this.scope,
this.matches,
this.notMatches,
this.txes,
notify,
extraNotify
)
)) as Promise<TxApplyResult>)
).success
const st = Date.now()
const result = await ((await this.ops.tx(
this.ops.txFactory.createTxApplyIf(
core.space.Tx,
this.scope,
this.matches,
this.notMatches,
this.txes,
this.measureName,
notify,
extraNotify
)
)) as Promise<TxApplyResult>)
return {
result: result.success,
time: Date.now() - st,
serverTime: result.serverTime
}
}
return true
return { result: true, time: 0, serverTime: 0 }
}
}

View File

@ -33,14 +33,15 @@ export interface StorageIterator {
close: (ctx: MeasureContext) => Promise<void>
}
export type BroadcastTargets = Record<string, (tx: Tx) => string[] | undefined>
export interface SessionOperationContext {
ctx: MeasureContext
// A parts of derived data to deal with after operation will be complete
derived: {
derived: Tx[]
target?: string[]
}[]
txes: Tx[]
targets: BroadcastTargets // A set of broadcast filters if required
}
with: <T>(
name: string,
params: ParamsType,

View File

@ -28,13 +28,13 @@ import type {
Space,
Timestamp
} from './classes'
import { clone } from './clone'
import core from './component'
import { setObjectValue } from './objvalue'
import { _getOperator } from './operator'
import { _toDoc } from './proxy'
import type { DocumentQuery, TxResult } from './storage'
import { generateId } from './utils'
import { clone } from './clone'
/**
* @public
@ -137,10 +137,14 @@ export interface TxApplyIf extends Tx {
// If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update
extraNotify?: Ref<Class<Doc>>[]
// If defined will go into a separate measure section
measureName?: string
}
export interface TxApplyResult {
success: boolean
serverTime: number
}
/**
@ -618,6 +622,7 @@ export class TxFactory {
match: DocumentClassQuery<Doc>[],
notMatch: DocumentClassQuery<Doc>[],
txes: TxCUD<Doc>[],
measureName: string | undefined,
notify: boolean = true,
extraNotify: Ref<Class<Doc>>[] = [],
modifiedOn?: Timestamp,
@ -634,6 +639,7 @@ export class TxFactory {
match,
notMatch,
txes,
measureName,
notify,
extraNotify
}

View File

@ -8,8 +8,6 @@ import {
type FindOptions,
type FindResult,
type Hierarchy,
type MeasureClient,
type MeasureDoneOperation,
type ModelDb,
type Ref,
type SearchOptions,
@ -65,7 +63,7 @@ export type PresentationMiddlewareCreator = (client: Client, next?: Presentation
/**
* @public
*/
export interface PresentationPipeline extends MeasureClient, Exclude<PresentationMiddleware, 'next'> {
export interface PresentationPipeline extends Client, Exclude<PresentationMiddleware, 'next'> {
close: () => Promise<void>
}
@ -75,7 +73,7 @@ export interface PresentationPipeline extends MeasureClient, Exclude<Presentatio
export class PresentationPipelineImpl implements PresentationPipeline {
private head: PresentationMiddleware | undefined
private constructor (readonly client: MeasureClient) {}
private constructor (readonly client: Client) {}
getHierarchy (): Hierarchy {
return this.client.getHierarchy()
@ -89,11 +87,7 @@ export class PresentationPipelineImpl implements PresentationPipeline {
await this.head?.notifyTx(...tx)
}
async measure (operationName: string): Promise<MeasureDoneOperation> {
return await this.client.measure(operationName)
}
static create (client: MeasureClient, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
static create (client: Client, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
const pipeline = new PresentationPipelineImpl(client)
pipeline.head = pipeline.buildChain(constructors)
return pipeline

View File

@ -14,26 +14,64 @@
// limitations under the License.
//
import { type Mixin, type Class, type Ref } from '@hcengineering/core'
import {
type Class,
type Client,
type Doc,
type DocumentQuery,
type FindOptions,
type FindResult,
type Mixin,
type Ref,
type SearchOptions,
type SearchQuery,
type SearchResult,
type Tx,
type TxResult,
type WithLookup
} from '@hcengineering/core'
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { type ComponentExtensionId } from '@hcengineering/ui'
import { type PresentationMiddlewareFactory } from './pipeline'
import type { PreviewConfig } from './preview'
import {
type ComponentPointExtension,
type DocRules,
type DocCreateExtension,
type DocRules,
type FilePreviewExtension,
type ObjectSearchCategory,
type InstantTransactions
type InstantTransactions,
type ObjectSearchCategory
} from './types'
import type { PreviewConfig } from './preview'
/**
* @public
*/
export const presentationId = 'presentation' as Plugin
/**
* @public
*/
export interface ClientHook {
findAll: <T extends Doc>(
client: Client,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<FindResult<T>>
findOne: <T extends Doc>(
client: Client,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<WithLookup<T> | undefined>
tx: (client: Client, tx: Tx) => Promise<TxResult>
searchFulltext: (client: Client, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
}
export default plugin(presentationId, {
class: {
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
@ -95,7 +133,8 @@ export default plugin(presentationId, {
CollaboratorApiUrl: '' as Metadata<string>,
Token: '' as Metadata<string>,
FrontUrl: '' as Asset,
PreviewConfig: '' as Metadata<PreviewConfig | undefined>
PreviewConfig: '' as Metadata<PreviewConfig | undefined>,
ClientHook: '' as Metadata<ClientHook>
},
status: {
FileTooLarge: '' as StatusCode

View File

@ -33,8 +33,6 @@ import core, {
type FindOptions,
type FindResult,
type Hierarchy,
type MeasureClient,
type MeasureDoneOperation,
type Mixin,
type Obj,
type Blob as PlatformBlob,
@ -65,7 +63,7 @@ export { reduceCalls } from '@hcengineering/core'
let liveQuery: LQ
let rawLiveQuery: LQ
let client: TxOperations & MeasureClient & OptimisticTxes
let client: TxOperations & Client & OptimisticTxes
let pipeline: PresentationPipeline
const txListeners: Array<(...tx: Tx[]) => void> = []
@ -95,16 +93,15 @@ export interface OptimisticTxes {
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
}
class UIClient extends TxOperations implements Client, MeasureClient, OptimisticTxes {
class UIClient extends TxOperations implements Client, OptimisticTxes {
hook = getMetadata(plugin.metadata.ClientHook)
constructor (
client: MeasureClient,
client: Client,
private readonly liveQuery: Client
) {
super(client, getCurrentAccount()._id)
}
afterMeasure: Tx[] = []
measureOp?: MeasureDoneOperation
protected pendingTxes = new Set<Ref<Tx>>()
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
@ -113,34 +110,30 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
}
async doNotify (...tx: Tx[]): Promise<void> {
if (this.measureOp !== undefined) {
this.afterMeasure.push(...tx)
} else {
const pending = get(this._pendingCreatedDocs)
let pendingUpdated = false
tx.forEach((t) => {
if (this.pendingTxes.has(t._id)) {
this.pendingTxes.delete(t._id)
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>
// 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 (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)
})
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> {
@ -165,6 +158,9 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
if (this.hook !== undefined) {
return await this.hook.findAll(this.liveQuery, _class, query, options)
}
return await this.liveQuery.findAll(_class, query, options)
}
@ -173,12 +169,17 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
if (this.hook !== undefined) {
return await this.hook.findOne(this.liveQuery, _class, query, options)
}
return await this.liveQuery.findOne(_class, query, options)
}
override async tx (tx: Tx): Promise<TxResult> {
void this.notifyEarly(tx)
if (this.hook !== undefined) {
return await this.hook.tx(this.client, tx)
}
return await this.client.tx(tx)
}
@ -221,39 +222,24 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
}
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
return await this.client.searchFulltext(query, options)
}
async measure (operationName: string): Promise<MeasureDoneOperation> {
// return await (this.client as MeasureClient).measure(operationName)
const mop = await (this.client as MeasureClient).measure(operationName)
this.measureOp = mop
return async () => {
const result = await mop()
this.measureOp = undefined
if (this.afterMeasure.length > 0) {
const txes = this.afterMeasure
this.afterMeasure = []
for (const tx of txes) {
await this.doNotify(tx)
}
}
return result
if (this.hook !== undefined) {
return await this.hook.searchFulltext(this.client, query, options)
}
return await this.client.searchFulltext(query, options)
}
}
/**
* @public
*/
export function getClient (): TxOperations & MeasureClient & OptimisticTxes {
export function getClient (): TxOperations & Client & OptimisticTxes {
return client
}
/**
* @public
*/
export async function setClient (_client: MeasureClient): Promise<void> {
export async function setClient (_client: Client): Promise<void> {
if (liveQuery !== undefined) {
await liveQuery.close()
}
@ -276,6 +262,7 @@ export async function setClient (_client: MeasureClient): Promise<void> {
liveQuery = new LQ(pipeline)
const uiClient = new UIClient(pipeline, liveQuery)
client = uiClient
_client.notify = (...tx: Tx[]) => {
@ -285,7 +272,6 @@ export async function setClient (_client: MeasureClient): Promise<void> {
await refreshClient(true)
}
}
/**
* @public
*/

View File

@ -98,7 +98,6 @@ FulltextStorage & {
searchFulltext: async (query: SearchQuery, options: SearchOptions): Promise<SearchResult> => {
return { docs: [] }
},
measure: async () => async () => ({ time: 0, serverTime: 0 }),
sendForceClose: async () => {}
}
}

View File

@ -14,12 +14,12 @@
-->
<script lang="ts">
import { DirectMessage } from '@hcengineering/chunter'
import { Avatar, CombineAvatars, personAccountByIdStore } from '@hcengineering/contact-resources'
import { Icon, IconSize } from '@hcengineering/ui'
import contact, { Person, PersonAccount } from '@hcengineering/contact'
import { classIcon } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import { Avatar, CombineAvatars, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Account, IdMap } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Icon, IconSize } from '@hcengineering/ui'
import { classIcon } from '@hcengineering/view-resources'
import chunter from '../plugin'
import { getDmPersons } from '../utils'
@ -33,10 +33,11 @@
let persons: Person[] = []
$: value &&
getDmPersons(client, value).then((res) => {
$: if (value !== undefined) {
void getDmPersons(client, value, $personByIdStore).then((res) => {
persons = res
})
}
let avatarSize = size

View File

@ -13,15 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import { createEventDispatcher } from 'svelte'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { Class, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
import { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation'
import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { Class, Doc, generateId, getCurrentAccount, Ref, type CommitResult } from '@hcengineering/core'
import { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text-editor'
import { createEventDispatcher } from 'svelte'
export let object: Doc
export let chatMessage: ChatMessage | undefined = undefined
@ -100,32 +100,20 @@
}
async function handleCreate (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
const doneOp = getClient().measure(`chunter.create.${_class} ${object._class}`)
try {
await createMessage(event, _id)
const res = await createMessage(event, _id, `chunter.create.${_class} ${object._class}`)
const d1 = Date.now()
void (await doneOp)().then((res) => {
console.log(`create.${_class} measure`, res, Date.now() - d1)
})
console.log(`create.${_class} measure`, res.serverTime, res.time)
} catch (err: any) {
void (await doneOp)()
Analytics.handleError(err)
console.error(err)
}
}
async function handleEdit (event: CustomEvent): Promise<void> {
const doneOp = getClient().measure(`chunter.edit.${_class} ${object._class}`)
try {
await editMessage(event)
const d1 = Date.now()
void (await doneOp)().then((res) => {
console.log(`edit.${_class} measure`, res, Date.now() - d1)
})
} catch (err: any) {
void (await doneOp)()
Analytics.handleError(err)
console.error(err)
}
@ -148,9 +136,9 @@
loading = false
}
async function createMessage (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
async function createMessage (event: CustomEvent, _id: Ref<ChatMessage>, msg: string): Promise<CommitResult> {
const { message, attachments } = event.detail
const operations = client.apply(_id)
const operations = client.apply(_id, msg)
if (_class === chunter.class.ThreadMessage) {
const parentMessage = object as ActivityMessage
@ -188,7 +176,7 @@
_id
)
}
await operations.commit()
return await operations.commit()
}
async function editMessage (event: CustomEvent): Promise<void> {

View File

@ -13,17 +13,17 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, getCurrentAccount, groupByArray, Ref, SortingOrder } from '@hcengineering/core'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
import activity from '@hcengineering/activity'
import { IntlString } from '@hcengineering/platform'
import { Action } from '@hcengineering/ui'
import { Class, Doc, getCurrentAccount, groupByArray, reduceCalls, Ref, SortingOrder } from '@hcengineering/core'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
import { Action } from '@hcengineering/ui'
import chunter from '../../../plugin'
import { ChatGroup, ChatNavGroupModel } from '../types'
import ChatNavSection from './ChatNavSection.svelte'
import chunter from '../../../plugin'
export let object: Doc | undefined
export let model: ChatNavGroupModel
@ -68,7 +68,11 @@
$: loadObjects(contexts)
$: void getSections(objectsByClass, model, shouldPushObject ? object : undefined).then((res) => {
$: pushObj = shouldPushObject ? object : undefined
const getPushObj = () => pushObj as Doc
$: void getSections(objectsByClass, model, pushObj, getPushObj, (res) => {
sections = res
})
@ -112,63 +116,68 @@
return 'activity'
}
async function getSections (
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
model: ChatNavGroupModel,
object: Doc | undefined
): Promise<Section[]> {
const result: Section[] = []
const getSections = reduceCalls(
async (
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
model: ChatNavGroupModel,
object: { _id: Doc['_id'], _class: Doc['_class'] } | undefined,
getPushObj: () => Doc,
handler: (result: Section[]) => void
): Promise<void> => {
const result: Section[] = []
if (!model.wrap) {
result.push({
id: model.id,
objects: Array.from(objectsByClass.values()).flat(),
label: model.label ?? chunter.string.Channels
})
if (!model.wrap) {
result.push({
id: model.id,
objects: Array.from(objectsByClass.values()).flat(),
label: model.label ?? chunter.string.Channels
})
return result
}
let isObjectPushed = false
if (
Array.from(objectsByClass.values())
.flat()
.some((o) => o._id === object?._id)
) {
isObjectPushed = true
}
for (const [_class, objects] of objectsByClass.entries()) {
const clazz = hierarchy.getClass(_class)
const sectionObjects = [...objects]
if (object && _class === object._class && !objects.some(({ _id }) => _id === object._id)) {
isObjectPushed = true
sectionObjects.push(object)
handler(result)
return
}
result.push({
id: _class,
_class,
objects: sectionObjects,
label: clazz.pluralLabel ?? clazz.label
})
let isObjectPushed = false
if (
Array.from(objectsByClass.values())
.flat()
.some((o) => o._id === object?._id)
) {
isObjectPushed = true
}
for (const [_class, objects] of objectsByClass.entries()) {
const clazz = hierarchy.getClass(_class)
const sectionObjects = [...objects]
if (object !== undefined && _class === object._class && !objects.some(({ _id }) => _id === object._id)) {
isObjectPushed = true
sectionObjects.push(getPushObj())
}
result.push({
id: _class,
_class,
objects: sectionObjects,
label: clazz.pluralLabel ?? clazz.label
})
}
if (!isObjectPushed && object !== undefined) {
const clazz = hierarchy.getClass(object._class)
result.push({
id: object._id,
_class: object._class,
objects: [getPushObj()],
label: clazz.pluralLabel ?? clazz.label
})
}
handler(result.sort((s1, s2) => s1.label.localeCompare(s2.label)))
}
if (!isObjectPushed && object) {
const clazz = hierarchy.getClass(object._class)
result.push({
id: object._id,
_class: object._class,
objects: [object],
label: clazz.pluralLabel ?? clazz.label
})
}
return result.sort((s1, s2) => s1.label.localeCompare(s2.label))
}
)
function getSectionActions (section: Section, contexts: DocNotifyContext[]): Action[] {
if (model.getActionsFn === undefined) {

View File

@ -13,20 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Doc, Ref } from '@hcengineering/core'
import contact from '@hcengineering/contact'
import { personAccountByIdStore, statusByUserStore } from '@hcengineering/contact-resources'
import { Doc, reduceCalls, Ref } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { getResource, IntlString, translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import ui, { Action, AnySvelteComponent, IconSize, ModernButton, NavGroup } from '@hcengineering/ui'
import { getDocTitle } from '@hcengineering/view-resources'
import contact from '@hcengineering/contact'
import { getResource, IntlString, translate } from '@hcengineering/platform'
import view from '@hcengineering/view'
import { personAccountByIdStore, statusByUserStore } from '@hcengineering/contact-resources'
import { getDocTitle } from '@hcengineering/view-resources'
import ChatNavItem from './ChatNavItem.svelte'
import chunter from '../../../plugin'
import { getChannelName, getObjectIcon } from '../../../utils'
import { ChatNavItemModel, SortFnOptions } from '../types'
import { getObjectIcon, getChannelName } from '../../../utils'
import ChatNavItem from './ChatNavItem.svelte'
export let id: string
export let header: IntlString
@ -47,7 +47,7 @@
let canShowMore = false
let isShownMore = false
$: void getChatNavItems(objects).then((res) => {
$: void getChatNavItems(objects, (res) => {
items = res
})
@ -60,44 +60,46 @@
$: visibleItems = getVisibleItems(canShowMore, isShownMore, maxItems, sortedItems, objectId)
async function getChatNavItems (objects: Doc[]): Promise<ChatNavItemModel[]> {
const items: ChatNavItemModel[] = []
const getChatNavItems = reduceCalls(
async (objects: Doc[], handler: (items: ChatNavItemModel[]) => void): Promise<void> => {
const items: ChatNavItemModel[] = []
for (const object of objects) {
const { _class } = object
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
const titleIntl = client.getHierarchy().getClass(_class).label
for (const object of objects) {
const { _class } = object
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
const titleIntl = client.getHierarchy().getClass(_class).label
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
let icon: AnySvelteComponent | undefined = undefined
let icon: AnySvelteComponent | undefined = undefined
if (iconMixin?.component) {
icon = await getResource(iconMixin.component)
if (iconMixin?.component) {
icon = await getResource(iconMixin.component)
}
const hasId = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectIdentifier) !== undefined
const showDescription = hasId && isDocChat && !isPerson
items.push({
id: object._id,
object,
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
description: showDescription ? await getDocTitle(client, object._id, object._class, object) : undefined,
icon: icon ?? getObjectIcon(_class),
iconProps: { showStatus: true },
iconSize,
withIconBackground: !isDirect && !isPerson,
isSecondary: isDocChat && !isPerson
})
}
const hasId = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectIdentifier) !== undefined
const showDescription = hasId && isDocChat && !isPerson
items.push({
id: object._id,
object,
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
description: showDescription ? await getDocTitle(client, object._id, object._class, object) : undefined,
icon: icon ?? getObjectIcon(_class),
iconProps: { showStatus: true },
iconSize,
withIconBackground: !isDirect && !isPerson,
isSecondary: isDocChat && !isPerson
})
handler(items)
}
return items
}
)
function onShowMore (): void {
isShownMore = !isShownMore

View File

@ -110,7 +110,7 @@
/>
</div>
<Scroller shrink>
{#each chatNavGroupModels as model}
{#each chatNavGroupModels as model (model.id)}
<ChatNavGroup {object} {model} on:select />
{/each}
</Scroller>

View File

@ -304,8 +304,7 @@ function getPinnedActions (contexts: DocNotifyContext[]): Action[] {
}
async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
const doneOp = await getClient().measure('unpinAllChannels')
const ops = getClient().apply(generateId())
const ops = getClient().apply(generateId(), 'unpinAllChannels')
try {
for (const context of contexts) {
@ -313,7 +312,6 @@ async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
}
} finally {
await ops.commit()
await doneOp()
}
}
@ -404,8 +402,7 @@ export function loadSavedAttachments (): void {
export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
const client = InboxNotificationsClientImpl.getClient()
const notificationsByContext = get(client.inboxNotificationsByContext)
const doneOp = await getClient().measure('removeActivityChannels')
const ops = getClient().apply(generateId())
const ops = getClient().apply(generateId(), 'removeActivityChannels')
try {
for (const context of contexts) {
@ -418,15 +415,13 @@ export async function removeActivityChannels (contexts: DocNotifyContext[]): Pro
}
} finally {
await ops.commit()
await doneOp()
}
}
export async function readActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
const client = InboxNotificationsClientImpl.getClient()
const notificationsByContext = get(client.inboxNotificationsByContext)
const doneOp = await getClient().measure('readActivityChannels')
const ops = getClient().apply(generateId())
const ops = getClient().apply(generateId(), 'readActivityChannels')
try {
for (const context of contexts) {
@ -441,6 +436,5 @@ export async function readActivityChannels (contexts: DocNotifyContext[]): Promi
}
} finally {
await ops.commit()
await doneOp()
}
}

View File

@ -32,7 +32,8 @@ import {
type IdMap,
type Ref,
type Space,
type Timestamp
type Timestamp,
type WithLookup
} from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import {
@ -163,7 +164,11 @@ async function getDmAccounts (client: Client, space?: Space): Promise<PersonAcco
})
}
export async function getDmPersons (client: Client, space: Space): Promise<Person[]> {
export async function getDmPersons (
client: Client,
space: Space,
personsMap: Map<Ref<WithLookup<Person>>, WithLookup<Person>>
): Promise<Person[]> {
const personAccounts: PersonAccount[] = await getDmAccounts(client, space)
const me = getCurrentAccount() as PersonAccount
const persons: Person[] = []
@ -172,7 +177,7 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
let myPerson: Person | undefined
for (const personRef of personRefs) {
const person = await client.findOne(contact.class.Person, { _id: personRef })
const person = personsMap.get(personRef) ?? (await client.findOne(contact.class.Person, { _id: personRef }))
if (person === undefined) {
continue
}
@ -192,8 +197,12 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
return myPerson !== undefined ? [myPerson] : []
}
export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage>): Promise<string> {
const direct = await client.findOne(chunter.class.DirectMessage, { _id: id })
export async function DirectTitleProvider (
client: Client,
id: Ref<DirectMessage>,
doc?: DirectMessage
): Promise<string> {
const direct = doc ?? (await client.findOne(chunter.class.DirectMessage, { _id: id }))
if (direct === undefined) {
return ''
@ -202,8 +211,8 @@ export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage
return await getDmName(client, direct)
}
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>): Promise<string> {
const channel = await client.findOne(chunter.class.Channel, { _id: id })
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>, doc?: Channel): Promise<string> {
const channel = doc ?? (await client.findOne(chunter.class.Channel, { _id: id }))
if (channel === undefined) {
return ''

View File

@ -28,7 +28,6 @@ import core, {
FindOptions,
FindResult,
LoadModelResponse,
MeasureDoneOperation,
Ref,
SearchOptions,
SearchQuery,
@ -524,26 +523,6 @@ class Connection implements ClientConnection {
return await promise.promise
}
async measure (operationName: string): Promise<MeasureDoneOperation> {
const dateNow = Date.now()
// Send measure-start
const mid = await this.sendRequest({
method: 'measure',
params: [operationName]
})
return async () => {
const serverTime: number = await this.sendRequest({
method: 'measure-done',
params: [operationName, mid]
})
return {
time: Date.now() - dateNow,
serverTime
}
}
}
async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
return await this.sendRequest({ method: 'loadModel', params: [last, hash] })
}

View File

@ -28,14 +28,7 @@ import core, {
createClient,
type ClientConnection
} from '@hcengineering/core'
import platform, {
Severity,
Status,
getMetadata,
getPlugins,
getResource,
setPlatformStatus
} from '@hcengineering/platform'
import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform'
import { connect } from './connection'
export { connect }
@ -88,7 +81,7 @@ export default async () => {
): Promise<AccountClient> => {
const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? false
let client = createClient(
const client = createClient(
(handler: TxHandler) => {
const url = concatLink(endpoint, `/${token}`)
@ -144,8 +137,6 @@ export default async () => {
createModelPersistence(getWSFromToken(token)),
ctx
)
// Check if we had dev hook for client.
client = hookClient(client)
return await client
}
}
@ -203,24 +194,6 @@ function createModelPersistence (workspace: string): TxPersistenceStore | undefi
}
}
async function hookClient (client: Promise<AccountClient>): Promise<AccountClient> {
const hook = getMetadata(clientPlugin.metadata.ClientHook)
if (hook !== undefined) {
const hookProc = await getResource(hook)
const _client = client
client = new Promise((resolve, reject) => {
_client
.then((res) => {
resolve(hookProc(res))
})
.catch((err) => {
reject(err)
})
})
}
return await client
}
function getWSFromToken (token: string): string {
const parts = token.split('.')

View File

@ -22,11 +22,6 @@ import { Metadata, plugin } from '@hcengineering/platform'
*/
export const clientId = 'client' as Plugin
/**
* @public
*/
export type ClientHook = (client: AccountClient) => Promise<AccountClient>
/**
* @public
*/
@ -74,7 +69,6 @@ export type ClientFactory = (
export default plugin(clientId, {
metadata: {
ClientHook: '' as Metadata<Resource<ClientHook>>,
ClientSocketFactory: '' as Metadata<ClientSocketFactory>,
FilterModel: '' as Metadata<boolean>,
ExtraPlugins: '' as Metadata<Plugin[]>,

View File

@ -194,7 +194,7 @@ async function createControlledDoc (
const success = await ops.commit()
return { seqNumber, success }
return { seqNumber, success: success.result }
}
export async function createDocumentTemplate (
@ -327,7 +327,7 @@ export async function createDocumentTemplate (
const success = await ops.commit()
return { seqNumber, success }
return { seqNumber, success: success.result }
}
export function getCollaborativeDocForDocument (

View File

@ -13,20 +13,15 @@
// limitations under the License.
//
import core, {
import {
DOMAIN_MODEL,
cutObjectArray,
type Account,
type AccountClient,
type Class,
type Client,
type Doc,
type DocumentQuery,
type FindOptions,
type FindResult,
type Hierarchy,
type MeasureDoneOperation,
type ModelDb,
type Ref,
type SearchOptions,
type SearchQuery,
@ -35,13 +30,10 @@ import core, {
type TxResult,
type WithLookup
} from '@hcengineering/core'
import { devModelId } from '@hcengineering/devmodel'
import { Builder } from '@hcengineering/model'
import { getMetadata, type IntlString, type Resources } from '@hcengineering/platform'
import { addTxListener } from '@hcengineering/presentation'
import type { ClientHook } from '@hcengineering/presentation/src/plugin'
import { testing } from '@hcengineering/ui'
import view from '@hcengineering/view'
import workbench from '@hcengineering/workbench'
import ModelView from './components/ModelView.svelte'
import devmodel from './plugin'
export interface TxWitHResult {
@ -57,48 +49,46 @@ export interface QueryWithResult {
findOne: boolean
}
class ModelClient implements AccountClient {
export class PresentationClientHook implements ClientHook {
notifyEnabled = true
constructor (readonly client: AccountClient) {
constructor () {
this.notifyEnabled = (localStorage.getItem('#platform.notification.logging') ?? 'true') === 'true'
client.notify = (...tx) => {
this.notify?.(...tx)
addTxListener((...tx) => {
if (this.notifyEnabled) {
console.debug(
'devmodel# notify=>',
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx.length === 1 ? tx[0] : tx
)
}
})
}
stackLine (): string {
const stack = (new Error().stack ?? '').split('\n')
let candidate = ''
for (let l of stack) {
l = l.trim()
if (l.includes('.svelte')) {
return l
}
if (l.includes('plugins/') && !l.includes('devmodel-resources/') && l.includes('.ts') && candidate === '') {
candidate = l
}
}
}
async measure (operationName: string): Promise<MeasureDoneOperation> {
return await this.client.measure(operationName)
}
notify?: (...tx: Tx[]) => void
getHierarchy (): Hierarchy {
return this.client.getHierarchy()
}
getModel (): ModelDb {
return this.client.getModel()
}
async getAccount (): Promise<Account> {
return await this.client.getAccount()
return candidate
}
async findOne<T extends Doc>(
client: Client,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
const startTime = Date.now()
const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL
const result = await this.client.findOne(_class, query, options)
const isModel = client.getHierarchy().findDomain(_class) === DOMAIN_MODEL
const result = await client.findOne(_class, query, options)
if (this.notifyEnabled && !isModel) {
console.debug(
'devmodel# findOne=>',
@ -108,22 +98,24 @@ class ModelClient implements AccountClient {
'result => ',
testing ? JSON.stringify(cutObjectArray(result)) : result,
' =>model',
this.client.getModel(),
client.getModel(),
getMetadata(devmodel.metadata.DevModel),
Date.now() - startTime
Date.now() - startTime,
this.stackLine()
)
}
return result
}
async findAll<T extends Doc>(
client: Client,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
const startTime = Date.now()
const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL
const result = await this.client.findAll(_class, query, options)
const isModel = client.getHierarchy().findDomain(_class) === DOMAIN_MODEL
const result = await client.findAll(_class, query, options)
if (this.notifyEnabled && !isModel) {
console.debug(
'devmodel# findAll=>',
@ -133,17 +125,18 @@ class ModelClient implements AccountClient {
'result => ',
testing ? JSON.stringify(cutObjectArray(result)).slice(0, 160) : result,
' =>model',
this.client.getModel(),
client.getModel(),
getMetadata(devmodel.metadata.DevModel),
Date.now() - startTime,
JSON.stringify(result).length
JSON.stringify(result).length,
this.stackLine()
)
}
return result
}
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
const result = await this.client.searchFulltext(query, options)
async searchFulltext (client: Client, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
const result = await client.searchFulltext(query, options)
if (this.notifyEnabled) {
console.debug(
'devmodel# searchFulltext=>',
@ -156,71 +149,25 @@ class ModelClient implements AccountClient {
return result
}
async tx (tx: Tx): Promise<TxResult> {
async tx (client: Client, tx: Tx): Promise<TxResult> {
const startTime = Date.now()
const result = await this.client.tx(tx)
const result = await client.tx(tx)
if (this.notifyEnabled) {
console.debug(
'devmodel# tx=>',
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx,
result,
getMetadata(devmodel.metadata.DevModel),
Date.now() - startTime
Date.now() - startTime,
this.stackLine()
)
}
return result
}
async close (): Promise<void> {
await this.client.close()
}
}
export async function Hook (client: AccountClient): Promise<Client> {
console.debug('devmodel# Client HOOKED by DevModel')
// Client is alive here, we could hook with some model extensions special for DevModel plugin.
const builder = new Builder()
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: 'DevModel' as IntlString,
icon: view.icon.DevModel,
alias: devModelId,
hidden: false,
navigatorModel: {
spaces: [],
specials: [
{
label: 'Transactions' as IntlString,
icon: view.icon.Table,
id: 'transactions',
component: devmodel.component.ModelView
}
]
}
},
devmodel.ids.DevModelApp
)
const model = client.getModel()
for (const tx of builder.getTxes()) {
await model.tx(tx)
}
return new ModelClient(client)
}
export function toIntl (value: string): IntlString {
return value as IntlString
}
export default async (): Promise<Resources> => ({
component: {
ModelView
},
hook: {
Hook
}
})
export default async (): Promise<Resources> => ({})

View File

@ -13,8 +13,7 @@
// limitations under the License.
//
import { ClientHook } from '@hcengineering/client'
import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform'
import type { Asset, Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
@ -32,9 +31,6 @@ export default plugin(devModelId, {
TransactionView: '' as AnyComponent,
NotificationsView: '' as AnyComponent
},
hook: {
Hook: '' as Resource<ClientHook>
},
metadata: {
DevModel: '' as Metadata<any>
}

View File

@ -13,37 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import view, { decodeObjectURI } from '@hcengineering/view'
import {
AnyComponent,
Component,
defineSeparators,
deviceOptionsStore as deviceInfo,
Label,
location as locationStore,
Location,
location as locationStore,
restoreLocation,
Scroller,
Separator,
TabItem,
TabList,
deviceOptionsStore as deviceInfo
TabList
} from '@hcengineering/ui'
import chunter from '@hcengineering/chunter'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { get } from 'svelte/store'
import { translate } from '@hcengineering/platform'
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import view, { decodeObjectURI } from '@hcengineering/view'
import { parseLinkId } from '@hcengineering/view-resources'
import { get } from 'svelte/store'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import SettingsButton from './SettingsButton.svelte'
import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
import { InboxData, InboxNotificationsFilter } from '../../types'
import InboxGroupedListView from './InboxGroupedListView.svelte'
import notification from '../../plugin'
import { InboxData, InboxNotificationsFilter } from '../../types'
import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
import InboxGroupedListView from './InboxGroupedListView.svelte'
import InboxMenuButton from './InboxMenuButton.svelte'
import SettingsButton from './SettingsButton.svelte'
const client = getClient()
const hierarchy = client.getHierarchy()
@ -248,8 +247,7 @@
const contextNotifications = $notificationsByContextStore.get(selectedContext._id) ?? []
const doneOp = await getClient().measure('readNotifications')
const ops = getClient().apply(selectedContext._id)
const ops = getClient().apply(selectedContext._id, 'readNotifications')
try {
await inboxClient.readNotifications(
ops,
@ -261,7 +259,6 @@
)
} finally {
await ops.commit()
await doneOp()
}
}

View File

@ -242,8 +242,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
async archiveAllNotifications (): Promise<void> {
const doneOp = await getClient().measure('archiveAllNotifications')
const ops = getClient().apply(generateId())
const ops = getClient().apply(generateId(), 'archiveAllNotifications')
try {
const inboxNotifications = await ops.findAll(
@ -264,13 +263,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
} finally {
await ops.commit()
await doneOp()
}
}
async readAllNotifications (): Promise<void> {
const doneOp = await getClient().measure('readAllNotifications')
const ops = getClient().apply(generateId())
const ops = getClient().apply(generateId(), 'readAllNotifications')
try {
const inboxNotifications = await ops.findAll(
@ -291,13 +288,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
} finally {
await ops.commit()
await doneOp()
}
}
async unreadAllNotifications (): Promise<void> {
const doneOp = await getClient().measure('unreadAllNotifications')
const ops = getClient().apply(generateId())
const ops = getClient().apply(generateId(), 'unreadAllNotifications')
try {
const inboxNotifications = await ops.findAll(
@ -332,7 +327,6 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
}
} finally {
await ops.commit()
await doneOp()
}
}
}

View File

@ -49,21 +49,21 @@ import { MessageBox, getClient } from '@hcengineering/presentation'
import {
getCurrentLocation,
getLocation,
locationStorageKeyId,
navigate,
parseLocation,
showPopup,
type Location,
type ResolvedLocation,
locationStorageKeyId
type ResolvedLocation
} from '@hcengineering/ui'
import { get, writable } from 'svelte/store'
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
import { getMetadata } from '@hcengineering/platform'
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
import { getObjectLinkId } from '@hcengineering/view-resources'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxData, type InboxNotificationsFilter } from './types'
import { getMetadata } from '@hcengineering/platform'
import { getObjectLinkId } from '@hcengineering/view-resources'
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
if (docNotifyContext.hidden) {
@ -107,8 +107,7 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
const doneOp = await getClient().measure('readNotifyContext')
const ops = getClient().apply(doc._id)
const ops = getClient().apply(doc._id, 'readNotifyContext')
try {
await inboxClient.readNotifications(
ops,
@ -117,7 +116,6 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
await ops.update(doc, { lastViewedTimestamp: Date.now() })
} finally {
await ops.commit()
await doneOp()
}
}
@ -133,8 +131,7 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
return
}
const doneOp = await getClient().measure('unReadNotifyContext')
const ops = getClient().apply(doc._id)
const ops = getClient().apply(doc._id, 'unReadNotifyContext')
try {
await inboxClient.unreadNotifications(
@ -154,7 +151,6 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
}
} finally {
await ops.commit()
await doneOp()
}
}
@ -166,8 +162,7 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
return
}
const doneOp = await getClient().measure('archiveContextNotifications')
const ops = getClient().apply(doc._id)
const ops = getClient().apply(doc._id, 'archiveContextNotifications')
try {
const notifications = await ops.findAll(
@ -182,7 +177,6 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
await ops.update(doc, { lastViewedTimestamp: Date.now() })
} finally {
await ops.commit()
await doneOp()
}
}
@ -194,8 +188,7 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
return
}
const doneOp = await getClient().measure('unarchiveContextNotifications')
const ops = getClient().apply(doc._id)
const ops = getClient().apply(doc._id, 'unarchiveContextNotifications')
try {
const notifications = await ops.findAll(
@ -209,7 +202,6 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
}
} finally {
await ops.commit()
await doneOp()
}
}

View File

@ -442,10 +442,8 @@
// TODO: We need a measure client and mark all operations with it as measure under one root,
// to prevent other operations to infer our measurement.
const doneOp = await getClient().measure('tracker.createIssue')
try {
const operations = client.apply(_id)
const operations = client.apply(_id, 'tracker.createIssue')
const lastOne = await client.findOne<Issue>(
tracker.class.Issue,
@ -533,7 +531,7 @@
}
}
await operations.commit()
const result = await operations.commit()
await descriptionBox?.createAttachments(_id)
const parents: IssueParentInfo[] =
@ -565,15 +563,12 @@
descriptionBox?.removeDraft(false)
isAssigneeTouched = false
const d1 = Date.now()
void doneOp().then((res) => {
console.log('createIssue measure', res, Date.now() - d1)
})
console.log('createIssue measure', result, Date.now() - d1)
} catch (err: any) {
resetObject()
draftController.remove()
descriptionBox?.removeDraft(false)
console.error(err)
await doneOp() // Complete in case of error
Analytics.handleError(err)
}
}

View File

@ -235,7 +235,7 @@
await ops.createDoc(tracker.class.Project, core.space.Space, { ...projectData, type: typeId }, projectId)
const succeeded = await ops.commit()
if (succeeded) {
if (succeeded.result) {
// Add space type's mixin with roles assignments
await client.createMixin(
projectId,

View File

@ -25,8 +25,8 @@ export async function issueIdentifierProvider (client: TxOperations, ref: Ref<Is
return object.identifier
}
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
const object = await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> })
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>, doc?: Issue): Promise<string> {
const object = doc ?? (await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> }))
if (object === undefined) {
return ''

View File

@ -652,7 +652,7 @@ async function ActivityReferenceCreate (tx: TxCUD<Doc>, control: TriggerControl)
)
if (txes.length !== 0) {
await control.apply(txes, true)
await control.apply(txes)
}
return []
@ -699,7 +699,7 @@ async function ActivityReferenceUpdate (tx: TxCUD<Doc>, control: TriggerControl)
)
if (txes.length !== 0) {
await control.apply(txes, true)
await control.apply(txes)
}
return []
@ -723,7 +723,7 @@ async function ActivityReferenceRemove (tx: Tx, control: TriggerControl): Promis
const txes: Tx[] = await getRemoveActivityReferenceTxes(control, txFactory, ctx.objectId)
if (txes.length !== 0) {
await control.apply(txes, true)
await control.apply(txes)
}
}

View File

@ -13,7 +13,9 @@
// limitations under the License.
//
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
import { Person, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
AttachedDoc,
@ -38,14 +40,12 @@ import notification, { Collaborators, NotificationContent } from '@hcengineering
import { getMetadata, IntlString } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import {
createCollaboratorNotifications,
getDocCollaborators,
getMixinTx,
createCollaboratorNotifications
getMixinTx
} from '@hcengineering/server-notification-resources'
import { workbenchId } from '@hcengineering/workbench'
import { stripTags } from '@hcengineering/text'
import { Person, PersonAccount } from '@hcengineering/contact'
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
import { workbenchId } from '@hcengineering/workbench'
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
@ -258,13 +258,24 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
* @public
*/
export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const res = await Promise.all([
OnThreadMessageCreated(tx, control),
OnThreadMessageDeleted(tx, control),
OnCollaboratorsChanged(tx as TxMixin<Doc, Collaborators>, control),
OnChatMessageCreated(tx, control)
])
return res.flat()
const res: Tx[] = []
res.push(
...(await control.ctx.with('OnThreadMessageCreated', {}, async (ctx) => await OnThreadMessageCreated(tx, control)))
)
res.push(
...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control)))
)
res.push(
...(await control.ctx.with(
'OnCollaboratorsChanged',
{},
async (ctx) => await OnCollaboratorsChanged(tx as TxMixin<Doc, Collaborators>, control)
))
)
res.push(
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
)
return res
}
/**
@ -393,7 +404,7 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
lastViewedTimestamp: tx.modifiedOn
})
await control.apply([createTx], true)
await control.apply([createTx])
} else {
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
hidden: false,

View File

@ -284,7 +284,7 @@ async function createDocumentTrainingRequest (doc: ControlledDocument, control:
// Force space to make transaction persistent and raise notifications
resTx.space = core.space.Tx
await control.apply([resTx], true)
await control.apply([resTx])
return []
}
@ -368,7 +368,7 @@ export async function OnDocPlannedEffectiveDateChanged (
if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) {
// Create with not derived tx factory in order for notifications to work
const factory = new TxFactory(control.txFactory.account)
await control.apply([makeDocEffective(doc, factory)], true)
await control.apply([makeDocEffective(doc, factory)])
}
return []
@ -385,7 +385,7 @@ export async function OnDocApprovalRequestApproved (
// Create with not derived tx factory in order for notifications to work
const factory = new TxFactory(control.txFactory.account)
await control.apply([makeDocEffective(doc, factory)], true)
await control.apply([makeDocEffective(doc, factory)])
// make doc effective immediately
return []

View File

@ -97,9 +97,10 @@ async function createUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
query: { person }
}
],
[tx]
[tx],
'createUserInfo'
)
await control.apply([ptx], true)
await control.apply([ptx])
return []
}
@ -114,7 +115,7 @@ async function removeUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
const person = account.person
const infos = await control.findAll(love.class.ParticipantInfo, { person })
for (const info of infos) {
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)], true)
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)])
}
}

View File

@ -15,6 +15,7 @@
//
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, {
type AvatarInfo,
@ -42,6 +43,7 @@ import core, {
RefTo,
Space,
Timestamp,
toIdMap,
Tx,
TxCollectionCUD,
TxCreateDoc,
@ -75,12 +77,11 @@ import serverNotification, {
getPersonAccountById,
NOTIFICATION_BODY_SIZE
} from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view'
import { stripTags } from '@hcengineering/text'
import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push'
import { encodeObjectURI } from '@hcengineering/view'
import serverView from '@hcengineering/server-view'
import { Analytics } from '@hcengineering/analytics'
import { Content, NotifyParams, NotifyResult, UserInfo } from './types'
import {
@ -396,14 +397,24 @@ export async function pushInboxNotifications (
hidden: false,
lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
})
await control.apply([createContextTx], true, [account.email])
await control.apply([createContextTx])
control.operationContext.derived.targets['docNotifyContext' + createContextTx._id] = (it) => {
if (it._id === createContextTx._id) {
return [account.email]
}
}
docNotifyContextId = createContextTx.objectId
} else {
if (shouldUpdateTimestamp && context.lastUpdateTimestamp !== modifiedOn) {
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
lastUpdateTimestamp: modifiedOn
})
await control.apply([updateTx], true, [account.email])
await control.apply([updateTx])
control.operationContext.derived.targets['docNotifyContext' + updateTx._id] = (it) => {
if (it._id === updateTx._id) {
return [account.email]
}
}
}
docNotifyContextId = context._id
}
@ -636,7 +647,7 @@ async function sendPushToSubscription (
console.log('Cannot send push notification to', targetUser, err)
if (err instanceof WebPushError && err.body.includes('expired')) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply([tx], true)
await control.apply([tx])
}
}
}
@ -1286,7 +1297,14 @@ async function applyUserTxes (
if (account !== undefined) {
cache.set(account._id, account)
await control.apply(txs, true, [account.email])
await control.apply(txs)
const m1 = toIdMap(txes)
control.operationContext.derived.targets.docNotifyContext = (it) => {
if (m1.has(it._id)) {
return [account.email]
}
}
}
}

View File

@ -99,7 +99,7 @@ async function OnRequestUpdate (tx: TxCollectionCUD<Doc, Request>, control: Trig
}
if (applyTxes.length > 0) {
await control.apply(applyTxes, true)
await control.apply(applyTxes)
}
return []

View File

@ -107,7 +107,7 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis
issue.collection,
innerTx
)
await control.apply([outerTx], true)
await control.apply([outerTx])
return []
}
}
@ -153,7 +153,7 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx
issue.collection,
innerTx
)
await control.apply([outerTx], true)
await control.apply([outerTx])
return []
}
}
@ -304,7 +304,7 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
if (funcs !== undefined) {
const func = await getResource(funcs.onDone)
const todoRes = await func(control, resEvents, todo)
await control.apply(todoRes, true)
await control.apply(todoRes)
}
return res
}
@ -447,7 +447,7 @@ async function createIssueHandler (issue: Issue, control: TriggerControl): Promi
if (status.category === task.statusCategory.Active || status.category === task.statusCategory.ToDo) {
const tx = await getCreateToDoTx(issue, issue.assignee, control)
if (tx !== undefined) {
await control.apply([tx], true)
await control.apply([tx])
}
}
}
@ -561,7 +561,7 @@ async function changeIssueStatusHandler (
if (todos.length === 0) {
const tx = await getCreateToDoTx(issue, issue.assignee, control)
if (tx !== undefined) {
await control.apply([tx], true)
await control.apply([tx])
}
}
}

View File

@ -24,9 +24,10 @@ import core, {
TxProcessor,
cutObjectArray,
toFindResult,
type Branding,
type Account,
type AttachedDoc,
type Branding,
type BroadcastTargets,
type Class,
type Client,
type Collection,
@ -50,6 +51,7 @@ import core, {
type Timestamp,
type Tx,
type TxApplyIf,
type TxApplyResult,
type TxCUD,
type TxCollectionCUD,
type TxRemoveDoc,
@ -612,7 +614,7 @@ export class TServerStorage implements ServerStorage {
return result
}
private async broadcastCtx (derived: SessionOperationContext['derived']): Promise<void> {
private async broadcastCtx (derived: Tx[], targets?: BroadcastTargets): Promise<void> {
const toSendTarget = new Map<string, Tx[]>()
const getTxes = (key: string): Tx[] => {
@ -626,16 +628,23 @@ export class TServerStorage implements ServerStorage {
// Put current user as send target
for (const txd of derived) {
if (txd.target === undefined) {
let target: string[] | undefined
for (const tt of Object.values(targets ?? {})) {
target = tt(txd)
if (target !== undefined) {
break
}
}
if (target === undefined) {
getTxes('') // Be sure we have empty one
// Also add to all other targeted sends
for (const v of toSendTarget.values()) {
v.push(...txd.derived)
v.push(txd)
}
} else {
for (const t of txd.target) {
getTxes(t).push(...txd.derived)
for (const t of target) {
getTxes(t).push(txd)
}
}
}
@ -708,7 +717,9 @@ export class TServerStorage implements ServerStorage {
)
const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx.ctx, txes, findAll))
const triggerControl: Omit<TriggerControl, 'txFactory' | 'ctx' | 'result'> = {
const applyTxes: Tx[] = []
const triggerControl: Omit<TriggerControl, 'txFactory' | 'ctx' | 'result' | 'apply'> = {
operationContext: ctx,
removedMap,
workspace: this.workspaceId,
@ -719,52 +730,73 @@ export class TServerStorage implements ServerStorage {
findAllCtx: findAll,
modelDb: this.modelDb,
hierarchy: this.hierarchy,
apply: async (tx, broadcast, target) => {
return await this.apply(ctx, tx, broadcast, target)
},
applyCtx: async (ctx, tx, broadcast, target) => {
return await this.apply(ctx, tx, broadcast, target)
applyCtx: async (ctx, tx, needResult) => {
if (needResult === true) {
return await this.apply(ctx, tx)
} else {
applyTxes.push(...tx)
}
return {}
},
// Will create a live query if missing and return values immediately if already asked.
queryFind: async (_class, query, options) => {
return await this.liveQuery.queryFind(_class, query, options)
}
}
const triggers = await ctx.with('process-triggers', {}, async (ctx) => {
const result: Tx[] = []
const { transactions, performAsync } = await this.triggers.apply(ctx, txes, {
...triggerControl,
ctx: ctx.ctx,
findAll: fAll(ctx.ctx),
result
})
result.push(...transactions)
const triggers = await ctx.with(
'process-triggers',
{},
async (ctx) => {
const result: Tx[] = []
const { transactions, performAsync } = await this.triggers.apply(ctx, txes, {
...triggerControl,
ctx: ctx.ctx,
findAll: fAll(ctx.ctx),
result
})
result.push(...transactions)
if (performAsync !== undefined) {
const asyncTriggerProcessor = async (): Promise<void> => {
await ctx.with('process-async-triggers', {}, async (ctx) => {
const sctx = ctx as SessionContext
const applyCtx: SessionContextImpl = new SessionContextImpl(
ctx.ctx,
sctx.userEmail,
sctx.sessionId,
sctx.admin,
[],
this.workspaceId,
this.options.branding
if (applyTxes.length > 0) {
await this.apply(ctx, applyTxes)
}
if (performAsync !== undefined) {
const asyncTriggerProcessor = async (): Promise<void> => {
await ctx.with(
'process-async-triggers',
{},
async (ctx) => {
const sctx = ctx as SessionContext
const applyCtx: SessionContextImpl = new SessionContextImpl(
ctx.ctx,
sctx.userEmail,
sctx.sessionId,
sctx.admin,
{ txes: [], targets: {} },
this.workspaceId,
this.options.branding,
true
)
const result = await performAsync(applyCtx)
if (applyTxes.length > 0) {
await this.apply(applyCtx, applyTxes)
}
// We need to broadcast changes
await this.broadcastCtx(applyCtx.derived.txes.concat(result), applyCtx.derived.targets)
},
{ count: txes.length }
)
const result = await performAsync(applyCtx)
// We need to broadcast changes
await this.broadcastCtx([{ derived: result }, ...applyCtx.derived])
}
setTimeout(() => {
void asyncTriggerProcessor()
})
}
setTimeout(() => {
void asyncTriggerProcessor()
})
}
return result
})
return result
},
{ count: txes.length }
)
const derived = [...removed, ...collections, ...moves, ...triggers]
@ -835,8 +867,8 @@ export class TServerStorage implements ServerStorage {
return { passed, onEnd }
}
async apply (ctx: SessionOperationContext, txes: Tx[], broadcast: boolean, target?: string[]): Promise<TxResult> {
return await this.processTxes(ctx, txes, broadcast, target)
async apply (ctx: SessionOperationContext, txes: Tx[]): Promise<TxResult> {
return await this.processTxes(ctx, txes)
}
fillTxes (txes: Tx[], txToStore: Tx[], modelTx: Tx[], txToProcess: Tx[], applyTxes: Tx[]): void {
@ -861,12 +893,7 @@ export class TServerStorage implements ServerStorage {
}
}
async processTxes (
ctx: SessionOperationContext,
txes: Tx[],
broadcast: boolean,
target?: string[]
): Promise<TxResult> {
async processTxes (ctx: SessionOperationContext, txes: Tx[]): Promise<TxResult> {
// store tx
const _findAll: ServerStorage['findAll'] = async <T extends Doc>(
ctx: MeasureContext,
@ -914,16 +941,25 @@ export class TServerStorage implements ServerStorage {
await this.triggers.tx(tx)
await this.modelDb.tx(tx)
}
await ctx.with('domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore))
result.push(...(await ctx.with('apply', {}, (ctx) => this.routeTx(ctx.ctx, removedMap, ...txToProcess))))
await ctx.with('domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore), {
count: txToStore.length
})
result.push(...(await ctx.with('apply', {}, (ctx) => this.routeTx(ctx.ctx, removedMap, ...txToProcess))), {
count: txToProcess.length
})
// invoke triggers and store derived objects
derived.push(...(await this.processDerived(ctx, txToProcess, _findAll, removedMap)))
// index object
await ctx.with('fulltext-tx', {}, async (ctx) => {
await this.fulltext.tx(ctx.ctx, [...txToProcess, ...derived])
})
await ctx.with(
'fulltext-tx',
{},
async (ctx) => {
await this.fulltext.tx(ctx.ctx, [...txToProcess, ...derived])
},
{ count: txToProcess.length + derived.length }
)
} catch (err: any) {
ctx.ctx.error('error process tx', { error: err })
Analytics.handleError(err)
@ -933,16 +969,33 @@ export class TServerStorage implements ServerStorage {
p()
})
}
if (derived.length > 0 && broadcast) {
ctx.derived.push({ derived, target })
if (derived.length > 0) {
ctx.derived.txes.push(...derived)
}
return result[0]
}
async tx (ctx: SessionOperationContext, tx: Tx): Promise<TxResult> {
return await ctx.with('client-tx', { _class: tx._class }, async (ctx) => {
return await this.processTxes(ctx, [tx], true)
})
let measureName: string | undefined
let st: number | undefined
if (tx._class === core.class.TxApplyIf && (tx as TxApplyIf).measureName !== undefined) {
measureName = (tx as TxApplyIf).measureName
st = Date.now()
}
const result = await ctx.with(
measureName !== undefined ? `📶 ${measureName}` : 'client-tx',
{ _class: tx._class },
async (ctx) => {
return await this.processTxes(ctx, [tx])
}
)
if (measureName !== undefined && st !== undefined) {
;(result as TxApplyResult).serverTime = Date.now() - st
}
return result
}
find (ctx: MeasureContext, domain: Domain, recheck?: boolean): StorageIterator {

View File

@ -17,6 +17,7 @@
import core, {
TxFactory,
cutObjectArray,
groupByArray,
matchQuery,
type AttachedDoc,
type Class,
@ -31,15 +32,18 @@ import core, {
type TxCreateDoc
} from '@hcengineering/core'
import { Analytics } from '@hcengineering/analytics'
import { getResource, type Resource } from '@hcengineering/platform'
import type { Trigger, TriggerControl, TriggerFunc } from './types'
import { Analytics } from '@hcengineering/analytics'
import serverCore from './plugin'
import type { SessionContextImpl } from './utils'
interface TriggerRecord {
query?: DocumentQuery<Tx>
trigger: { op: TriggerFunc, resource: Resource<TriggerFunc>, isAsync: boolean }
arrays: boolean
}
/**
* @public
@ -64,7 +68,8 @@ export class Triggers {
this.triggers.push({
query: match,
trigger: { op: func, resource: trigger, isAsync }
trigger: { op: func, resource: trigger, isAsync },
arrays: (createTx as TxCreateDoc<Trigger>).attributes.arrays === true
})
}
}
@ -73,47 +78,55 @@ export class Triggers {
async apply (
ctx: SessionOperationContext,
tx: Tx[],
ctrl: Omit<TriggerControl, 'txFactory'>
ctrl: Omit<TriggerControl, 'txFactory' | 'apply'>
): Promise<{
transactions: Tx[]
performAsync?: (ctx: SessionOperationContext) => Promise<Tx[]>
}> {
const result: Tx[] = []
const suppressAsync = (ctx as SessionContextImpl).isAsyncContext ?? false
const asyncRequest: {
matches: Tx[]
trigger: TriggerRecord['trigger']
arrays: TriggerRecord['arrays']
}[] = []
const applyTrigger = async (
ctx: SessionOperationContext,
matches: Tx[],
trigger: TriggerRecord['trigger'],
{ trigger, arrays }: TriggerRecord,
result: Tx[]
): Promise<void> => {
for (const tx of matches) {
try {
result.push(
...(await trigger.op(tx, {
...ctrl,
ctx: ctx.ctx,
txFactory: new TxFactory(tx.modifiedBy, true),
findAll: async (clazz, query, options) => await ctrl.findAllCtx(ctx.ctx, clazz, query, options),
apply: async (tx, broadcast, target) => {
return await ctrl.applyCtx(ctx, tx, broadcast, target)
},
result
}))
)
} catch (err: any) {
ctx.ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err })
Analytics.handleError(err)
const group = groupByArray(matches, (it) => it.modifiedBy)
const tctrl: TriggerControl = {
...ctrl,
operationContext: ctx,
ctx: ctx.ctx,
txFactory: new TxFactory(core.account.System, true),
findAll: async (clazz, query, options) => await ctrl.findAllCtx(ctx.ctx, clazz, query, options),
apply: async (tx, needResult) => {
return await ctrl.applyCtx(ctx, tx, needResult)
},
result
}
for (const [k, v] of group.entries()) {
const m = arrays ? [v] : v
tctrl.txFactory = new TxFactory(k, true)
for (const tx of m) {
try {
result.push(...(await trigger.op(tx, tctrl)))
} catch (err: any) {
ctx.ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err })
Analytics.handleError(err)
}
}
}
}
const promises: Promise<void>[] = []
for (const { query, trigger } of this.triggers) {
for (const { query, trigger, arrays } of this.triggers) {
let matches = tx
if (query !== undefined) {
this.addDerived(query, 'objectClass')
@ -121,23 +134,26 @@ export class Triggers {
matches = matchQuery(tx, query, core.class.Tx, ctrl.hierarchy) as Tx[]
}
if (matches.length > 0) {
if (trigger.isAsync) {
if (trigger.isAsync && !suppressAsync) {
asyncRequest.push({
matches,
trigger
trigger,
arrays
})
} else {
promises.push(
ctx.with(trigger.resource, {}, async (ctx) => {
await applyTrigger(ctx, matches, trigger, result)
})
await ctx.with(
trigger.resource,
{},
async (ctx) => {
const tresult: Tx[] = []
await applyTrigger(ctx, matches, { trigger, arrays }, tresult)
result.push(...tresult)
},
{ count: matches.length, arrays }
)
}
}
}
// Wait all regular triggers to complete in parallel
await Promise.all(promises)
return {
transactions: result,
performAsync:
@ -148,7 +164,7 @@ export class Triggers {
for (const request of asyncRequest) {
try {
await ctx.with(request.trigger.resource, {}, async (ctx) => {
await applyTrigger(ctx, request.matches, request.trigger, result)
await applyTrigger(ctx, request.matches, request, result)
})
} catch (err: any) {
ctx.ctx.error('failed to process trigger', {

View File

@ -16,6 +16,7 @@
import {
MeasureMetricsContext,
type Account,
type Branding,
type Class,
type Doc,
type DocumentQuery,
@ -40,8 +41,7 @@ import {
type TxFactory,
type TxResult,
type WorkspaceId,
type WorkspaceIdWithUrl,
type Branding
type WorkspaceIdWithUrl
} from '@hcengineering/core'
import type { Asset, Resource } from '@hcengineering/platform'
import { type Readable } from 'stream'
@ -162,8 +162,8 @@ export interface TriggerControl {
storageAdapter: StorageAdapter
serviceAdaptersManager: ServiceAdaptersManager
// Bulk operations in case trigger require some
apply: (tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
applyCtx: (ctx: SessionOperationContext, tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
apply: (tx: Tx[], needResult?: boolean) => Promise<TxResult>
applyCtx: (ctx: SessionOperationContext, tx: Tx[], needResult?: boolean) => Promise<TxResult>
// Will create a live query if missing and return values immediately if already asked.
queryFind: <T extends Doc>(
@ -179,7 +179,7 @@ export interface TriggerControl {
/**
* @public
*/
export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise<Tx[]>
export type TriggerFunc = (tx: Tx | Tx[], ctrl: TriggerControl) => Promise<Tx[]>
/**
* @public
@ -192,6 +192,9 @@ export interface Trigger extends Doc {
// We should match transaction
txMatch?: DocumentQuery<Tx>
// If set trigger will handle Tx[] instead of Tx
arrays?: boolean
}
/**

View File

@ -142,7 +142,8 @@ export class SessionContextImpl implements SessionContext {
readonly admin: boolean | undefined,
readonly derived: SessionContext['derived'],
readonly workspace: WorkspaceIdWithUrl,
readonly branding: Branding | null
readonly branding: Branding | null,
readonly isAsyncContext: boolean
) {}
with<T>(
@ -163,7 +164,8 @@ export class SessionContextImpl implements SessionContext {
this.admin,
this.derived,
this.workspace,
this.branding
this.branding,
this.isAsyncContext
)
),
fullParams

View File

@ -32,7 +32,7 @@ import platform, { PlatformError, Severity, Status } from '@hcengineering/platfo
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
import { DOMAIN_PREFERENCE } from '@hcengineering/server-preference'
import { BaseMiddleware } from './base'
import { getUser, mergeTargets } from './utils'
import { getUser } from './utils'
/**
* @public
@ -65,12 +65,7 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware {
}
}
}
const res = await this.provideTx(ctx, tx)
// Add target to all broadcasts
ctx.derived.forEach((it) => {
it.target = mergeTargets(target, it.target)
})
return res
return await this.provideTx(ctx, tx)
}
override async findAll<T extends Doc>(

View File

@ -335,10 +335,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
await this.processPermissionsUpdatesFromTx(ctx, tx)
await this.checkPermissions(ctx, tx)
const res = await this.provideTx(ctx, tx)
for (const txd of ctx.derived) {
for (const tx of txd.derived) {
await this.processPermissionsUpdatesFromTx(ctx, tx)
}
for (const txd of ctx.derived.txes) {
await this.processPermissionsUpdatesFromTx(ctx, txd)
}
return res
}
@ -347,7 +345,9 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
if (tx._class === core.class.TxApplyIf) {
const applyTx = tx as TxApplyIf
await Promise.all(applyTx.txes.map((t) => this.checkPermissions(ctx, t)))
for (const t of applyTx.txes) {
await this.checkPermissions(ctx, t)
}
return
}

View File

@ -46,7 +46,7 @@ import core, {
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
import { BaseMiddleware } from './base'
import { getUser, isOwner, isSystem, mergeTargets } from './utils'
import { getUser, isOwner, isSystem } from './utils'
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class'>
@ -246,10 +246,13 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
space: core.space.DerivedTx,
params: null
}
ctx.derived.push({
derived: [tx],
target: targets
})
ctx.derived.txes.push(tx)
ctx.derived.targets.security = (it) => {
// TODO: I'm not sure it is called
if (it._id === tx._id) {
return targets
}
}
}
private async broadcastNonMembers (ctx: SessionContext, space: SpaceWithMembers): Promise<void> {
@ -413,17 +416,10 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
await this.processTx(ctx, tx)
const targets = await this.getTxTargets(ctx, tx)
const res = await this.provideTx(ctx, tx)
for (const txd of ctx.derived) {
for (const tx of txd.derived) {
await this.processTx(ctx, tx)
}
for (const txd of ctx.derived.txes) {
await this.processTx(ctx, txd)
}
ctx.derived.forEach((it) => {
it.target = mergeTargets(targets, it.target)
})
return res
}
@ -435,6 +431,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
for (const tx of txes) {
const h = this.storage.hierarchy
if (h.isDerived(tx._class, core.class.TxCUD)) {
// TODO: Do we need security check here?
const cudTx = tx as TxCUD<Doc>
await this.processTxSpaceDomain(cudTx)
} else if (tx._class === core.class.TxWorkspaceEvent) {

View File

@ -17,18 +17,6 @@ import core, { Account, AccountRole, systemAccountEmail } from '@hcengineering/c
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { SessionContext, type ServerStorage } from '@hcengineering/server-core'
export function mergeTargets (current: string[] | undefined, prev: string[] | undefined): string[] | undefined {
if (current === undefined) return prev
if (prev === undefined) return current
const res: string[] = []
for (const value of current) {
if (prev.includes(value)) {
res.push(value)
}
}
return res
}
export async function getUser (storage: ServerStorage, ctx: SessionContext): Promise<Account> {
if (ctx.userEmail === undefined) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))

View File

@ -182,7 +182,10 @@ describe('mongo operations', () => {
})
const soCtx: SessionOperationContext = {
ctx,
derived: [],
derived: {
txes: [],
targets: {}
},
with: async <T>(
name: string,
params: ParamsType,
@ -206,7 +209,6 @@ describe('mongo operations', () => {
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
loadModel: async () => txes,
getAccount: async () => ({}) as any,
measure: async () => async () => ({ time: 0, serverTime: 0 }),
sendForceClose: async () => {}
}
return st

View File

@ -114,9 +114,13 @@ export class ClientSession implements Session {
this.token.email,
this.sessionId,
this.token.extra?.admin === 'true',
[],
{
txes: [],
targets: {}
},
this._pipeline.storage.workspaceId,
this._pipeline.storage.branding
this._pipeline.storage.branding,
false
)
await this._pipeline.tx(context, createTx)
const acc = TxProcessor.createDoc2Doc(createTx)
@ -144,9 +148,13 @@ export class ClientSession implements Session {
this.token.email,
this.sessionId,
this.token.extra?.admin === 'true',
[],
{
txes: [],
targets: {}
},
this._pipeline.storage.workspaceId,
this._pipeline.storage.branding
this._pipeline.storage.branding,
false
)
return await this._pipeline.findAll(context, _class, query, options)
}
@ -167,9 +175,13 @@ export class ClientSession implements Session {
this.token.email,
this.sessionId,
this.token.extra?.admin === 'true',
[],
{
txes: [],
targets: {}
},
this._pipeline.storage.workspaceId,
this._pipeline.storage.branding
this._pipeline.storage.branding,
false
)
await ctx.sendResponse(await this._pipeline.searchFulltext(context, query, options))
}
@ -183,9 +195,13 @@ export class ClientSession implements Session {
this.token.email,
this.sessionId,
this.token.extra?.admin === 'true',
[],
{
txes: [],
targets: {}
},
this._pipeline.storage.workspaceId,
this._pipeline.storage.branding
this._pipeline.storage.branding,
false
)
const result = await this._pipeline.tx(context, tx)
@ -209,18 +225,24 @@ export class ClientSession implements Session {
}
// Put current user as send target
toSendTarget.set(this.getUser(), [])
for (const txd of context.derived) {
if (txd.target === undefined) {
getTxes('') // be sure we have empty one
for (const txd of context.derived.txes) {
let target: string[] | undefined
for (const tt of Object.values(context.derived.targets ?? {})) {
target = tt(txd)
if (target !== undefined) {
break
}
}
if (target === undefined) {
getTxes('') // Be sure we have empty one
// Also add to all other targeted sends
for (const v of toSendTarget.values()) {
v.push(...txd.derived)
v.push(txd)
}
} else {
for (const t of txd.target) {
getTxes(t).push(...txd.derived)
for (const t of target) {
getTxes(t).push(txd)
}
}
}

View File

@ -912,10 +912,6 @@ class TSessionManager implements SessionManager {
}
})
if (request.method === 'measure' || request.method === 'measure-done') {
await this.handleMeasure<S>(service, request, opContext(ctx))
return
}
service.requests.set(reqId, {
id: reqId,
params: request,
@ -930,9 +926,7 @@ class TSessionManager implements SessionManager {
try {
const params = [...request.params]
service.measureCtx?.ctx !== undefined
? await f.apply(service, [opContext(service.measureCtx?.ctx), ...params])
: await ctx.with('🧨 process', {}, async (callTx) => f.apply(service, [opContext(callTx), ...params]))
await ctx.with('🧨 process', {}, async (callTx) => f.apply(service, [opContext(callTx), ...params]))
} catch (err: any) {
Analytics.handleError(err)
if (LOGGING_ENABLED) {
@ -955,34 +949,7 @@ class TSessionManager implements SessionManager {
service.requests.delete(reqId)
}
}
private async handleMeasure<S extends Session>(
service: S,
request: Request<any[]>,
ctx: ClientSessionCtx
): Promise<void> {
let serverTime = 0
if (request.method === 'measure') {
service.measureCtx = { ctx: ctx.ctx.newChild('📶 ' + request.params[0], {}), time: Date.now() }
} else {
if (service.measureCtx !== undefined) {
serverTime = Date.now() - service.measureCtx.time
service.measureCtx.ctx.end(serverTime)
service.measureCtx = undefined
}
}
try {
await ctx.sendResponse(request.method === 'measure' ? 'started' : serverTime)
} catch (err: any) {
Analytics.handleError(err)
if (LOGGING_ENABLED) {
ctx.ctx.error('error handle measure', { error: err, request })
}
await ctx.sendError(JSON.parse(JSON.stringify(err?.stack)), unknownError(err))
}
}
}
/**
* @public
*/

View File

@ -77,8 +77,6 @@ export interface Session {
current: StatisticsElement
mins5: StatisticsElement
measureCtx?: { ctx: MeasureContext, time: number }
lastRequest: number
isUpgradeClient: () => boolean