@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,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-safe metadata for the DSH adapter over Univer's Sheets Facade.
|
|
3
|
+
*
|
|
4
|
+
* The catalog is deliberately an allow-list. It gives an Agent discoverable
|
|
5
|
+
* method names and argument contracts without exposing arbitrary JavaScript or
|
|
6
|
+
* an Injector instance to the tool protocol. Methods that change workbook
|
|
7
|
+
* state map to the same durable operations used by the typed tools.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { WorkbookOperationType } from '../types.ts'
|
|
11
|
+
|
|
12
|
+
export type UniverApiMode = 'read' | 'write'
|
|
13
|
+
export type UniverApiAvailability = 'implemented' | 'metadata-only'
|
|
14
|
+
|
|
15
|
+
export interface UniverApiArgument {
|
|
16
|
+
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'json'
|
|
17
|
+
required?: boolean
|
|
18
|
+
description: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UniverApiDefinition {
|
|
22
|
+
api: string
|
|
23
|
+
mode: UniverApiMode
|
|
24
|
+
availability: UniverApiAvailability
|
|
25
|
+
description: string
|
|
26
|
+
args: Record<string, UniverApiArgument>
|
|
27
|
+
operation?: WorkbookOperationType
|
|
28
|
+
notes?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const rangeArgs: Record<string, UniverApiArgument> = {
|
|
32
|
+
sheet: { type: 'string', description: 'sheet id 或名称;也可传 sheetId/sheetName;单 sheet workbook 可省略' },
|
|
33
|
+
range: { type: 'object', required: true, description: '0-based 矩形范围:{ startRow, startColumn, endRow, endColumn };也接受 A1:D4' },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Common Facade methods exposed by the DSH Host adapter. */
|
|
37
|
+
export const UNIVER_API_CATALOG: readonly UniverApiDefinition[] = [
|
|
38
|
+
{
|
|
39
|
+
api: 'workbook.getSnapshot',
|
|
40
|
+
mode: 'read',
|
|
41
|
+
availability: 'implemented',
|
|
42
|
+
description: '读取当前 workbook 的 Univer snapshot。大表格请优先使用 worksheet.getRangeValues。',
|
|
43
|
+
args: {},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
api: 'workbook.getWorksheets',
|
|
47
|
+
mode: 'read',
|
|
48
|
+
availability: 'implemented',
|
|
49
|
+
description: '返回 workbook 内所有 worksheet 的 id、名称和顺序。',
|
|
50
|
+
args: {},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
api: 'worksheet.getRangeValues',
|
|
54
|
+
mode: 'read',
|
|
55
|
+
availability: 'implemented',
|
|
56
|
+
description: '读取 worksheet 的矩形区域值。',
|
|
57
|
+
args: rangeArgs,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
api: 'worksheet.setValues',
|
|
61
|
+
mode: 'write',
|
|
62
|
+
availability: 'implemented',
|
|
63
|
+
description: '通过 Univer Range.setValues 写入值矩阵;range 可只传起点或省略,Host 会按矩阵尺寸推导。',
|
|
64
|
+
args: { sheet: rangeArgs.sheet, range: { type: 'object', description: '可选 0-based 范围;也可只传 startRow/startColumn,或使用 A1:D4' }, values: { type: 'array', required: true, description: '二维数组,也接受单行一维数组' } },
|
|
65
|
+
operation: 'update_cells',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
api: 'worksheet.clearContent',
|
|
69
|
+
mode: 'write',
|
|
70
|
+
availability: 'implemented',
|
|
71
|
+
description: '清除 worksheet range 的内容。',
|
|
72
|
+
args: rangeArgs,
|
|
73
|
+
operation: 'clear_range',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
api: 'worksheet.setBackgroundColor',
|
|
77
|
+
mode: 'write',
|
|
78
|
+
availability: 'implemented',
|
|
79
|
+
description: '设置 range 背景色。',
|
|
80
|
+
args: { ...rangeArgs, color: { type: 'string', required: true, description: 'CSS 颜色' } },
|
|
81
|
+
operation: 'format_cells',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
api: 'worksheet.setFontColor',
|
|
85
|
+
mode: 'write',
|
|
86
|
+
availability: 'implemented',
|
|
87
|
+
description: '设置 range 字体颜色。',
|
|
88
|
+
args: { ...rangeArgs, color: { type: 'string', required: true, description: 'CSS 颜色' } },
|
|
89
|
+
operation: 'format_cells',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
api: 'worksheet.setFontFamily',
|
|
93
|
+
mode: 'write',
|
|
94
|
+
availability: 'implemented',
|
|
95
|
+
description: '设置 range 字体。',
|
|
96
|
+
args: { ...rangeArgs, family: { type: 'string', required: true, description: '字体 family' } },
|
|
97
|
+
operation: 'format_cells',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
api: 'worksheet.setFontSize',
|
|
101
|
+
mode: 'write',
|
|
102
|
+
availability: 'implemented',
|
|
103
|
+
description: '设置 range 字号。',
|
|
104
|
+
args: { ...rangeArgs, size: { type: 'number', required: true, description: '字号,单位 pt' } },
|
|
105
|
+
operation: 'format_cells',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
api: 'worksheet.setFontWeight',
|
|
109
|
+
mode: 'write',
|
|
110
|
+
availability: 'implemented',
|
|
111
|
+
description: '设置 range 粗体状态。',
|
|
112
|
+
args: { ...rangeArgs, bold: { type: 'boolean', required: true, description: '是否粗体' } },
|
|
113
|
+
operation: 'format_cells',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
api: 'worksheet.setFontStyle',
|
|
117
|
+
mode: 'write',
|
|
118
|
+
availability: 'implemented',
|
|
119
|
+
description: '设置 range 斜体状态。',
|
|
120
|
+
args: { ...rangeArgs, italic: { type: 'boolean', required: true, description: '是否斜体' } },
|
|
121
|
+
operation: 'format_cells',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
api: 'worksheet.setHorizontalAlignment',
|
|
125
|
+
mode: 'write',
|
|
126
|
+
availability: 'implemented',
|
|
127
|
+
description: '设置 range 水平对齐。',
|
|
128
|
+
args: { ...rangeArgs, alignment: { type: 'string', required: true, description: 'left、center 或 normal' } },
|
|
129
|
+
operation: 'format_cells',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
api: 'worksheet.setVerticalAlignment',
|
|
133
|
+
mode: 'write',
|
|
134
|
+
availability: 'implemented',
|
|
135
|
+
description: '设置 range 垂直对齐。',
|
|
136
|
+
args: { ...rangeArgs, alignment: { type: 'string', required: true, description: 'top、middle 或 bottom' } },
|
|
137
|
+
operation: 'format_cells',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
api: 'worksheet.merge',
|
|
141
|
+
mode: 'write',
|
|
142
|
+
availability: 'implemented',
|
|
143
|
+
description: '合并 worksheet range。',
|
|
144
|
+
args: rangeArgs,
|
|
145
|
+
operation: 'merge_cells',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
api: 'worksheet.breakApart',
|
|
149
|
+
mode: 'write',
|
|
150
|
+
availability: 'implemented',
|
|
151
|
+
description: '取消 worksheet range 的合并。',
|
|
152
|
+
args: rangeArgs,
|
|
153
|
+
operation: 'unmerge_cells',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
api: 'worksheet.sort',
|
|
157
|
+
mode: 'write',
|
|
158
|
+
availability: 'implemented',
|
|
159
|
+
description: '对 worksheet range 按列排序。',
|
|
160
|
+
args: { ...rangeArgs, column: { type: 'number', required: true, description: '0-based 排序列索引' }, direction: { type: 'string', description: 'asc 或 desc,默认 asc' } },
|
|
161
|
+
operation: 'sort_range',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
api: 'worksheet.setColumnWidth',
|
|
165
|
+
mode: 'write',
|
|
166
|
+
availability: 'implemented',
|
|
167
|
+
description: '设置列宽。',
|
|
168
|
+
args: { sheet: rangeArgs.sheet, index: { type: 'number', required: true, description: '0-based 列索引' }, size: { type: 'number', required: true, description: '像素宽度' } },
|
|
169
|
+
operation: 'set_column_width',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
api: 'worksheet.setRowHeight',
|
|
173
|
+
mode: 'write',
|
|
174
|
+
availability: 'implemented',
|
|
175
|
+
description: '设置行高。',
|
|
176
|
+
args: { sheet: rangeArgs.sheet, index: { type: 'number', required: true, description: '0-based 行索引' }, size: { type: 'number', required: true, description: '像素高度' } },
|
|
177
|
+
operation: 'set_row_height',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
api: 'worksheet.setHyperLink',
|
|
181
|
+
mode: 'write',
|
|
182
|
+
availability: 'implemented',
|
|
183
|
+
description: '给 range 左上角单元格设置超链接;等价于 workbook_edit 的 set_hyperlink command。',
|
|
184
|
+
args: { ...rangeArgs, url: { type: 'string', required: true, description: '目标 URL' }, label: { type: 'string', description: '显示文本' } },
|
|
185
|
+
operation: 'set_hyperlink',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
api: 'worksheet.cancelHyperLink',
|
|
189
|
+
mode: 'write',
|
|
190
|
+
availability: 'implemented',
|
|
191
|
+
description: '移除 range 左上角单元格的超链接。',
|
|
192
|
+
args: rangeArgs,
|
|
193
|
+
operation: 'remove_hyperlink',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
api: 'worksheet.setFreeze',
|
|
197
|
+
mode: 'write',
|
|
198
|
+
availability: 'implemented',
|
|
199
|
+
description: '冻结指定行列。',
|
|
200
|
+
args: { sheet: rangeArgs.sheet, rows: { type: 'number', description: '冻结行数,默认 0' }, columns: { type: 'number', description: '冻结列数,默认 0' } },
|
|
201
|
+
operation: 'freeze_panes',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
api: 'worksheet.deleteCells',
|
|
205
|
+
mode: 'write',
|
|
206
|
+
availability: 'implemented',
|
|
207
|
+
description: '删除 range 内容,或按 left/up 移动邻近单元格。',
|
|
208
|
+
args: { ...rangeArgs, shift: { type: 'string', description: 'clear、left 或 up,默认 clear' } },
|
|
209
|
+
operation: 'delete_cells',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
api: 'worksheet.insertRows',
|
|
213
|
+
mode: 'write',
|
|
214
|
+
availability: 'implemented',
|
|
215
|
+
description: '在指定行位置插入行。',
|
|
216
|
+
args: { sheet: rangeArgs.sheet, index: { type: 'number', required: true, description: '0-based 行位置' }, count: { type: 'number', description: '行数,默认 1' } },
|
|
217
|
+
operation: 'insert_rows',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
api: 'worksheet.deleteRows',
|
|
221
|
+
mode: 'write',
|
|
222
|
+
availability: 'implemented',
|
|
223
|
+
description: '删除指定行。',
|
|
224
|
+
args: { sheet: rangeArgs.sheet, index: { type: 'number', required: true, description: '0-based 行位置' }, count: { type: 'number', description: '行数,默认 1' } },
|
|
225
|
+
operation: 'delete_rows',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
api: 'worksheet.insertColumns',
|
|
229
|
+
mode: 'write',
|
|
230
|
+
availability: 'implemented',
|
|
231
|
+
description: '在指定列位置插入列。',
|
|
232
|
+
args: { sheet: rangeArgs.sheet, index: { type: 'number', required: true, description: '0-based 列位置' }, count: { type: 'number', description: '列数,默认 1' } },
|
|
233
|
+
operation: 'insert_columns',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
api: 'worksheet.deleteColumns',
|
|
237
|
+
mode: 'write',
|
|
238
|
+
availability: 'implemented',
|
|
239
|
+
description: '删除指定列。',
|
|
240
|
+
args: { sheet: rangeArgs.sheet, index: { type: 'number', required: true, description: '0-based 列位置' }, count: { type: 'number', description: '列数,默认 1' } },
|
|
241
|
+
operation: 'delete_columns',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
api: 'worksheet.setTextRotation',
|
|
245
|
+
mode: 'write',
|
|
246
|
+
availability: 'implemented',
|
|
247
|
+
description: '设置 range 文字旋转角度。',
|
|
248
|
+
args: { ...rangeArgs, rotation: { type: 'number', required: true, description: '旋转角度' } },
|
|
249
|
+
operation: 'format_cells',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
api: 'worksheet.setNumberFormat',
|
|
253
|
+
mode: 'write',
|
|
254
|
+
availability: 'implemented',
|
|
255
|
+
description: '设置 range 数字格式。',
|
|
256
|
+
args: { ...rangeArgs, format: { type: 'string', required: true, description: 'Univer number format' } },
|
|
257
|
+
operation: 'format_cells',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
api: 'worksheet.setName',
|
|
261
|
+
mode: 'write',
|
|
262
|
+
availability: 'implemented',
|
|
263
|
+
description: '修改 worksheet 名称。',
|
|
264
|
+
args: { sheet: rangeArgs.sheet, name: { type: 'string', required: true, description: '新的 sheet 名称' } },
|
|
265
|
+
operation: 'rename_sheet',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
api: 'workbook.createSheet',
|
|
269
|
+
mode: 'write',
|
|
270
|
+
availability: 'implemented',
|
|
271
|
+
description: '持久化创建 worksheet;可用 sheetId 指定稳定 id。',
|
|
272
|
+
args: { name: { type: 'string', required: true, description: 'sheet 名称' }, sheetId: { type: 'string', description: '可选稳定 sheet id' } },
|
|
273
|
+
operation: 'create_sheet',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
api: 'workbook.deleteSheet',
|
|
277
|
+
mode: 'write',
|
|
278
|
+
availability: 'implemented',
|
|
279
|
+
description: '持久化删除 worksheet;工作簿至少保留一个 sheet。',
|
|
280
|
+
args: { sheet: { type: 'string', description: 'sheet id 或名称;也接受 sheetId/sheetName' } },
|
|
281
|
+
operation: 'delete_sheet',
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
export const UNIVER_API_VERSION = '0.25.1-dsh-1'
|
|
286
|
+
|
|
287
|
+
export function findUniverApi(api: string): UniverApiDefinition | undefined {
|
|
288
|
+
return UNIVER_API_CATALOG.find(item => item.api === api)
|
|
289
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Shared wire-level errors for the Host API and browser client. */
|
|
2
|
+
export type DataSheetErrorCode =
|
|
3
|
+
| 'VALIDATION_ERROR'
|
|
4
|
+
| 'WORKBOOK_NOT_FOUND'
|
|
5
|
+
| 'WORKTREE_NOT_FOUND'
|
|
6
|
+
| 'SESSION_SCOPE_DENIED'
|
|
7
|
+
| 'REVISION_CONFLICT'
|
|
8
|
+
| 'REVISION_UNAVAILABLE'
|
|
9
|
+
| 'WORKTREE_STATE_INVALID'
|
|
10
|
+
| 'PAYLOAD_TOO_LARGE'
|
|
11
|
+
| 'PERSISTENCE_FAILED'
|
|
12
|
+
| 'COLLABORATION_LIMIT'
|
|
13
|
+
| 'INTERNAL_ERROR'
|
|
14
|
+
|
|
15
|
+
export class DataSheetError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly code: DataSheetErrorCode,
|
|
18
|
+
message: string,
|
|
19
|
+
readonly status = 400,
|
|
20
|
+
readonly details?: Record<string, unknown>,
|
|
21
|
+
) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.name = 'DataSheetError'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static plugin assets served on demand (docs/issue/0022 P3).
|
|
3
|
+
*
|
|
4
|
+
* The Univer engine chunk, the two xlsx workers and the standalone workbook
|
|
5
|
+
* page are all classic scripts or worker entry points the browser fetches after
|
|
6
|
+
* boot, so they are served from here rather than inlined into the client bundle.
|
|
7
|
+
* `univer-api.json` is the catalogue the Agent reads through its tool.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto'
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import type { LegacyRoute } from './routes.ts'
|
|
15
|
+
import { guardDataSheetRequest } from './routes.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The plugin directory, found by walking up to the `package.json` that owns
|
|
19
|
+
* this file.
|
|
20
|
+
*
|
|
21
|
+
* It used to be a pair of `new URL('../../lib/...', import.meta.url)` candidates
|
|
22
|
+
* because the path depth differs between `src/` and the emitted `lib/types/`,
|
|
23
|
+
* which meant every file move silently changed what those URLs pointed at. The
|
|
24
|
+
* walk is depth-independent, so moving this module cannot break it.
|
|
25
|
+
*/
|
|
26
|
+
function pluginRoot(): string {
|
|
27
|
+
let directory = dirname(fileURLToPath(import.meta.url))
|
|
28
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
29
|
+
if (existsSync(join(directory, 'package.json'))) return directory
|
|
30
|
+
const parent = dirname(directory)
|
|
31
|
+
if (parent === directory) break
|
|
32
|
+
directory = parent
|
|
33
|
+
}
|
|
34
|
+
return dirname(fileURLToPath(import.meta.url))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Absolute path of one build artifact under `lib/`. */
|
|
38
|
+
const artifact = (name: string): string => join(pluginRoot(), 'lib', name)
|
|
39
|
+
|
|
40
|
+
/** One artifact read, plus the identity that read is still valid for. */
|
|
41
|
+
interface ResidentArtifact {
|
|
42
|
+
/** `mtimeMs:size`; a different string means the file was rebuilt. */
|
|
43
|
+
identity: string
|
|
44
|
+
bytes: Buffer
|
|
45
|
+
/** First 12 hex of the content's SHA-256 — the same value the chunk URL carries. */
|
|
46
|
+
hash: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Last read per artifact name. The engine chunk is 13MB and changes only on rebuild. */
|
|
50
|
+
const residentArtifacts = new Map<string, ResidentArtifact>()
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read one `lib/` artifact, reusing the last read while the file is unchanged.
|
|
54
|
+
*
|
|
55
|
+
* The engine chunk used to be re-read from disk on every request, which was the
|
|
56
|
+
* only way to notice a rebuild. The `mtimeMs`/`size` identity notices the same
|
|
57
|
+
* rebuild without the disk read, and it is what lets the route answer the
|
|
58
|
+
* content-hash check below without hashing 13MB per request.
|
|
59
|
+
*
|
|
60
|
+
* @param name - file name under `lib/`.
|
|
61
|
+
* @returns the current bytes and content hash.
|
|
62
|
+
*/
|
|
63
|
+
async function readArtifact(name: string): Promise<ResidentArtifact> {
|
|
64
|
+
const path = artifact(name)
|
|
65
|
+
const info = await stat(path)
|
|
66
|
+
const identity = `${info.mtimeMs}:${info.size}`
|
|
67
|
+
const resident = residentArtifacts.get(name)
|
|
68
|
+
if (resident !== undefined && resident.identity === identity) return resident
|
|
69
|
+
const bytes = await readFile(path)
|
|
70
|
+
const next: ResidentArtifact = { identity, bytes, hash: createHash('sha256').update(bytes).digest('hex').slice(0, 12) }
|
|
71
|
+
residentArtifacts.set(name, next)
|
|
72
|
+
return next
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Every static route this plugin serves. */
|
|
76
|
+
export function assetRoutes(): LegacyRoute[] {
|
|
77
|
+
const routes: LegacyRoute[] = []
|
|
78
|
+
routes.push({
|
|
79
|
+
kind: 'exact',
|
|
80
|
+
path: '/data-sheet/univer-client.js',
|
|
81
|
+
handler: async (req, res) => {
|
|
82
|
+
if (!guardDataSheetRequest(req, res)) return
|
|
83
|
+
try {
|
|
84
|
+
const chunk = await readArtifact('univer-client.js')
|
|
85
|
+
// The loader asks for this file with its content hash in `?v=`. An exact
|
|
86
|
+
// match means the bytes cannot have changed, so the browser may keep them
|
|
87
|
+
// for a year and never ask again — which is the point: the chunk is 13MB
|
|
88
|
+
// and the sheet is opened in a fresh page constantly.
|
|
89
|
+
//
|
|
90
|
+
// Anything else — no `?v=` at all (the `/api/data-sheet?path=` transport mode
|
|
91
|
+
// drops the query), or a hash from a build that has since been replaced —
|
|
92
|
+
// is served `no-store`. A stale URL must never be able to pin different
|
|
93
|
+
// bytes, and that is exactly the failure the old unconditional `no-store`
|
|
94
|
+
// was there to prevent.
|
|
95
|
+
const requested = new URL(req.url ?? '/', 'http://x').searchParams.get('v')
|
|
96
|
+
const immutable = requested !== null && requested === chunk.hash
|
|
97
|
+
res.writeHead(200, {
|
|
98
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
99
|
+
'cache-control': immutable ? 'public, max-age=31536000, immutable' : 'no-store, must-revalidate',
|
|
100
|
+
})
|
|
101
|
+
if (req.method === 'HEAD') {
|
|
102
|
+
res.end()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
res.end(chunk.bytes)
|
|
106
|
+
} catch {
|
|
107
|
+
res.writeHead(404)
|
|
108
|
+
res.end()
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// XLSX/CSV encoding is loaded only when the user exports. Keeping it in a
|
|
114
|
+
// worker prevents SheetJS serialization from blocking the Univer canvas.
|
|
115
|
+
routes.push({
|
|
116
|
+
kind: 'exact',
|
|
117
|
+
path: '/data-sheet/xlsx-export-worker.js',
|
|
118
|
+
handler: async (_req, res) => {
|
|
119
|
+
if (!guardDataSheetRequest(_req, res)) return
|
|
120
|
+
try {
|
|
121
|
+
const body = await readFile(artifact('xlsx-export-worker.js'))
|
|
122
|
+
res.writeHead(200, {
|
|
123
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
124
|
+
'cache-control': 'no-cache',
|
|
125
|
+
})
|
|
126
|
+
res.end(body)
|
|
127
|
+
} catch {
|
|
128
|
+
res.writeHead(404)
|
|
129
|
+
res.end()
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
routes.push({
|
|
135
|
+
kind: 'exact',
|
|
136
|
+
path: '/data-sheet/xlsx-import-worker.js',
|
|
137
|
+
handler: async (_req, res) => {
|
|
138
|
+
if (!guardDataSheetRequest(_req, res)) return
|
|
139
|
+
try {
|
|
140
|
+
const body = await readFile(artifact('xlsx-import-worker.js'))
|
|
141
|
+
res.writeHead(200, {
|
|
142
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
143
|
+
'cache-control': 'no-cache',
|
|
144
|
+
})
|
|
145
|
+
res.end(body)
|
|
146
|
+
} catch {
|
|
147
|
+
res.writeHead(404)
|
|
148
|
+
res.end()
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Standalone client bundle for the optional full-browser workbook page.
|
|
154
|
+
routes.push({
|
|
155
|
+
kind: 'exact',
|
|
156
|
+
path: '/data-sheet/workbook-page.js',
|
|
157
|
+
handler: async (_req, res) => {
|
|
158
|
+
if (!guardDataSheetRequest(_req, res)) return
|
|
159
|
+
try {
|
|
160
|
+
const body = await readFile(artifact('workbook-page.js'))
|
|
161
|
+
res.writeHead(200, {
|
|
162
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
163
|
+
'cache-control': 'no-cache',
|
|
164
|
+
})
|
|
165
|
+
res.end(body)
|
|
166
|
+
} catch {
|
|
167
|
+
res.writeHead(404)
|
|
168
|
+
res.end()
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Workbook catalog for the browser picker. Workbook content stays behind
|
|
174
|
+
// the per-workbook snapshot endpoint; this route returns names and sheet
|
|
175
|
+
// summaries and accepts the small lifecycle mutations used by the picker.
|
|
176
|
+
return routes
|
|
177
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live collaboration stream (docs/issue/0022 P3).
|
|
3
|
+
*
|
|
4
|
+
* A server-sent event stream plus the presence beacon. This is a subscription
|
|
5
|
+
* protocol rather than request/response — the response stays open until the
|
|
6
|
+
* client goes away — which is why it is a module of its own.
|
|
7
|
+
*/
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
9
|
+
import { DataSheetError } from '../errors.ts'
|
|
10
|
+
import { publishWorkbookPresence, subscribeWorkbookCollaboration, subscribeWorkbookPresence } from '../collaboration-hub.ts'
|
|
11
|
+
import { readWorkbook } from '../workbook-manager.ts'
|
|
12
|
+
import { parseRevision, readJsonBody, requireDataSheetSession, writeOperationError, type WorkbookRoute } from './routes.ts'
|
|
13
|
+
import type { WorkbookOperation, WorkbookPresence } from '../types.ts'
|
|
14
|
+
|
|
15
|
+
export async function handleCollaborationRoute(route: WorkbookRoute, req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean> {
|
|
16
|
+
const id = route.id
|
|
17
|
+
|
|
18
|
+
if (route.kind === 'presence' && req.method === 'POST') {
|
|
19
|
+
const sessionId = requireDataSheetSession(req, res, url)
|
|
20
|
+
if (sessionId === null) return true
|
|
21
|
+
let body: Record<string, unknown>
|
|
22
|
+
try { body = await readJsonBody(req) } catch (error) { writeOperationError(res, error); return true }
|
|
23
|
+
const kind = body.kind
|
|
24
|
+
if (kind !== 'selection' && kind !== 'editing' && kind !== 'active-sheet') {
|
|
25
|
+
writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'presence.kind 必须是 selection、editing 或 active-sheet'))
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
const payload = body.payload
|
|
29
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
30
|
+
writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'presence.payload 必须是对象'))
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
if (JSON.stringify(payload).length > 8192) {
|
|
34
|
+
writeOperationError(res, new DataSheetError('PAYLOAD_TOO_LARGE', 'presence.payload 不能超过 8 KiB', 413))
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
publishWorkbookPresence({ workbookId: id, sessionId, actor: 'user', kind, payload: payload as Record<string, unknown>, timestamp: new Date().toISOString() })
|
|
38
|
+
res.writeHead(202, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
39
|
+
res.end(JSON.stringify({ ok: true, workbookId: id }))
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (route.kind === 'collaboration' && req.method === 'GET') {
|
|
44
|
+
const sessionId = requireDataSheetSession(req, res, url)
|
|
45
|
+
if (sessionId === null) return true
|
|
46
|
+
const initialMeta = await readWorkbook(id)
|
|
47
|
+
if (initialMeta === null) {
|
|
48
|
+
res.writeHead(404)
|
|
49
|
+
res.end()
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
const after = parseRevision(url.searchParams.get('after'))
|
|
53
|
+
const operations = (initialMeta.operations ?? []).filter(operation => operation.revision > after)
|
|
54
|
+
const currentRevision = initialMeta.revision ?? 0
|
|
55
|
+
const oldestRevision = operations[0]?.revision ?? currentRevision
|
|
56
|
+
const requiresSnapshot = after < oldestRevision - 1
|
|
57
|
+
const snapshot = requiresSnapshot
|
|
58
|
+
? initialMeta.snapshot
|
|
59
|
+
: undefined
|
|
60
|
+
const pendingEvents: Array<{ operation: WorkbookOperation }> = []
|
|
61
|
+
const pendingPresence: Array<{ workbookId: string; presence: WorkbookPresence }> = []
|
|
62
|
+
let ready = false
|
|
63
|
+
let closed = false
|
|
64
|
+
const pendingWrites: string[] = []
|
|
65
|
+
const flushWrites = (): void => {
|
|
66
|
+
while (!closed && pendingWrites.length > 0) {
|
|
67
|
+
const frame = pendingWrites.shift() as string
|
|
68
|
+
if (!res.write(frame)) break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const writeOperationEvent = (operation: WorkbookOperation): void => {
|
|
72
|
+
if (closed) return
|
|
73
|
+
const frame = `id: ${operation.revision}\nevent: operation\ndata: ${JSON.stringify({ workbookId: id, operation })}\n\n`
|
|
74
|
+
if (pendingWrites.length > 0 || !res.write(frame)) {
|
|
75
|
+
if (pendingWrites.length >= 100) {
|
|
76
|
+
res.destroy()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
pendingWrites.push(frame)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const writePresenceEvent = (presence: WorkbookPresence): void => {
|
|
83
|
+
if (closed || presence.sessionId === sessionId) return
|
|
84
|
+
const frame = `id: ${Date.parse(presence.timestamp) || Date.now()}\nevent: presence\ndata: ${JSON.stringify({ workbookId: id, presence })}\n\n`
|
|
85
|
+
if (pendingWrites.length > 0 || !res.write(frame)) {
|
|
86
|
+
if (pendingWrites.length >= 100) { res.destroy(); return }
|
|
87
|
+
pendingWrites.push(frame)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let unsubscribe: (() => void) | undefined
|
|
91
|
+
let unsubscribePresence: (() => void) | undefined
|
|
92
|
+
try {
|
|
93
|
+
unsubscribe = subscribeWorkbookCollaboration(id, event => {
|
|
94
|
+
if (closed) return
|
|
95
|
+
if (!ready) {
|
|
96
|
+
pendingEvents.push(event)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
writeOperationEvent(event.operation)
|
|
100
|
+
})
|
|
101
|
+
unsubscribePresence = subscribeWorkbookPresence(id, event => {
|
|
102
|
+
if (closed || event.presence.sessionId === sessionId) return
|
|
103
|
+
if (!ready) pendingPresence.push(event)
|
|
104
|
+
else writePresenceEvent(event.presence)
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
res.writeHead(429, { 'content-type': 'application/json; charset=utf-8', 'retry-after': '5' })
|
|
108
|
+
res.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }))
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
112
|
+
res.on('drain', flushWrites)
|
|
113
|
+
res.once('close', () => {
|
|
114
|
+
closed = true
|
|
115
|
+
if (heartbeat !== undefined) clearInterval(heartbeat)
|
|
116
|
+
unsubscribe?.()
|
|
117
|
+
unsubscribePresence?.()
|
|
118
|
+
})
|
|
119
|
+
heartbeat = setInterval(() => {
|
|
120
|
+
if (!closed) res.write(': heartbeat\n\n')
|
|
121
|
+
}, 15000)
|
|
122
|
+
res.writeHead(200, {
|
|
123
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
124
|
+
'cache-control': 'no-cache, no-transform',
|
|
125
|
+
connection: 'keep-alive',
|
|
126
|
+
})
|
|
127
|
+
res.write(`event: ready\ndata: ${JSON.stringify({
|
|
128
|
+
workbookId: id,
|
|
129
|
+
revision: currentRevision,
|
|
130
|
+
operations: requiresSnapshot ? [] : operations,
|
|
131
|
+
requiresSnapshot,
|
|
132
|
+
...(snapshot === undefined ? {} : { snapshot }),
|
|
133
|
+
})}\n\n`)
|
|
134
|
+
ready = true
|
|
135
|
+
for (const event of pendingEvents) {
|
|
136
|
+
if (event.operation.revision > currentRevision) writeOperationEvent(event.operation)
|
|
137
|
+
}
|
|
138
|
+
for (const event of pendingPresence) writePresenceEvent(event.presence)
|
|
139
|
+
await new Promise<void>(resolve => res.once('close', resolve))
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false
|
|
144
|
+
}
|