@tita-agent/dsh-data-table 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +16766 -0
- package/lib/contract-envelope.d.ts +91 -0
- package/lib/contract-envelope.js +106 -0
- package/lib/contract-layout.d.ts +130 -0
- package/lib/contract-layout.js +138 -0
- package/lib/contract-refs.d.ts +39 -0
- package/lib/contract-refs.js +9 -0
- package/lib/contract-version.d.ts +311 -0
- package/lib/contract-version.js +270 -0
- package/lib/contract-wire.d.ts +147 -0
- package/lib/contract-wire.js +99 -0
- package/lib/contracts.d.ts +12 -0
- package/lib/contracts.js +12 -0
- package/lib/types/audit.d.ts +42 -0
- package/lib/types/audit.js +39 -0
- package/lib/types/client/ProjectImportMenu.d.ts +30 -0
- package/lib/types/client/ProjectImportMenu.js +70 -0
- package/lib/types/client/SheetPanel.d.ts +33 -0
- package/lib/types/client/SheetPanel.js +1766 -0
- package/lib/types/client/SheetToggle.d.ts +18 -0
- package/lib/types/client/SheetToggle.js +26 -0
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +110 -0
- package/lib/types/client/persistence/changes.d.ts +15 -0
- package/lib/types/client/persistence/changes.js +51 -0
- package/lib/types/client/persistence/ids.d.ts +34 -0
- package/lib/types/client/persistence/ids.js +48 -0
- package/lib/types/client/persistence/merge.d.ts +35 -0
- package/lib/types/client/persistence/merge.js +362 -0
- package/lib/types/client/store.d.ts +61 -0
- package/lib/types/client/store.js +152 -0
- package/lib/types/client/surface-context.d.ts +27 -0
- package/lib/types/client/surface-context.js +104 -0
- package/lib/types/client/surface-control.d.ts +2 -0
- package/lib/types/client/surface-control.js +12 -0
- package/lib/types/client/transport/workbookApi.d.ts +159 -0
- package/lib/types/client/transport/workbookApi.js +246 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
- package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
- package/lib/types/client/univer/WorkbookCanvas.js +87 -0
- package/lib/types/client/univer/WorkbookView.d.ts +3 -0
- package/lib/types/client/univer/WorkbookView.js +2 -0
- package/lib/types/client/univer/loader.d.ts +18 -0
- package/lib/types/client/univer/loader.js +81 -0
- package/lib/types/client/univer/standalone-entry.d.ts +1 -0
- package/lib/types/client/univer/standalone-entry.js +19 -0
- package/lib/types/client/versions/VersionChrome.d.ts +33 -0
- package/lib/types/client/versions/VersionChrome.js +21 -0
- package/lib/types/client/versions/inspect.d.ts +70 -0
- package/lib/types/client/versions/inspect.js +88 -0
- package/lib/types/client/versions/preview-decoration.d.ts +11 -0
- package/lib/types/client/versions/preview-decoration.js +54 -0
- package/lib/types/client/versions/records.d.ts +19 -0
- package/lib/types/client/versions/records.js +70 -0
- package/lib/types/client/workspace-host.d.ts +61 -0
- package/lib/types/client/workspace-host.js +124 -0
- package/lib/types/collaboration-hub.d.ts +31 -0
- package/lib/types/collaboration-hub.js +69 -0
- package/lib/types/contract/ids.d.ts +24 -0
- package/lib/types/contract/ids.js +46 -0
- package/lib/types/contract/mutations.d.ts +25 -0
- package/lib/types/contract/mutations.js +782 -0
- package/lib/types/contract/operations.d.ts +54 -0
- package/lib/types/contract/operations.js +203 -0
- package/lib/types/contract/snapshots.d.ts +176 -0
- package/lib/types/contract/snapshots.js +351 -0
- package/lib/types/contract/univer-api-catalog.d.ts +29 -0
- package/lib/types/contract/univer-api-catalog.js +264 -0
- package/lib/types/errors.d.ts +8 -0
- package/lib/types/errors.js +12 -0
- package/lib/types/http/assets-routes.d.ts +3 -0
- package/lib/types/http/assets-routes.js +169 -0
- package/lib/types/http/collaboration.d.ts +10 -0
- package/lib/types/http/collaboration.js +154 -0
- package/lib/types/http/history.d.ts +15 -0
- package/lib/types/http/history.js +134 -0
- package/lib/types/http/project-files.d.ts +79 -0
- package/lib/types/http/project-files.js +206 -0
- package/lib/types/http/project-scope.d.ts +50 -0
- package/lib/types/http/project-scope.js +52 -0
- package/lib/types/http/routes.d.ts +98 -0
- package/lib/types/http/routes.js +323 -0
- package/lib/types/http/standalone.d.ts +9 -0
- package/lib/types/http/standalone.js +31 -0
- package/lib/types/http/trunk.d.ts +10 -0
- package/lib/types/http/trunk.js +322 -0
- package/lib/types/http/worktree.d.ts +9 -0
- package/lib/types/http/worktree.js +145 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/index.js +156 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/invariant.js +15 -0
- package/lib/types/request-params.d.ts +21 -0
- package/lib/types/request-params.js +76 -0
- package/lib/types/store/paths.d.ts +68 -0
- package/lib/types/store/paths.js +148 -0
- package/lib/types/store/project.d.ts +88 -0
- package/lib/types/store/project.js +120 -0
- package/lib/types/store/read.d.ts +110 -0
- package/lib/types/store/read.js +404 -0
- package/lib/types/store/retention.d.ts +27 -0
- package/lib/types/store/retention.js +111 -0
- package/lib/types/store/write.d.ts +22 -0
- package/lib/types/store/write.js +210 -0
- package/lib/types/tools/arguments.d.ts +40 -0
- package/lib/types/tools/arguments.js +130 -0
- package/lib/types/tools/cells.d.ts +50 -0
- package/lib/types/tools/cells.js +298 -0
- package/lib/types/tools/index.d.ts +2 -0
- package/lib/types/tools/index.js +250 -0
- package/lib/types/tools/render.d.ts +47 -0
- package/lib/types/tools/render.js +290 -0
- package/lib/types/tools/univer-api.d.ts +18 -0
- package/lib/types/tools/univer-api.js +198 -0
- package/lib/types/tools/workbook-catalog.d.ts +3 -0
- package/lib/types/tools/workbook-catalog.js +83 -0
- package/lib/types/tools/workbook-edit.d.ts +192 -0
- package/lib/types/tools/workbook-edit.js +912 -0
- package/lib/types/tools/workbook-find.d.ts +11 -0
- package/lib/types/tools/workbook-find.js +89 -0
- package/lib/types/tools/workbook-history.d.ts +2 -0
- package/lib/types/tools/workbook-history.js +59 -0
- package/lib/types/tools/workbook-read.d.ts +21 -0
- package/lib/types/tools/workbook-read.js +204 -0
- package/lib/types/types.d.ts +377 -0
- package/lib/types/types.js +34 -0
- package/lib/types/univer/apply-operation.d.ts +14 -0
- package/lib/types/univer/apply-operation.js +398 -0
- package/lib/types/univer/chunk.d.ts +68 -0
- package/lib/types/univer/chunk.js +16 -0
- package/lib/types/univer/decoration.d.ts +34 -0
- package/lib/types/univer/decoration.js +88 -0
- package/lib/types/univer/diagnostics.d.ts +31 -0
- package/lib/types/univer/diagnostics.js +141 -0
- package/lib/types/univer/engine.d.ts +33 -0
- package/lib/types/univer/engine.js +338 -0
- package/lib/types/univer/entry.d.ts +8 -0
- package/lib/types/univer/entry.js +11 -0
- package/lib/types/univer/facade.d.ts +107 -0
- package/lib/types/univer/facade.js +1 -0
- package/lib/types/univer/fonts.d.ts +202 -0
- package/lib/types/univer/fonts.js +140 -0
- package/lib/types/univer/plugins/collaboration.d.ts +23 -0
- package/lib/types/univer/plugins/collaboration.js +47 -0
- package/lib/types/univer/plugins/export.d.ts +39 -0
- package/lib/types/univer/plugins/export.js +246 -0
- package/lib/types/univer/plugins/history.d.ts +24 -0
- package/lib/types/univer/plugins/history.js +50 -0
- package/lib/types/univer/plugins/version.d.ts +13 -0
- package/lib/types/univer/plugins/version.js +13 -0
- package/lib/types/version/diff.d.ts +36 -0
- package/lib/types/version/diff.js +156 -0
- package/lib/types/version/grouping.d.ts +7 -0
- package/lib/types/version/grouping.js +94 -0
- package/lib/types/version/records.d.ts +17 -0
- package/lib/types/version/records.js +102 -0
- package/lib/types/version/session.d.ts +36 -0
- package/lib/types/version/session.js +89 -0
- package/lib/types/version/summary.d.ts +24 -0
- package/lib/types/version/summary.js +111 -0
- package/lib/types/workbook-manager.d.ts +191 -0
- package/lib/types/workbook-manager.js +935 -0
- package/lib/types/xlsx-export-worker.d.ts +5 -0
- package/lib/types/xlsx-export-worker.js +24 -0
- package/lib/types/xlsx-export.d.ts +21 -0
- package/lib/types/xlsx-export.js +371 -0
- package/lib/types/xlsx-import-worker.d.ts +1 -0
- package/lib/types/xlsx-import-worker.js +19 -0
- package/lib/univer-client.js +690 -0
- package/lib/workbook-page.js +23826 -0
- package/lib/xlsx-export-worker.js +104 -0
- package/lib/xlsx-import-worker.js +46 -0
- package/package.json +106 -3
- package/src/audit.ts +58 -0
- package/src/client/ProjectImportMenu.tsx +100 -0
- package/src/client/SheetPanel.module.css +1578 -0
- package/src/client/SheetPanel.tsx +1934 -0
- package/src/client/SheetToggle.module.css +73 -0
- package/src/client/SheetToggle.tsx +65 -0
- package/src/client/contract.d.ts +19 -0
- package/src/client/index.ts +131 -0
- package/src/client/persistence/changes.ts +58 -0
- package/src/client/persistence/ids.ts +60 -0
- package/src/client/persistence/merge.ts +392 -0
- package/src/client/store.ts +188 -0
- package/src/client/surface-context.ts +123 -0
- package/src/client/surface-control.ts +13 -0
- package/src/client/transport/workbookApi.ts +355 -0
- package/src/client/univer/SheetView.module.css +110 -0
- package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
- package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
- package/src/client/univer/WorkbookCanvas.tsx +128 -0
- package/src/client/univer/WorkbookView.module.css +30 -0
- package/src/client/univer/WorkbookView.tsx +3 -0
- package/src/client/univer/loader.ts +90 -0
- package/src/client/univer/react-dom-client.d.ts +10 -0
- package/src/client/univer/standalone-entry.tsx +19 -0
- package/src/client/univer/standalone.css +12 -0
- package/src/client/univer/univer-locales.d.ts +89 -0
- package/src/client/univer/univer-modules.d.ts +26 -0
- package/src/client/versions/VersionChrome.tsx +114 -0
- package/src/client/versions/inspect.ts +138 -0
- package/src/client/versions/preview-decoration.ts +54 -0
- package/src/client/versions/records.ts +78 -0
- package/src/client/workspace-host.ts +147 -0
- package/src/collaboration-hub.ts +83 -0
- package/src/contract/ids.ts +50 -0
- package/src/contract/mutations.ts +791 -0
- package/src/contract/operations.ts +221 -0
- package/src/contract/snapshots.ts +383 -0
- package/src/contract/univer-api-catalog.ts +289 -0
- package/src/css-modules.d.ts +6 -0
- package/src/errors.ts +25 -0
- package/src/http/assets-routes.ts +177 -0
- package/src/http/collaboration.ts +144 -0
- package/src/http/history.ts +151 -0
- package/src/http/project-files.ts +227 -0
- package/src/http/project-scope.ts +77 -0
- package/src/http/routes.ts +364 -0
- package/src/http/standalone.ts +32 -0
- package/src/http/trunk.ts +322 -0
- package/src/http/worktree.ts +119 -0
- package/src/index.ts +165 -0
- package/src/invariant.ts +17 -0
- package/src/request-params.ts +92 -0
- package/src/store/paths.ts +165 -0
- package/src/store/project.ts +137 -0
- package/src/store/read.ts +454 -0
- package/src/store/retention.ts +106 -0
- package/src/store/write.ts +230 -0
- package/src/tools/arguments.ts +128 -0
- package/src/tools/cells.ts +298 -0
- package/src/tools/index.ts +274 -0
- package/src/tools/render.ts +301 -0
- package/src/tools/univer-api.ts +243 -0
- package/src/tools/workbook-catalog.ts +94 -0
- package/src/tools/workbook-edit.ts +987 -0
- package/src/tools/workbook-find.ts +80 -0
- package/src/tools/workbook-history.ts +63 -0
- package/src/tools/workbook-read.ts +210 -0
- package/src/types.ts +432 -0
- package/src/univer/apply-operation.ts +369 -0
- package/src/univer/chunk.ts +77 -0
- package/src/univer/decoration.ts +111 -0
- package/src/univer/diagnostics.ts +136 -0
- package/src/univer/engine.ts +370 -0
- package/src/univer/entry.ts +14 -0
- package/src/univer/facade.ts +91 -0
- package/src/univer/fonts.ts +148 -0
- package/src/univer/overrides.css +33 -0
- package/src/univer/plugins/collaboration.ts +47 -0
- package/src/univer/plugins/export.ts +236 -0
- package/src/univer/plugins/history.ts +49 -0
- package/src/univer/plugins/version.ts +13 -0
- package/src/version/diff.ts +169 -0
- package/src/version/grouping.ts +96 -0
- package/src/version/records.ts +113 -0
- package/src/version/session.ts +90 -0
- package/src/version/summary.ts +117 -0
- package/src/workbook-manager.ts +1111 -0
- package/src/xlsx-export-worker.ts +34 -0
- package/src/xlsx-export.ts +373 -0
- package/src/xlsx-import-worker.ts +30 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a workbook, its sidecar index, and its catalog (docs/issue/0022 P1).
|
|
3
|
+
*
|
|
4
|
+
* The in-process cache lives here because the DSH host is the single mutation
|
|
5
|
+
* authority: a reader either sees a workbook's committed state or misses, and
|
|
6
|
+
* `store/write.ts` is the only thing that invalidates it.
|
|
7
|
+
*
|
|
8
|
+
* The write-ahead journal's *lock* and *recovery* are here too: `readWorkbook`
|
|
9
|
+
* must finish an interrupted commit before it reads, and `store/write.ts`
|
|
10
|
+
* takes the same lock to commit, so the two cannot race. The direction is
|
|
11
|
+
* one-way — `read` never imports `write`.
|
|
12
|
+
*/
|
|
13
|
+
import { DataSheetError } from '../errors.ts'
|
|
14
|
+
import { access, readFile, readdir, rename, unlink } from 'node:fs/promises'
|
|
15
|
+
import {
|
|
16
|
+
type SheetSchema,
|
|
17
|
+
type WorkbookChange,
|
|
18
|
+
type WorkbookMeta,
|
|
19
|
+
type WorkbookOperation,
|
|
20
|
+
type WorkbookOperationType,
|
|
21
|
+
type WorkbookProposal,
|
|
22
|
+
type WorkbookSummary,
|
|
23
|
+
type WorkbookWorktree,
|
|
24
|
+
type WorkbookWorktreeStatus,
|
|
25
|
+
type WorkbookWorktreeSummary,
|
|
26
|
+
} from '../types.ts'
|
|
27
|
+
import { normalizeWorkbookId } from '../contract/ids.ts'
|
|
28
|
+
import { cloneSnapshot, isSheetSchema, normalizeSnapshotName } from '../contract/snapshots.ts'
|
|
29
|
+
import { applyWorkbookOperation } from '../contract/mutations.ts'
|
|
30
|
+
import {
|
|
31
|
+
workbookCheckpointsPath,
|
|
32
|
+
workbookIndexPath,
|
|
33
|
+
workbookOperationsPath,
|
|
34
|
+
workbookPath,
|
|
35
|
+
workbookProposalsPath,
|
|
36
|
+
workbookSnapshotPath,
|
|
37
|
+
workbookTransactionPath,
|
|
38
|
+
workbookRoot,
|
|
39
|
+
workbookWorktreeDirectory,
|
|
40
|
+
workbookWorktreePath,
|
|
41
|
+
} from './paths.ts'
|
|
42
|
+
|
|
43
|
+
/** Whether a workbook file exists. */
|
|
44
|
+
export async function workbookExists(id: string): Promise<boolean> {
|
|
45
|
+
return fileExists(workbookPath(id))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read only the sidecar index; a missing/invalid sidecar falls back safely. */
|
|
49
|
+
export async function readWorkbookIndex(id: string): Promise<WorkbookIndex | null> {
|
|
50
|
+
try {
|
|
51
|
+
await withWorkbookTransactionQueue(normalizeWorkbookId(id), () => recoverWorkbookTransactionNow(normalizeWorkbookId(id)))
|
|
52
|
+
const parsed = JSON.parse(await readFile(workbookIndexPath(id), 'utf8')) as Partial<WorkbookIndex>
|
|
53
|
+
if (typeof parsed.id !== 'string' || typeof parsed.name !== 'string' || !Number.isInteger(parsed.revision)
|
|
54
|
+
|| !Array.isArray(parsed.sheets) || !Array.isArray(parsed.operations) || !Array.isArray(parsed.proposals)) return null
|
|
55
|
+
return {
|
|
56
|
+
id: parsed.id,
|
|
57
|
+
name: parsed.name,
|
|
58
|
+
sheetOrder: Array.isArray(parsed.sheetOrder) ? parsed.sheetOrder.filter(item => typeof item === 'string') : parsed.sheets.map(sheet => sheet.id),
|
|
59
|
+
sheets: parsed.sheets.filter(isStoredSheetSummary),
|
|
60
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
|
|
61
|
+
revision: parsed.revision as number,
|
|
62
|
+
operations: parsed.operations.filter(operation => isStoredIndexOperation(operation)),
|
|
63
|
+
proposals: parsed.proposals.filter(isStoredIndexProposal),
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// A missing or malformed sidecar is not an error: the caller learns "no
|
|
67
|
+
// workbook here" and moves on. A *corrupt transaction journal* is, and it
|
|
68
|
+
// used to be swallowed by this same catch — so a workbook that still had
|
|
69
|
+
// every one of its files simply disappeared from the catalog and
|
|
70
|
+
// `workbook_read` answered 「工作簿不存在」, three hand-written error messages
|
|
71
|
+
// away from the reader.
|
|
72
|
+
if (error instanceof DataSheetError) throw error
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Read one workbook meta. */
|
|
78
|
+
export async function readWorkbook(id: string, options: { includeCheckpoints?: boolean } = {}): Promise<WorkbookMeta | null> {
|
|
79
|
+
try {
|
|
80
|
+
await withWorkbookTransactionQueue(normalizeWorkbookId(id), () => recoverWorkbookTransactionNow(normalizeWorkbookId(id)))
|
|
81
|
+
const cached = workbookCache.get(normalizeWorkbookId(id))
|
|
82
|
+
if (cached !== undefined) {
|
|
83
|
+
if (options.includeCheckpoints === true && !cached.checkpointsLoaded) {
|
|
84
|
+
const stored = await readJsonFile(workbookCheckpointsPath(id)) as { checkpoints?: unknown } | null
|
|
85
|
+
cached.meta.checkpoints = Array.isArray(stored?.checkpoints)
|
|
86
|
+
? stored.checkpoints.filter(checkpoint => isStoredCheckpoint(checkpoint))
|
|
87
|
+
: []
|
|
88
|
+
cached.checkpointsLoaded = true
|
|
89
|
+
}
|
|
90
|
+
return cached.meta
|
|
91
|
+
}
|
|
92
|
+
const raw = await readFile(workbookPath(id), 'utf8')
|
|
93
|
+
const parsed = JSON.parse(raw) as WorkbookMeta
|
|
94
|
+
if (typeof parsed.id !== 'string' || typeof parsed.name !== 'string') return null
|
|
95
|
+
let snapshot = parsed.snapshot
|
|
96
|
+
let snapshotRevision = 0
|
|
97
|
+
if (snapshot === undefined) {
|
|
98
|
+
const stored = await readJsonFile(workbookSnapshotPath(id)) as { revision?: unknown; snapshot?: unknown } | null
|
|
99
|
+
if (stored === null || !Object.prototype.hasOwnProperty.call(stored, 'snapshot')) return null
|
|
100
|
+
snapshot = stored.snapshot
|
|
101
|
+
snapshotRevision = typeof stored.revision === 'number' && Number.isInteger(stored.revision) ? stored.revision : 0
|
|
102
|
+
}
|
|
103
|
+
const storedCheckpoints = options.includeCheckpoints === true
|
|
104
|
+
? await readJsonFile(workbookCheckpointsPath(id)) as { checkpoints?: unknown } | null
|
|
105
|
+
: null
|
|
106
|
+
const rawCheckpoints = Array.isArray(parsed.checkpoints)
|
|
107
|
+
? parsed.checkpoints
|
|
108
|
+
: Array.isArray(storedCheckpoints?.checkpoints) ? storedCheckpoints.checkpoints : []
|
|
109
|
+
const storedOperations = Array.isArray(parsed.operations)
|
|
110
|
+
? parsed.operations
|
|
111
|
+
: await readJsonFile(workbookOperationsPath(id))
|
|
112
|
+
const operations = Array.isArray(storedOperations)
|
|
113
|
+
? storedOperations.filter(operation => isStoredOperation(operation))
|
|
114
|
+
: []
|
|
115
|
+
const storedProposals = Array.isArray(parsed.proposals)
|
|
116
|
+
? parsed.proposals
|
|
117
|
+
: await readJsonFile(workbookProposalsPath(id))
|
|
118
|
+
if (parsed.snapshot === undefined && snapshotRevision < (parsed.revision ?? 0)) {
|
|
119
|
+
for (const operation of operations) {
|
|
120
|
+
if (operation.revision <= snapshotRevision) continue
|
|
121
|
+
snapshot = applyWorkbookOperation(snapshot, operation)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const result: WorkbookMeta = {
|
|
125
|
+
...parsed,
|
|
126
|
+
snapshot: normalizeSnapshotName(snapshot, parsed.name),
|
|
127
|
+
revision: Number.isInteger(parsed.revision) && (parsed.revision as number) >= 0 ? parsed.revision : 0,
|
|
128
|
+
snapshotRevision: parsed.snapshot === undefined ? snapshotRevision : (typeof parsed.snapshotRevision === 'number' ? parsed.snapshotRevision : parsed.revision ?? 0),
|
|
129
|
+
operations,
|
|
130
|
+
proposals: Array.isArray(storedProposals)
|
|
131
|
+
? storedProposals.filter(proposal => isStoredProposal(proposal))
|
|
132
|
+
: [],
|
|
133
|
+
checkpoints: rawCheckpoints.filter(checkpoint => isStoredCheckpoint(checkpoint)),
|
|
134
|
+
}
|
|
135
|
+
workbookCache.set(normalizeWorkbookId(id), { meta: result, checkpointsLoaded: Array.isArray(parsed.checkpoints) || options.includeCheckpoints === true })
|
|
136
|
+
return result
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// See `readWorkbookIndex`: only the deliberate persistence failures escape.
|
|
139
|
+
if (error instanceof DataSheetError) throw error
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Reconstruct a historical workbook revision from the nearest checkpoint and
|
|
146
|
+
* the retained operation tail. A null result means the requested revision has
|
|
147
|
+
* been compacted or contains an opaque snapshot replacement.
|
|
148
|
+
*/
|
|
149
|
+
export async function readWorkbookAtRevision(workbookId: string, targetRevision: number): Promise<{ snapshot: unknown; revision: number } | null> {
|
|
150
|
+
const meta = await readWorkbook(workbookId, { includeCheckpoints: true })
|
|
151
|
+
if (meta === null) return null
|
|
152
|
+
const currentRevision = meta.revision ?? 0
|
|
153
|
+
if (!Number.isInteger(targetRevision) || targetRevision < 0 || targetRevision > currentRevision) return null
|
|
154
|
+
if (targetRevision === currentRevision) return { snapshot: cloneSnapshot(meta.snapshot), revision: currentRevision }
|
|
155
|
+
|
|
156
|
+
// The snapshot sidecar is itself a durable checkpoint. Older workbooks may
|
|
157
|
+
// have been written before the in-memory checkpoint index existed, so use it
|
|
158
|
+
// directly when the requested revision is exactly the sidecar revision.
|
|
159
|
+
const sidecar = await readJsonFile(workbookSnapshotPath(workbookId)) as { revision?: unknown; snapshot?: unknown } | null
|
|
160
|
+
if (sidecar !== null && sidecar.snapshot !== undefined && sidecar.revision === targetRevision) {
|
|
161
|
+
return { snapshot: cloneSnapshot(sidecar.snapshot), revision: targetRevision }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const checkpoint = [...(meta.checkpoints ?? [])]
|
|
165
|
+
.filter(item => item.revision <= targetRevision)
|
|
166
|
+
.sort((left, right) => right.revision - left.revision)[0]
|
|
167
|
+
if (checkpoint === undefined) return null
|
|
168
|
+
let snapshot = cloneSnapshot(checkpoint.snapshot)
|
|
169
|
+
const operations = (meta.operations ?? [])
|
|
170
|
+
.filter(operation => operation.revision > checkpoint.revision && operation.revision <= targetRevision)
|
|
171
|
+
.sort((left, right) => left.revision - right.revision)
|
|
172
|
+
if (operations.length !== targetRevision - checkpoint.revision) return null
|
|
173
|
+
for (const operation of operations) {
|
|
174
|
+
if (operation.type === 'snapshot_replace' && operation.payload.snapshot === undefined) return null
|
|
175
|
+
snapshot = applyWorkbookOperation(snapshot, operation)
|
|
176
|
+
}
|
|
177
|
+
return { snapshot, revision: targetRevision }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** List workbook ids (files in the repo root, sorted by name). */
|
|
181
|
+
export async function listWorkbooks(): Promise<string[]> {
|
|
182
|
+
try {
|
|
183
|
+
const entries = await readdir(workbookRoot(), { withFileTypes: true })
|
|
184
|
+
return entries
|
|
185
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.json')
|
|
186
|
+
&& !entry.name.endsWith('.index.json') && !entry.name.endsWith('.snapshot.json')
|
|
187
|
+
&& !entry.name.endsWith('.checkpoints.json') && !entry.name.endsWith('.operations.json')
|
|
188
|
+
&& !entry.name.endsWith('.proposals.json') && !entry.name.endsWith('.transaction.json'))
|
|
189
|
+
.map(entry => entry.name.slice(0, -'.json'.length))
|
|
190
|
+
.sort()
|
|
191
|
+
} catch {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** List workbook summaries for the browser picker and other read-only clients. */
|
|
197
|
+
export async function listWorkbookSummaries(): Promise<WorkbookSummary[]> {
|
|
198
|
+
const summaries: WorkbookSummary[] = []
|
|
199
|
+
for (const id of await listWorkbooks()) {
|
|
200
|
+
const index = await readWorkbookIndex(id)
|
|
201
|
+
if (index !== null) summaries.push({ id: index.id, name: index.name, sheets: index.sheets, updatedAt: index.updatedAt, revision: index.revision })
|
|
202
|
+
else {
|
|
203
|
+
const meta = await readWorkbook(id)
|
|
204
|
+
if (meta !== null) summaries.push(summarize(meta))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return summaries
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** List worktrees belonging to a workbook, optionally scoped to one session. */
|
|
211
|
+
export async function listWorkbookWorktrees(workbookId: string, ownerSessionId?: string): Promise<WorkbookWorktreeSummary[]> {
|
|
212
|
+
const normalizedWorkbookId = normalizeWorkbookId(workbookId)
|
|
213
|
+
try {
|
|
214
|
+
const entries = await readdir(workbookWorktreeDirectory(normalizedWorkbookId), { withFileTypes: true })
|
|
215
|
+
const result: WorkbookWorktreeSummary[] = []
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue
|
|
218
|
+
const worktree = await readWorkbookWorktree(normalizedWorkbookId, entry.name.slice(0, -'.json'.length))
|
|
219
|
+
if (worktree === null || ownerSessionId !== undefined && worktree.ownerSessionId !== ownerSessionId) continue
|
|
220
|
+
result.push(toWorktreeSummary(worktree))
|
|
221
|
+
}
|
|
222
|
+
return result.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
223
|
+
} catch {
|
|
224
|
+
return []
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Read one isolated worktree, including its snapshot and operation tail. */
|
|
229
|
+
export async function readWorkbookWorktree(workbookId: string, worktreeId: string): Promise<WorkbookWorktree | null> {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(await readFile(workbookWorktreePath(workbookId, worktreeId), 'utf8')) as Partial<WorkbookWorktree>
|
|
232
|
+
if (typeof parsed.id !== 'string' || typeof parsed.workbookId !== 'string'
|
|
233
|
+
|| typeof parsed.ownerSessionId !== 'string' || typeof parsed.name !== 'string'
|
|
234
|
+
|| !Number.isInteger(parsed.baseRevision) || !Number.isInteger(parsed.revision)
|
|
235
|
+
|| !isWorktreeStatus(parsed.status) || typeof parsed.createdAt !== 'string'
|
|
236
|
+
|| typeof parsed.updatedAt !== 'string' || parsed.snapshot === undefined) return null
|
|
237
|
+
return {
|
|
238
|
+
id: parsed.id,
|
|
239
|
+
workbookId: normalizeWorkbookId(parsed.workbookId),
|
|
240
|
+
ownerSessionId: parsed.ownerSessionId,
|
|
241
|
+
name: parsed.name,
|
|
242
|
+
baseRevision: parsed.baseRevision as number,
|
|
243
|
+
revision: parsed.revision as number,
|
|
244
|
+
status: parsed.status,
|
|
245
|
+
createdAt: parsed.createdAt,
|
|
246
|
+
updatedAt: parsed.updatedAt,
|
|
247
|
+
snapshot: parsed.snapshot,
|
|
248
|
+
operations: Array.isArray(parsed.operations) ? parsed.operations.filter(isStoredOperation) : [],
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function readJsonFile(path: string): Promise<unknown | null> {
|
|
256
|
+
try { return JSON.parse(await readFile(path, 'utf8')) as unknown } catch { return null }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function fileExists(path: string): Promise<boolean> {
|
|
260
|
+
try { await access(path); return true } catch { return false }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Derive a lightweight summary from a stored workbook. */
|
|
264
|
+
export function summarize(meta: WorkbookMeta): WorkbookSummary {
|
|
265
|
+
const snap = meta.snapshot as {
|
|
266
|
+
sheetOrder?: readonly string[]
|
|
267
|
+
sheets?: Record<string, { id?: string; name?: string; dataSheetSchema?: unknown }>
|
|
268
|
+
} | null
|
|
269
|
+
const order = snap?.sheetOrder ?? Object.keys(snap?.sheets ?? {})
|
|
270
|
+
const sheets = order.map(id => ({
|
|
271
|
+
id,
|
|
272
|
+
name: snap?.sheets?.[id]?.name ?? id,
|
|
273
|
+
...(isSheetSchema(snap?.sheets?.[id]?.dataSheetSchema) ? { schema: snap?.sheets?.[id]?.dataSheetSchema } : {}),
|
|
274
|
+
}))
|
|
275
|
+
return {
|
|
276
|
+
id: meta.id,
|
|
277
|
+
name: meta.name,
|
|
278
|
+
sheets,
|
|
279
|
+
updatedAt: meta.updatedAt,
|
|
280
|
+
revision: meta.revision ?? 0,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function withWorkbookTransactionQueue<T>(id: string, task: () => Promise<T>): Promise<T> {
|
|
285
|
+
const previous = workbookTransactionQueues.get(id) ?? Promise.resolve()
|
|
286
|
+
const current = previous.catch(() => undefined).then(task)
|
|
287
|
+
workbookTransactionQueues.set(id, current)
|
|
288
|
+
try { return await current } finally {
|
|
289
|
+
if (workbookTransactionQueues.get(id) === current) workbookTransactionQueues.delete(id)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Finish a prepared multi-file commit after a process interruption. */
|
|
294
|
+
export async function recoverWorkbookTransactionNow(id: string): Promise<void> {
|
|
295
|
+
const journalPath = workbookTransactionPath(id)
|
|
296
|
+
let transaction: StoredWorkbookTransaction
|
|
297
|
+
try {
|
|
298
|
+
transaction = JSON.parse(await readFile(journalPath, 'utf8')) as StoredWorkbookTransaction
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
301
|
+
throw new DataSheetError('PERSISTENCE_FAILED', `工作簿事务日志损坏:${id}`, 500)
|
|
302
|
+
}
|
|
303
|
+
if (!Number.isInteger(transaction.revision) || !Array.isArray(transaction.files) || transaction.files.some(file => typeof file?.target !== 'string' || typeof file?.temp !== 'string')) {
|
|
304
|
+
throw new DataSheetError('PERSISTENCE_FAILED', `工作簿事务日志无效:${id}`, 500)
|
|
305
|
+
}
|
|
306
|
+
for (const file of transaction.files) {
|
|
307
|
+
if (await fileExists(file.temp)) await rename(file.temp, file.target)
|
|
308
|
+
else if (!(await fileExists(file.target))) throw new DataSheetError('PERSISTENCE_FAILED', `工作簿事务缺少文件:${file.target}`, 500)
|
|
309
|
+
}
|
|
310
|
+
await unlink(journalPath).catch(error => {
|
|
311
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
312
|
+
})
|
|
313
|
+
workbookCache.delete(normalizeWorkbookId(id))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Mirrored by the browser projection's `applyAppendRows`, which runs the same
|
|
318
|
+
* check against live Univer state. The hint names `headerRowIndex` because that
|
|
319
|
+
* is what unblocks a report sheet whose real header is not row 0.
|
|
320
|
+
*/
|
|
321
|
+
|
|
322
|
+
export function isStoredOperation(value: unknown): value is WorkbookOperation {
|
|
323
|
+
if (typeof value !== 'object' || value === null) return false
|
|
324
|
+
const operation = value as Partial<WorkbookOperation>
|
|
325
|
+
return typeof operation.opId === 'string'
|
|
326
|
+
&& typeof operation.workbookId === 'string'
|
|
327
|
+
&& typeof operation.revision === 'number'
|
|
328
|
+
&& (operation.actor === 'agent' || operation.actor === 'user')
|
|
329
|
+
&& isKnownOperationType(operation.type)
|
|
330
|
+
&& typeof operation.timestamp === 'string'
|
|
331
|
+
&& typeof operation.payload === 'object'
|
|
332
|
+
&& operation.payload !== null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function isStoredIndexOperation(value: unknown): value is WorkbookIndexOperation {
|
|
336
|
+
if (typeof value !== 'object' || value === null) return false
|
|
337
|
+
const operation = value as Partial<WorkbookIndexOperation>
|
|
338
|
+
return typeof operation.opId === 'string'
|
|
339
|
+
&& typeof operation.workbookId === 'string'
|
|
340
|
+
&& typeof operation.revision === 'number'
|
|
341
|
+
&& (operation.actor === 'agent' || operation.actor === 'user')
|
|
342
|
+
&& isKnownOperationType(operation.type)
|
|
343
|
+
&& typeof operation.timestamp === 'string'
|
|
344
|
+
&& typeof operation.summary === 'string'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function isKnownOperationType(value: unknown): value is WorkbookOperationType {
|
|
348
|
+
return value === 'update_cells' || value === 'clear_range' || value === 'delete_cells'
|
|
349
|
+
|| value === 'insert_rows' || value === 'delete_rows' || value === 'insert_columns'
|
|
350
|
+
|| value === 'delete_columns' || value === 'create_sheet' || value === 'delete_sheet'
|
|
351
|
+
|| value === 'rename_sheet' || value === 'append_rows' || value === 'format_cells'
|
|
352
|
+
|| value === 'merge_cells' || value === 'unmerge_cells' || value === 'freeze_panes'
|
|
353
|
+
|| value === 'set_column_width' || value === 'set_row_height' || value === 'sort_range'
|
|
354
|
+
|| value === 'set_hyperlink' || value === 'remove_hyperlink' || value === 'remove_duplicates'
|
|
355
|
+
|| value === 'batch' || value === 'snapshot_replace'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function isStoredSheetSummary(value: unknown): value is { id: string; name: string; schema?: SheetSchema } {
|
|
359
|
+
if (typeof value !== 'object' || value === null) return false
|
|
360
|
+
const sheet = value as { id?: unknown; name?: unknown; schema?: unknown }
|
|
361
|
+
return typeof sheet.id === 'string' && typeof sheet.name === 'string'
|
|
362
|
+
&& (sheet.schema === undefined || isSheetSchema(sheet.schema))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function isStoredIndexProposal(value: unknown): value is WorkbookIndexProposal {
|
|
366
|
+
if (typeof value !== 'object' || value === null) return false
|
|
367
|
+
const proposal = value as Partial<WorkbookIndexProposal>
|
|
368
|
+
return typeof proposal.id === 'string' && typeof proposal.workbookId === 'string'
|
|
369
|
+
&& (proposal.actor === 'agent' || proposal.actor === 'user')
|
|
370
|
+
&& typeof proposal.baseRevision === 'number' && typeof proposal.createdAt === 'string'
|
|
371
|
+
&& typeof proposal.updatedAt === 'string'
|
|
372
|
+
&& (proposal.status === 'pending' || proposal.status === 'committed' || proposal.status === 'rejected' || proposal.status === 'expired')
|
|
373
|
+
&& typeof proposal.summary === 'string' && Array.isArray(proposal.changes)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function isStoredProposal(value: unknown): value is WorkbookProposal {
|
|
377
|
+
if (typeof value !== 'object' || value === null) return false
|
|
378
|
+
const proposal = value as Partial<WorkbookProposal>
|
|
379
|
+
return typeof proposal.id === 'string' && typeof proposal.workbookId === 'string'
|
|
380
|
+
&& (proposal.actor === 'agent' || proposal.actor === 'user')
|
|
381
|
+
&& typeof proposal.baseRevision === 'number' && typeof proposal.createdAt === 'string'
|
|
382
|
+
&& typeof proposal.updatedAt === 'string'
|
|
383
|
+
&& (proposal.status === 'pending' || proposal.status === 'committed' || proposal.status === 'rejected' || proposal.status === 'expired')
|
|
384
|
+
&& Array.isArray(proposal.changes) && 'snapshot' in proposal
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function isStoredCheckpoint(value: unknown): value is { revision: number; actor: 'agent' | 'user'; timestamp: string; snapshot: unknown } {
|
|
388
|
+
if (typeof value !== 'object' || value === null) return false
|
|
389
|
+
const checkpoint = value as { revision?: unknown; actor?: unknown; timestamp?: unknown; snapshot?: unknown }
|
|
390
|
+
return typeof checkpoint.revision === 'number'
|
|
391
|
+
&& (checkpoint.actor === 'agent' || checkpoint.actor === 'user')
|
|
392
|
+
&& typeof checkpoint.timestamp === 'string'
|
|
393
|
+
&& 'snapshot' in checkpoint
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface WorkbookIndex {
|
|
397
|
+
id: string
|
|
398
|
+
name: string
|
|
399
|
+
sheetOrder: string[]
|
|
400
|
+
sheets: Array<{ id: string; name: string; schema?: SheetSchema }>
|
|
401
|
+
updatedAt: string
|
|
402
|
+
revision: number
|
|
403
|
+
operations: WorkbookIndexOperation[]
|
|
404
|
+
proposals: WorkbookIndexProposal[]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Payload-free operation summary used by catalog/history bootstrap. */
|
|
408
|
+
export interface WorkbookIndexOperation {
|
|
409
|
+
opId: string
|
|
410
|
+
workbookId: string
|
|
411
|
+
revision: number
|
|
412
|
+
actor: 'agent' | 'user'
|
|
413
|
+
type: WorkbookOperationType
|
|
414
|
+
timestamp: string
|
|
415
|
+
summary: string
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export interface WorkbookIndexProposal {
|
|
419
|
+
id: string
|
|
420
|
+
workbookId: string
|
|
421
|
+
actor: 'agent' | 'user'
|
|
422
|
+
baseRevision: number
|
|
423
|
+
createdAt: string
|
|
424
|
+
updatedAt: string
|
|
425
|
+
status: 'pending' | 'committed' | 'rejected' | 'expired'
|
|
426
|
+
summary: string
|
|
427
|
+
changes: WorkbookChange[]
|
|
428
|
+
committedRevision?: number
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface WorkbookTransactionFile {
|
|
432
|
+
target: string
|
|
433
|
+
value: unknown
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface StoredWorkbookTransaction {
|
|
437
|
+
revision: number
|
|
438
|
+
files: Array<{ target: string; temp: string }>
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** In-process cache: the DSH host is the single mutation authority. */
|
|
442
|
+
export const workbookCache = new Map<string, { meta: WorkbookMeta; checkpointsLoaded: boolean }>()
|
|
443
|
+
|
|
444
|
+
/** Serialize transaction recovery and commits independently from mutations. */
|
|
445
|
+
export const workbookTransactionQueues = new Map<string, Promise<unknown>>()
|
|
446
|
+
|
|
447
|
+
export function isWorktreeStatus(value: unknown): value is WorkbookWorktreeStatus {
|
|
448
|
+
return value === 'draft' || value === 'ready' || value === 'merged' || value === 'discarded'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function toWorktreeSummary(worktree: WorkbookWorktree): WorkbookWorktreeSummary {
|
|
452
|
+
const { snapshot: _snapshot, operations: _operations, ...summary } = worktree
|
|
453
|
+
return summary
|
|
454
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long history is kept (docs/issue/0022 P1).
|
|
3
|
+
*
|
|
4
|
+
* Two rules, both policy rather than mechanism: how often a rebuildable
|
|
5
|
+
* checkpoint is taken, and how old a Version's last Step must be before the
|
|
6
|
+
* session that produced it is considered over. Both are read by the write path
|
|
7
|
+
* and by the version session, so they live below both.
|
|
8
|
+
*/
|
|
9
|
+
import { readdir, rm, stat } from 'node:fs/promises'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { AUTO_CHECKPOINT_KEEP, VERSION_IDLE_CLOSE_MS, selectAutoCheckpointsToPrune } from '#contracts'
|
|
12
|
+
import { toolOutputRoot } from './paths.ts'
|
|
13
|
+
import { versionKindOfOperations } from '../contract/operations.ts'
|
|
14
|
+
import type { WorkbookMeta, WorkbookOperation } from '../types.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drop the operations of auto checkpoints outside the retention window
|
|
18
|
+
* (PRD v3.1 §6.2, D3: newest 100 + the last one of every older day).
|
|
19
|
+
*
|
|
20
|
+
* Only history a checkpoint can rebuild from is removed: nothing at or above
|
|
21
|
+
* `newestCheckpoint - CHECKPOINT_INTERVAL` is touched, so every recent Version
|
|
22
|
+
* still previews and restores exactly as before. Older auto Versions disappear
|
|
23
|
+
* from the list instead of lingering as rows that can no longer be replayed;
|
|
24
|
+
* named Versions and anything a survivor references are always kept.
|
|
25
|
+
*/
|
|
26
|
+
export function pruneAutoVersionOperations(meta: WorkbookMeta): number {
|
|
27
|
+
const operations = meta.operations ?? []
|
|
28
|
+
// A conservative early exit, and `AUTO_CHECKPOINT_KEEP` rather than a literal
|
|
29
|
+
// 100: the policy always keeps the newest N *Versions*, and a Version is one or
|
|
30
|
+
// more operations, so fewer operations than N means fewer Versions than N and
|
|
31
|
+
// nothing can be prunable. The count is of operations, the bound is a Version
|
|
32
|
+
// count, and that is only sound for this direction — which is why the constant
|
|
33
|
+
// is the right thing to name here and a bare `100` was not.
|
|
34
|
+
if (operations.length < AUTO_CHECKPOINT_KEEP) return 0
|
|
35
|
+
const checkpoints = (meta.checkpoints ?? []).map(checkpoint => checkpoint.revision).sort((left, right) => left - right)
|
|
36
|
+
const newest = checkpoints.at(-1)
|
|
37
|
+
if (newest === undefined) return 0
|
|
38
|
+
const cutoff = newest - CHECKPOINT_INTERVAL
|
|
39
|
+
if (cutoff <= 0) return 0
|
|
40
|
+
const versionIdOf = (operation: WorkbookOperation): string => operation.versionId ?? `revision:${operation.revision}`
|
|
41
|
+
const byVersion = new Map<string, WorkbookOperation[]>()
|
|
42
|
+
for (const operation of operations) {
|
|
43
|
+
const id = versionIdOf(operation)
|
|
44
|
+
const group = byVersion.get(id)
|
|
45
|
+
if (group === undefined) byVersion.set(id, [operation])
|
|
46
|
+
else group.push(operation)
|
|
47
|
+
}
|
|
48
|
+
const versions = [...byVersion.entries()].map(([versionId, group], index) => ({
|
|
49
|
+
versionId,
|
|
50
|
+
number: index + 1,
|
|
51
|
+
status: (group.at(-1)?.versionStatus === 'final' ? 'committed' : 'open') as 'open' | 'committed',
|
|
52
|
+
kind: versionKindOfOperations(group),
|
|
53
|
+
createdAt: group[0]?.timestamp,
|
|
54
|
+
}))
|
|
55
|
+
const prunable = new Set(selectAutoCheckpointsToPrune(versions).map(version => version.versionId))
|
|
56
|
+
if (prunable.size === 0) return 0
|
|
57
|
+
const kept = operations.filter(operation => {
|
|
58
|
+
const id = versionIdOf(operation)
|
|
59
|
+
if (!prunable.has(id)) return true
|
|
60
|
+
// Only drop what a checkpoint can rebuild from.
|
|
61
|
+
return operation.revision > cutoff
|
|
62
|
+
})
|
|
63
|
+
if (kept.length === operations.length) return 0
|
|
64
|
+
meta.operations = kept
|
|
65
|
+
return operations.length - kept.length
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether an operation is older than the shared Version merge window (§4.1). */
|
|
69
|
+
export function isIdlePast(timestamp: string, nowMs = Date.now()): boolean {
|
|
70
|
+
const at = Date.parse(timestamp)
|
|
71
|
+
return Number.isFinite(at) && nowMs - at >= VERSION_IDLE_CLOSE_MS
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const CHECKPOINT_INTERVAL = 20
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How many spilled tool results to keep.
|
|
78
|
+
*
|
|
79
|
+
* They exist so the model can page through a result that was too large to inline
|
|
80
|
+
* — a debugging and continuity aid, not data. Nothing pruned them: every
|
|
81
|
+
* oversized `workbook_read` wrote a file under the storage root that stayed
|
|
82
|
+
* forever, which is disk growth with no ceiling and no way for a user to know
|
|
83
|
+
* why their storage directory kept getting bigger.
|
|
84
|
+
*/
|
|
85
|
+
export const TOOL_OUTPUT_KEEP = 50
|
|
86
|
+
|
|
87
|
+
/** Delete all but the newest `keep` spilled results. Returns how many went. */
|
|
88
|
+
export async function pruneToolOutput(keep = TOOL_OUTPUT_KEEP): Promise<number> {
|
|
89
|
+
const directory = toolOutputRoot()
|
|
90
|
+
const names = await readdir(directory).catch(() => [])
|
|
91
|
+
const files = []
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
if (!name.endsWith('.json')) continue
|
|
94
|
+
const full = join(directory, name)
|
|
95
|
+
const info = await stat(full).catch(() => null)
|
|
96
|
+
if (info !== null && info.isFile()) files.push({ full, at: info.mtimeMs })
|
|
97
|
+
}
|
|
98
|
+
if (files.length <= keep) return 0
|
|
99
|
+
files.sort((left, right) => right.at - left.at)
|
|
100
|
+
let removed = 0
|
|
101
|
+
for (const file of files.slice(keep)) {
|
|
102
|
+
await rm(file.full, { force: true }).catch(() => undefined)
|
|
103
|
+
removed += 1
|
|
104
|
+
}
|
|
105
|
+
return removed
|
|
106
|
+
}
|