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