From fecdf19f74fcbcf3d202321415a94c2516e7e1e8 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Wed, 24 Jun 2026 05:51:49 +0000 Subject: [PATCH] fix(core): coalesce orphan-tx warnings in ModelDb.addTxes Per-TX 'no document found' warnings were previously emitted via ctx.warn for every orphan-targeted transaction the loader skipped. On workspaces that have evolved through multiple model versions this produces dozens of identical-looking warnings on every page load, drowning out real warnings without giving operators an actionable signal. Replace with a single coalesced ctx.info summary at the end of addTxes: total count plus a byClass breakdown. The information needed for diagnosing migration issues is still present, just consolidated. Signed-off-by: Michael Uray --- .../packages/core/src/__tests__/memdb.test.ts | 78 +++++++++++++++++++ foundations/core/packages/core/src/memdb.ts | 40 +++++++--- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/foundations/core/packages/core/src/__tests__/memdb.test.ts b/foundations/core/packages/core/src/__tests__/memdb.test.ts index ae17e9ef2c9..7f64fe4a5dc 100644 --- a/foundations/core/packages/core/src/__tests__/memdb.test.ts +++ b/foundations/core/packages/core/src/__tests__/memdb.test.ts @@ -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 }> = [] + const warnCalls: Array<{ message: string, args?: Record }> = [] + const recordingLogger = { + info: (message: string, args?: Record) => { + infoCalls.push({ message, args }) + }, + warn: (message: string, args?: Record) => { + 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() + const orphanB = generateId() + const orphanC = generateId() + 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 }> = [] + const recordingLogger = { + info: (message: string, args?: Record) => { + 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() + }) }) diff --git a/foundations/core/packages/core/src/memdb.ts b/foundations/core/packages/core/src/memdb.ts index 8e07c322664..5a0896e9197 100644 --- a/foundations/core/packages/core/src/memdb.ts +++ b/foundations/core/packages/core/src/memdb.ts @@ -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, _class: string, objectId: Ref }> = [] for (const tx of txes) { switch (tx._class) { case core.class.TxCreateDoc: @@ -338,11 +351,7 @@ 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 } @@ -350,7 +359,7 @@ export class ModelDb extends MemDb { try { this.delDoc((tx as TxRemoveDoc).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).objectId @@ -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 = {} + 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): Promise {