compact-table.ts3.2 KBView on GitHub /**
* "Compact" for a table document — the manual trigger for the server's Y.Doc rebuild.
*
* A table's `content_yjs` grows with EDIT COUNT, not row count: interleaved cell rewrites
* defeat Y.js's item merging, so a static 200-row table whose statuses churn accrues bytes
* while the row count never moves (measured at ~9.7 bytes per interleaved write — see
* `apps/server/src/services/documents/table/table-compaction.ts`). Compaction rebuilds the
* doc from its current schema and rows, and the server normally does it for itself on the
* next write after the bloat detector raises `metadata.compactionDue`. This is the manual
* override for the case where someone is looking at a table that has visibly bloated and does
* not want to wait for the next write to trigger it.
*
* ── Why a narrowed client interface rather than the typed proxy ──
* Same reason `download-table.ts` does it: the call is described by the one field it sends and
* the four it reads, so a test double is a plain object rather than a mock of the tRPC proxy.
* Here it also buys the toolbar independence from the exact moment the procedure lands on
* `AppRouter` — the shape is the contract, and it is written down once, here.
*
* ── The procedure: `documents.compactTable({ documentId })` ──
* A `privateProcedure`, deliberately separate from the staff-only `admin.tables.compact`
* (which is path-keyed, for an operator investigating someone else's table). This one is a
* user acting on their own document, so it cannot be staff-gated — and the view holds a
* documentId, not a path. Both call the same `compactTableDocument` service, so there is
* one compaction implementation.
*
* It passes `force: true`, because a human clicking the button means the bloat threshold is
* not a reason to skip. What can still decline is an attached viewer — replacing server
* state under an open editor would discard its unflushed edits — which `canCompactNow`
* defers and the returned `note` names. That arrives here as `success: false`, and the
* toolbar reports it as deferred rather than as done.
*/
export interface TableCompactionResult {
/** False when the server declined — e.g. a subscriber was attached, so it deferred. */
success: boolean;
bytesBefore: number;
bytesAfter: number;
/** `bytesBefore - bytesAfter`, computed server-side so the two cannot disagree. */
reclaimedBytes: number;
}
/** The client surface `compactTableDocument` needs, narrowed so callers can pass a double. */
export interface TableCompactClient {
documents: {
compactTable: {
mutate: (input: { documentId: string }) => Promise<TableCompactionResult>;
};
};
}
export function compactTableDocument(
client: TableCompactClient,
documentId: string,
): Promise<TableCompactionResult> {
return client.documents.compactTable.mutate({ documentId });
}
/**
* Bytes, for a human reading a toast. Same thresholds and precision as the attachment-size
* formatter in `modules/files`, so two byte counts in the same product read the same way.
*/
export function formatByteCount(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}