Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions foundations/core/packages/core/src/__tests__/memdb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,4 +554,82 @@ describe('memdb', () => {
expect(e).toEqual(new Error('createDoc cannot be used for objects inherited from AttachedDoc'))
}
})

it('addTxes coalesces orphan-targeted transactions into a single info line', async () => {
const { MeasureMetricsContext } = await import('@hcengineering/measurements')
const { TxFactory } = await import('../tx')
const { generateId } = await import('../utils')

const infoCalls: Array<{ message: string, args?: Record<string, any> }> = []
const warnCalls: Array<{ message: string, args?: Record<string, any> }> = []
const recordingLogger = {
info: (message: string, args?: Record<string, any>) => {
infoCalls.push({ message, args })
},
warn: (message: string, args?: Record<string, any>) => {
warnCalls.push({ message, args })
},
error: () => {},
logOperation: () => {},
childLogger: () => recordingLogger,
close: async () => {}
}
const ctx = new MeasureMetricsContext('test', {}, {}, undefined, recordingLogger as any)

const hierarchy = new Hierarchy()
for (const tx of txes) hierarchy.tx(tx)
const db = new ModelDb(hierarchy)
db.addTxes(ctx, txes, true)

// Reset capture and apply a batch of orphan-targeted transactions
infoCalls.length = 0
warnCalls.length = 0

const factory = new TxFactory(core.account.System)
const orphanA = generateId<Doc>()
const orphanB = generateId<Doc>()
const orphanC = generateId<Doc>()
const orphans: Tx[] = [
factory.createTxUpdateDoc(core.class.Space, core.space.Model, orphanA, { name: 'X' } as any),
factory.createTxUpdateDoc(core.class.Space, core.space.Model, orphanA, { name: 'Y' } as any),
factory.createTxUpdateDoc(test.class.Task, core.space.Model, orphanB, { name: 'X' } as any),
factory.createTxRemoveDoc(test.class.Task, core.space.Model, orphanC)
]
db.addTxes(ctx, orphans, true)

// Single coalesced info line (not one warn per orphan)
const orphanInfo = infoCalls.find((c) => c.message.startsWith('skipped model transactions'))
expect(orphanInfo).toBeDefined()
expect(orphanInfo?.args?.total).toEqual(4)
expect(orphanInfo?.args?.byClass).toMatchObject({
[core.class.TxUpdateDoc]: 3,
[core.class.TxRemoveDoc]: 1
})
expect(warnCalls.find((c) => c.message.includes('no document found'))).toBeUndefined()
})

it('addTxes emits no orphan summary when all transactions apply cleanly', async () => {
const { MeasureMetricsContext } = await import('@hcengineering/measurements')

const infoCalls: Array<{ message: string, args?: Record<string, any> }> = []
const recordingLogger = {
info: (message: string, args?: Record<string, any>) => {
infoCalls.push({ message, args })
},
warn: () => {},
error: () => {},
logOperation: () => {},
childLogger: () => recordingLogger,
close: async () => {}
}
const ctx = new MeasureMetricsContext('test', {}, {}, undefined, recordingLogger as any)

const hierarchy = new Hierarchy()
for (const tx of txes) hierarchy.tx(tx)
const db = new ModelDb(hierarchy)
db.addTxes(ctx, txes, true)

// No orphan summary emitted because all model txes apply cleanly
expect(infoCalls.find((c) => c.message.includes('skipped model transactions'))).toBeUndefined()
})
})
40 changes: 29 additions & 11 deletions foundations/core/packages/core/src/memdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,19 @@ export class ModelDb extends MemDb {
}

addTxes (ctx: MeasureContext, txes: Tx[], clone: boolean): void {
// Per-TX orphan warnings were previously logged via ctx.warn at the
// browser console — one line per skipped TX, which on workspaces that
// have evolved through multiple model versions adds up to dozens of
// identical-looking warnings on every page load. The information is
// useful for operators diagnosing migration issues but not actionable
// for end-users, and noisy enough to drown out real warnings.
//
// Replace with a single coalesced summary at the end of the loop:
// count by tx-class and report once. The detailed per-orphan list is
// not emitted here since MeasureContext has no debug channel —
// operators who need it can re-enable a temporary dump via a local
// patch.
const orphans: Array<{ _id: Ref<Doc>, _class: string, objectId: Ref<Doc> }> = []
for (const tx of txes) {
switch (tx._class) {
case core.class.TxCreateDoc:
Expand All @@ -338,19 +351,15 @@ export class ModelDb extends MemDb {
this.updateDoc(cud.objectId, doc, cud)
TxProcessor.updateDoc2Doc(doc, cud)
} else {
ctx.warn('no document found, failed to apply model transaction, skipping', {
_id: tx._id,
_class: tx._class,
objectId: cud.objectId
})
orphans.push({ _id: tx._id, _class: tx._class, objectId: cud.objectId })
}
break
}
case core.class.TxRemoveDoc:
try {
this.delDoc((tx as TxRemoveDoc<Doc>).objectId)
} catch (err: any) {
ctx.warn('no document found, failed to apply model transaction, skipping', {
orphans.push({
_id: tx._id,
_class: tx._class,
objectId: (tx as TxRemoveDoc<Doc>).objectId
Expand All @@ -364,16 +373,25 @@ export class ModelDb extends MemDb {
this.updateDoc(mix.objectId, doc, mix)
TxProcessor.updateMixin4Doc(doc, mix)
} else {
ctx.warn('no document found, failed to apply model transaction, skipping', {
_id: tx._id,
_class: tx._class,
objectId: mix.objectId
})
orphans.push({ _id: tx._id, _class: tx._class, objectId: mix.objectId })
}
break
}
}
}
if (orphans.length > 0) {
const byClass: Record<string, number> = {}
for (const o of orphans) byClass[o._class] = (byClass[o._class] ?? 0) + 1
// Single coalesced info line per addTxes call. The previous per-TX
// `ctx.warn` produced one console line per orphan on every page
// load. MeasureContext has no `debug` channel, so a deeper dump for
// diagnostics isn't available here — operators who need the per-TX
// list can re-enable it via a local patch.
ctx.info('skipped model transactions for orphan documents', {
total: orphans.length,
byClass
})
}
}

protected async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
Expand Down