@univerjs-pro/collaboration-endpoint 1.0.0-beta.2
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/README.md +193 -0
- package/README.zh-CN.md +198 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.mjs +1 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @univerjs-pro/collaboration-endpoint
|
|
2
|
+
|
|
3
|
+
English | [简体中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
`UniverCollabEndpoint` is the protocol layer of a Univer Collaboration server. It receives HTTP requests and WebSocket messages dispatched by Node Transport, uses the `UniverCollabService` API to read and write collaboration data, and manages Sessions, Unit rooms, Presence, ACKs, and broadcasts.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Node HTTP server
|
|
9
|
+
→ Transport application authentication; sets ctx.userID and ctx.customData
|
|
10
|
+
→ UniverCollabEndpoint HTTP/WebSocket protocol, Sessions, and realtime rooms
|
|
11
|
+
→ UniverCollabService data reads, changeset submission, and Unit lifecycle
|
|
12
|
+
→ Database Adapter snapshot, changeset, and revision persistence
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Register Endpoint with `@univerjs-pro/collaboration-transport-node` through `transport.register()`. It does not start a separate HTTP or WebSocket server.
|
|
16
|
+
|
|
17
|
+
## Server assembly
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add \
|
|
21
|
+
@univerjs-pro/collaboration-service \
|
|
22
|
+
@univerjs-pro/collaboration-endpoint \
|
|
23
|
+
@univerjs-pro/collaboration-transport-node \
|
|
24
|
+
@univerjs-pro/collaboration-database-memory
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { MemoryDatabaseAdapter } from '@univerjs-pro/collaboration-database-memory';
|
|
29
|
+
import { UniverCollabEndpoint } from '@univerjs-pro/collaboration-endpoint';
|
|
30
|
+
import { UniverCollabService } from '@univerjs-pro/collaboration-service';
|
|
31
|
+
import { createNodeTransport } from '@univerjs-pro/collaboration-transport-node';
|
|
32
|
+
|
|
33
|
+
const database = new MemoryDatabaseAdapter();
|
|
34
|
+
const collabService = new UniverCollabService({ dbAdapter: database });
|
|
35
|
+
const endpoint = new UniverCollabEndpoint(collabService);
|
|
36
|
+
const transport = createNodeTransport();
|
|
37
|
+
|
|
38
|
+
// Authenticate HTTP requests and attach identity in Transport middleware first.
|
|
39
|
+
transport.use(async (ctx, next) => {
|
|
40
|
+
const user = await authenticate(ctx.incomingMessage);
|
|
41
|
+
if (!user) {
|
|
42
|
+
ctx.response.statusCode = 401;
|
|
43
|
+
ctx.response.end('Authentication required');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ctx.userID = user.userId;
|
|
48
|
+
ctx.customData.user = user;
|
|
49
|
+
await next();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Endpoint must be registered after authentication middleware.
|
|
53
|
+
transport.register(endpoint);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This uses the Memory Adapter to keep the example minimal; all data is lost when the process exits. See the `@univerjs-pro/collaboration-transport-node` README for attaching Node HTTP requests and WebSocket upgrades.
|
|
57
|
+
|
|
58
|
+
All Collaboration and Univer frontend packages should use matching versions from the same release cohort.
|
|
59
|
+
|
|
60
|
+
## Two request classes
|
|
61
|
+
|
|
62
|
+
Endpoint handles two broad protocol classes: document-data requests and realtime Sessions. They use different middleware and contexts.
|
|
63
|
+
|
|
64
|
+
### Document-data requests
|
|
65
|
+
|
|
66
|
+
Snapshot, block, fetch-missing, new-changes, and Unit delete/recover are HTTP requests. They use the identity and custom data attached to the current HTTP request by Transport middleware:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
HTTP snapshot / block / fetch-missing / new-changes / delete / recover
|
|
70
|
+
→ Transport middleware sets ctx.userID and ctx.customData
|
|
71
|
+
→ Endpoint parses the protocol and calls UniverCollabService APIs
|
|
72
|
+
→ Service middleware reads ctx.userID and ctx.customData
|
|
73
|
+
→ Service / Database Adapter
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Service middleware receives `ctx.userID` and `ctx.customData` from the current HTTP request's Transport middleware. They can be used for authorization, logging, and external integrations. For example:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { CollabError } from '@univerjs-pro/collaboration-service';
|
|
80
|
+
|
|
81
|
+
collabService.use('readUnitData', async (ctx, next) => {
|
|
82
|
+
// Both fields come from Transport middleware for the current HTTP request.
|
|
83
|
+
if (!await acl.canRead(ctx.userID, ctx.request.unitID)) {
|
|
84
|
+
throw new CollabError('PERMISSION_DENIED', 'Unit is not accessible');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await next();
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`readUnitData` protects snapshot, block, and fetch-missing. New-changes and Unit delete/recover use the Service actions `submitChangeset`, `deleteUnits`, and `recoverUnits`, respectively.
|
|
92
|
+
|
|
93
|
+
### Realtime Sessions
|
|
94
|
+
|
|
95
|
+
Session-ticket and WebSocket open, HELLO, HEARTBEAT, JOIN, LEAVE, and Presence belong to the realtime Session flow:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
GET /universer-api/user/session-ticket
|
|
99
|
+
→ Transport middleware sets ctx.userID and ctx.customData
|
|
100
|
+
→ Endpoint stores the association server-side and returns a one-time opaque ticket
|
|
101
|
+
→ WebSocket handshake consumes the ticket; invalid tickets return HTTP 401 before upgrade
|
|
102
|
+
→ Endpoint creates Session { userID, memberID, customData }
|
|
103
|
+
→ Endpoint middleware reads ctx.session
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The ticket string does not contain `userID` or `customData`. Of all HTTP routes, only the session-ticket request carries its current `ctx.userID` and `ctx.customData` into the WebSocket Session.
|
|
107
|
+
|
|
108
|
+
Endpoint middleware reads them from `ctx.session.userID` and `ctx.session.customData`. Endpoint generates a `memberID` for each WebSocket Session, and it changes after reconnecting.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { CollabError } from '@univerjs-pro/collaboration-service';
|
|
112
|
+
|
|
113
|
+
endpoint.use('joinUnit', async (ctx, next) => {
|
|
114
|
+
// Both fields originate from the session-ticket HTTP request.
|
|
115
|
+
if (!await acl.canRead(ctx.session.userID, ctx.unitID)) {
|
|
116
|
+
throw new CollabError('PERMISSION_DENIED', 'Unit is not accessible');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await next();
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `new-changes` connects both classes
|
|
124
|
+
|
|
125
|
+
New-changes is an HTTP document-data request, but its ACK and broadcast are delivered over WebSocket:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
HTTP new-changes { memberID }
|
|
129
|
+
→ Endpoint locates an online Session by memberID
|
|
130
|
+
→ verifies session.userID === current HTTP ctx.userID
|
|
131
|
+
→ verifies that the Session has JOINed the target Unit
|
|
132
|
+
→ calls the Service with the current HTTP ctx.userID and ctx.customData
|
|
133
|
+
→ sends ACK and broadcast over WebSocket
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`memberID` is only used to locate a WebSocket Session and is not an identity credential by itself. Endpoint owns these associations and checks; the application does not need to implement them.
|
|
137
|
+
|
|
138
|
+
## Middleware
|
|
139
|
+
|
|
140
|
+
Endpoint provides four realtime protocol middleware actions. Document-data requests use the Service middleware described above; see the `@univerjs-pro/collaboration-service` README for all Service actions.
|
|
141
|
+
|
|
142
|
+
| Action | When it runs | Visible fields | Retry semantics | Typical uses |
|
|
143
|
+
| --- | --- | --- | --- | --- |
|
|
144
|
+
| `connect` | After consuming the ticket and creating the Session, before handling `HELLO` | `session`, `connection`, `member` | Once per connection; runs again after reconnecting | Connection policy, member names and avatars |
|
|
145
|
+
| `joinUnit` | After receiving `JOIN`, before the Session joins the room | `session`, `unitID` | Once for the Session's first JOIN; Endpoint does not retry | Room read ACL |
|
|
146
|
+
| `receivePresence` | After a joined Session sends Presence, before broadcasting to the room | `session`, `unitID`, `payload`, `customData` | Once per Presence message; Endpoint does not retry | Presence validation, rate limits, filtering |
|
|
147
|
+
| `sendPresence` | Before sending Presence to one room member | Fields above plus `targetMemberID` | Once per target member; send failures are not retried | Per-recipient Presence filtering |
|
|
148
|
+
|
|
149
|
+
The `session` in this table originates from the session-ticket request. Top-level `customData` in a Presence context is a separate object created by Transport for the current WebSocket message.
|
|
150
|
+
|
|
151
|
+
Middleware for the same action runs in registration order. `await next()` enters the next middleware; throw `CollabError` for an expected rejection.
|
|
152
|
+
|
|
153
|
+
`joinUnit` only protects the realtime room and cannot replace Service `readUnitData` middleware. A client that has not joined a room can still issue a snapshot HTTP request directly.
|
|
154
|
+
|
|
155
|
+
## Protocol route reference
|
|
156
|
+
|
|
157
|
+
The table uses the default `protocolBasePath=/universer-api`. Univer Collaboration Client normally calls these routes; applications do not need to wrap them manually.
|
|
158
|
+
|
|
159
|
+
| Channel | Method / Message | Path | Purpose |
|
|
160
|
+
| --- | --- | --- | --- |
|
|
161
|
+
| HTTP | `GET` | `/universer-api/user/session-ticket` | Obtain a one-time WebSocket ticket |
|
|
162
|
+
| HTTP | `GET` | `/universer-api/snapshot/:type/unit/:unitID/rev/:revision` | Load Unit snapshot and changesets |
|
|
163
|
+
| HTTP | `GET` | `/universer-api/snapshot/:type/unit/:unitID/block/:blockID` | Load a Unit block |
|
|
164
|
+
| HTTP | `GET` | `/universer-api/snapshot/block/:type/unit/:unitID/block/:blockID` | Load a parsed Unit block |
|
|
165
|
+
| HTTP | `GET` | `/universer-api/snapshot/:type/unit/:unitID/fetchmissing?from=:from&to=:to` | Load missing changesets |
|
|
166
|
+
| HTTP | `POST` | `/universer-api/comb/:type/unit/:unitID/new_changes` | Submit a changeset |
|
|
167
|
+
| HTTP | `DELETE` | `/universer-api/snapshot/-/units` | Delete Units |
|
|
168
|
+
| HTTP | `POST` | `/universer-api/snapshot/-/units/recover` | Recover Units |
|
|
169
|
+
| WebSocket open | `GET` + Upgrade | `/universer-api/comb/connect?sessionTicket=:ticket` | Consume a ticket and establish a Session |
|
|
170
|
+
| WebSocket | `HELLO/HEARTBEAT/JOIN/LEAVE/Presence` | `/universer-api/comb/connect` | Members, rooms, and Presence |
|
|
171
|
+
|
|
172
|
+
## Configuration and deployment
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const endpoint = new UniverCollabEndpoint(collabService, {
|
|
176
|
+
protocolBasePath: '/universer-api', // default
|
|
177
|
+
sessionTicketTtlMs: 5 * 60_000, // default: 5 minutes
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Sessions, Unit rooms, and pending-send queues are process-local, so realtime delivery is only guaranteed within a single Endpoint process. Cross-process online members, rooms, and broadcasts are not currently supported.
|
|
182
|
+
|
|
183
|
+
WebSocket ACKs and broadcasts are not guaranteed to reach the client and are not an authoritative data source. After disconnecting, clients use revisions and fetch-missing HTTP requests to recover confirmed changesets.
|
|
184
|
+
|
|
185
|
+
### Shutdown
|
|
186
|
+
|
|
187
|
+
Transport disposes registered Endpoints. Endpoint clears Sessions, rooms, and Service event subscriptions. The application must still dispose the Service and Database Adapter.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
await transport.dispose();
|
|
191
|
+
await collabService.dispose();
|
|
192
|
+
await database.dispose();
|
|
193
|
+
```
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @univerjs-pro/collaboration-endpoint
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [简体中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
`UniverCollabEndpoint` 是 Univer Collaboration 服务端的协议层。它接收 Node Transport 分派的 HTTP 请求和 WebSocket 消息,使用 `UniverCollabService` API 读写协同数据,并管理 Session、Unit 房间、Presence、ACK 和广播。
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Node HTTP server
|
|
9
|
+
→ Transport 应用认证,挂载 ctx.userID 和 ctx.customData
|
|
10
|
+
→ UniverCollabEndpoint HTTP/WebSocket 协议、Session 和实时房间
|
|
11
|
+
→ UniverCollabService 数据读取、changeset 提交和 Unit 生命周期
|
|
12
|
+
→ Database Adapter snapshot、changeset 和 revision 持久化
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Endpoint 通过 `transport.register()` 注册到 `@univerjs-pro/collaboration-transport-node`,
|
|
16
|
+
不需要单独启动 HTTP 或 WebSocket server。
|
|
17
|
+
|
|
18
|
+
## 服务端组装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add \
|
|
22
|
+
@univerjs-pro/collaboration-service \
|
|
23
|
+
@univerjs-pro/collaboration-endpoint \
|
|
24
|
+
@univerjs-pro/collaboration-transport-node \
|
|
25
|
+
@univerjs-pro/collaboration-database-memory
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { MemoryDatabaseAdapter } from '@univerjs-pro/collaboration-database-memory';
|
|
30
|
+
import { UniverCollabEndpoint } from '@univerjs-pro/collaboration-endpoint';
|
|
31
|
+
import { UniverCollabService } from '@univerjs-pro/collaboration-service';
|
|
32
|
+
import { createNodeTransport } from '@univerjs-pro/collaboration-transport-node';
|
|
33
|
+
|
|
34
|
+
const database = new MemoryDatabaseAdapter();
|
|
35
|
+
const collabService = new UniverCollabService({ dbAdapter: database });
|
|
36
|
+
const endpoint = new UniverCollabEndpoint(collabService);
|
|
37
|
+
const transport = createNodeTransport();
|
|
38
|
+
|
|
39
|
+
// 应用先在 Transport middleware 中认证 HTTP 请求并挂载身份。
|
|
40
|
+
transport.use(async (ctx, next) => {
|
|
41
|
+
const user = await authenticate(ctx.incomingMessage);
|
|
42
|
+
if (!user) {
|
|
43
|
+
ctx.response.statusCode = 401;
|
|
44
|
+
ctx.response.end('Authentication required');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.userID = user.userId;
|
|
49
|
+
ctx.customData.user = user;
|
|
50
|
+
await next();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Endpoint 必须注册在认证 middleware 之后。
|
|
54
|
+
transport.register(endpoint);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
这里使用 Memory Adapter 保持示例最小,进程退出后数据会丢失。Node HTTP server 和 WebSocket upgrade 的挂载方式见 `@univerjs-pro/collaboration-transport-node` README。
|
|
58
|
+
|
|
59
|
+
全部 Collaboration 与 Univer 前端 package 应使用同一 release cohort 的匹配版本。
|
|
60
|
+
|
|
61
|
+
## 两类请求
|
|
62
|
+
|
|
63
|
+
Endpoint 处理的协议可以先分成两组:文档数据请求和实时 Session。两组请求使用不同的 middleware 和上下文。
|
|
64
|
+
|
|
65
|
+
### 文档数据请求
|
|
66
|
+
|
|
67
|
+
snapshot、block、fetch-missing、new-changes 和 Unit delete/recover 都是 HTTP 请求。它们使用当前 HTTP 请求中由 Transport middleware 挂载的身份和自定义数据:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
HTTP snapshot / block / fetch-missing / new-changes / delete / recover
|
|
71
|
+
→ Transport middleware 设置 ctx.userID 和 ctx.customData
|
|
72
|
+
→ Endpoint 解析协议并调用 UniverCollabService API
|
|
73
|
+
→ Service middleware 通过 ctx.userID 和 ctx.customData 读取
|
|
74
|
+
→ Service / Database Adapter
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Service middleware 获取的是当前 HTTP 请求中由 Transport middleware 挂载的 `ctx.userID` 和
|
|
78
|
+
`ctx.customData`,可用于权限控制、日志和外部系统集成等。下面以读取权限为例。
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { CollabError } from '@univerjs-pro/collaboration-service';
|
|
82
|
+
|
|
83
|
+
collabService.use('readUnitData', async (ctx, next) => {
|
|
84
|
+
// ctx.userID 和 ctx.customData 来自当前 HTTP 请求的 Transport middleware。
|
|
85
|
+
if (!await acl.canRead(ctx.userID, ctx.request.unitID)) {
|
|
86
|
+
throw new CollabError('PERMISSION_DENIED', 'Unit is not accessible');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await next();
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`readUnitData` 保护 snapshot、block 和 fetch-missing。new-changes 和 Unit delete/recover 分别使用 Service 的 `submitChangeset`、`deleteUnits` 和 `recoverUnits` action。
|
|
94
|
+
|
|
95
|
+
### 实时 Session
|
|
96
|
+
|
|
97
|
+
session-ticket 和 WebSocket open、HELLO、HEARTBEAT、JOIN、LEAVE、Presence 属于实时 Session 流程:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
GET /universer-api/user/session-ticket
|
|
101
|
+
→ Transport middleware 设置 ctx.userID 和 ctx.customData
|
|
102
|
+
→ Endpoint 在服务端保存关联,返回一次性 opaque ticket
|
|
103
|
+
→ WebSocket handshake 消费 ticket;无效 ticket 在升级前返回 HTTP 401
|
|
104
|
+
→ Endpoint 创建 Session { userID, memberID, customData }
|
|
105
|
+
→ Endpoint middleware 通过 ctx.session 读取
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
ticket 字符串本身不包含 `userID` 或 `customData`。在所有 HTTP 路由中,只有 session-ticket 请求会在 WebSocket 建连时把当前 `ctx.userID` 和 `ctx.customData` 带入 Session。
|
|
109
|
+
|
|
110
|
+
Endpoint middleware 通过 `ctx.session.userID` 和 `ctx.session.customData` 读取这些数据。`memberID` 由 Endpoint 为每个 WebSocket Session 生成,重连后会变化。
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { CollabError } from '@univerjs-pro/collaboration-service';
|
|
114
|
+
|
|
115
|
+
endpoint.use('joinUnit', async (ctx, next) => {
|
|
116
|
+
// ctx.session.userID 和 ctx.session.customData 来自 session-ticket HTTP 请求。
|
|
117
|
+
if (!await acl.canRead(ctx.session.userID, ctx.unitID)) {
|
|
118
|
+
throw new CollabError('PERMISSION_DENIED', 'Unit is not accessible');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await next();
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `new-changes` 连接两组请求
|
|
126
|
+
|
|
127
|
+
new-changes 是 HTTP 文档数据请求,但它的 ACK 和广播通过 WebSocket 投递:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
HTTP new-changes { memberID }
|
|
131
|
+
→ Endpoint 使用 memberID 查找在线 Session
|
|
132
|
+
→ 验证 session.userID === 当前 HTTP ctx.userID
|
|
133
|
+
→ 验证 Session 已 JOIN 目标 Unit
|
|
134
|
+
→ 使用当前 HTTP 请求的 ctx.userID 和 ctx.customData 调用 Service
|
|
135
|
+
→ 通过 WebSocket 发送 ACK 和广播
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`memberID` 只用于定位 WebSocket Session,不能单独作为身份凭据。这些关联和校验由 Endpoint 完成,应用不需要自己处理。
|
|
139
|
+
|
|
140
|
+
## Middleware
|
|
141
|
+
|
|
142
|
+
Endpoint 提供以下四个实时协议 middleware action。文档数据请求使用的 Service middleware 已在上一节说明,具体 action 见 `@univerjs-pro/collaboration-service` README。
|
|
143
|
+
|
|
144
|
+
| Action | 触发时机 | 可见字段 | 重试语义 | 典型用途 |
|
|
145
|
+
| --- | --- | --- | --- | --- |
|
|
146
|
+
| `connect` | 消费 ticket 并创建 Session 后,处理 `HELLO` 前 | `session`、`connection`、`member` | 每次建连一次;重连会再次执行 | 连接策略、补充成员名称和头像 |
|
|
147
|
+
| `joinUnit` | 收到 `JOIN` 后,Session 加入房间前 | `session`、`unitID` | 当前 Session 首次 JOIN 时一次;Endpoint 不重试 | 房间读取 ACL |
|
|
148
|
+
| `receivePresence` | 已 JOIN 的 Session 发送 Presence 后、向房间广播前 | `session`、`unitID`、`payload`、`customData` | 每条 Presence 一次;Endpoint 不重试 | 校验、限流和过滤 Presence |
|
|
149
|
+
| `sendPresence` | Presence 即将发送给某个房间成员前 | 上述字段、`targetMemberID` | 每个目标成员一次;发送失败不重试 | 按接收者过滤 Presence |
|
|
150
|
+
|
|
151
|
+
表中的 `session` 来自 session-ticket 请求;Presence context 顶层的 `customData` 则是
|
|
152
|
+
Transport 为当前 WebSocket message 创建的独立对象。
|
|
153
|
+
|
|
154
|
+
同一 action 的 middleware 按注册顺序执行;`await next()` 进入下一个 middleware,抛出
|
|
155
|
+
`CollabError` 表示预期拒绝。
|
|
156
|
+
|
|
157
|
+
`joinUnit` 只保护实时房间,不能取代 Service `readUnitData` middleware;未加入房间的客户端仍可能直接发起 snapshot HTTP 请求。
|
|
158
|
+
|
|
159
|
+
## 协议路由参考
|
|
160
|
+
|
|
161
|
+
下表使用默认 `protocolBasePath=/universer-api`。通常由 Univer Collaboration Client 调用这些路由,应用不需要手动封装。
|
|
162
|
+
|
|
163
|
+
| 通道 | Method / Message | Path | 用途 |
|
|
164
|
+
| --- | --- | --- | --- |
|
|
165
|
+
| HTTP | `GET` | `/universer-api/user/session-ticket` | 获取一次性 WebSocket ticket |
|
|
166
|
+
| HTTP | `GET` | `/universer-api/snapshot/:type/unit/:unitID/rev/:revision` | 加载 Unit snapshot 和 changesets |
|
|
167
|
+
| HTTP | `GET` | `/universer-api/snapshot/:type/unit/:unitID/block/:blockID` | 加载 Unit block |
|
|
168
|
+
| HTTP | `GET` | `/universer-api/snapshot/block/:type/unit/:unitID/block/:blockID` | 加载已解析的 Unit block |
|
|
169
|
+
| HTTP | `GET` | `/universer-api/snapshot/:type/unit/:unitID/fetchmissing?from=:from&to=:to` | 加载缺失 changesets |
|
|
170
|
+
| HTTP | `POST` | `/universer-api/comb/:type/unit/:unitID/new_changes` | 提交 changeset |
|
|
171
|
+
| HTTP | `DELETE` | `/universer-api/snapshot/-/units` | 删除 Unit |
|
|
172
|
+
| HTTP | `POST` | `/universer-api/snapshot/-/units/recover` | 恢复 Unit |
|
|
173
|
+
| WebSocket open | `GET` + Upgrade | `/universer-api/comb/connect?sessionTicket=:ticket` | 消费 ticket 并建立 Session |
|
|
174
|
+
| WebSocket | `HELLO/HEARTBEAT/JOIN/LEAVE/Presence` | `/universer-api/comb/connect` | 成员、房间和 Presence |
|
|
175
|
+
|
|
176
|
+
## 配置与部署
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const endpoint = new UniverCollabEndpoint(collabService, {
|
|
180
|
+
protocolBasePath: '/universer-api', // 默认值
|
|
181
|
+
sessionTicketTtlMs: 5 * 60_000, // 默认 5 分钟
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Session、Unit room 和待发送消息队列都在进程内,因此实时广播只保证单 Endpoint 进程。当前没有跨进程的在线成员、房间或广播能力。
|
|
186
|
+
|
|
187
|
+
WebSocket ACK 和广播不保证每条消息都送达,也不是权威数据源。客户端断线后使用 revision
|
|
188
|
+
和 fetch-missing HTTP 补齐 confirmed changesets。
|
|
189
|
+
|
|
190
|
+
### 停止服务
|
|
191
|
+
|
|
192
|
+
Transport 会释放注册的 Endpoint;Endpoint 会清理 Session、房间和 Service event 订阅。应用仍需释放 Service 和 Database Adapter。
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
await transport.dispose();
|
|
196
|
+
await collabService.dispose();
|
|
197
|
+
await database.dispose();
|
|
198
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object['defineProperty'](exports,Symbol['toStringTag'],{'value':'Module'});let e=require('node:crypto'),t=require('@univerjs-pro/collaboration-service'),n=require('@univerjs/protocol'),r=require('@univerjs-pro/collaboration-transport-node');var i={'CHANGESET_ACK':'changeset_ack','CHANGESET_REJ':'changeset_rej','CHANGESET_SHOULD_RETRY':'changeset_should_retry','NEW_CHANGESETS':'new_changesets','PERMISSION_REJ':'permission_rej','UPDATE_CURSOR':'update_cursor','USERS_ENTER':'users_enter','USERS_LEAVE':'users_leave'};function a(_0x3c0785,_0x3b5d51,_0x3081fb=''){return{'cmd':_0x3c0785,'code':n['CmdRspCode']['OK'],'reason':'success','routeKey':_0x3081fb,'infoRsp':{'memberID':_0x3b5d51}};}function o(_0x1856bd,_0xdbf89,_0x3eaa88,_0x556c16){return{'cmd':n['CombCmd']['JOIN'],'code':_0xdbf89,'reason':_0x556c16,'routeKey':_0x1856bd,'joinRsp':{'roomInfos':_0x3eaa88}};}function s(_0x558dd9){return{'memberID':_0x558dd9['memberID'],'userID':_0x558dd9['userID'],'name':_0x558dd9['name'],..._0x558dd9['avatar']===void 0x0?{}:{'avatar':_0x558dd9['avatar']}};}function c(_0x250369,_0x107319){return{'cmd':n['CombCmd']['RECV'],'code':n['CmdRspCode']['OK'],'reason':'success','routeKey':_0x250369,'collaMsg':_0x107319};}function l(_0x4ac451,_0x450e26){return c(_0x4ac451,{'eventID':i['USERS_ENTER'],'joinEvent':{..._0x450e26,'avatar':_0x450e26['avatar']??''}});}function u(_0x5a47e4,_0x39d068){return c(_0x5a47e4,{'eventID':i['USERS_LEAVE'],'leaveEvent':_0x39d068});}function d(_0x5bae29,_0x3a910d){return c(_0x5bae29,{'eventID':i['UPDATE_CURSOR'],'updateCursorEvent':_0x3a910d});}function f(_0xa4ac5d,_0x5a6efb,_0x4866d5){let _0x4c0e74={[i['CHANGESET_ACK']]:'csAckEvent',[i['CHANGESET_REJ']]:'csRejEvent',[i['CHANGESET_SHOULD_RETRY']]:'csShouldRetryEvent',[i['NEW_CHANGESETS']]:'newCsEvent',[i['PERMISSION_REJ']]:'permissionRejEvent'}[_0x5a6efb];return c(_0xa4ac5d,{'eventID':_0x5a6efb,[_0x4c0e74]:{'cs':_0x4866d5}});}function p(_0x363329,_0x56e3e5,_0x5edff1){return c(_0x363329,{..._0x5edff1,'eventID':_0x56e3e5});}var m=class{['_middlewares']=new Map();['use'](_0x54aa7e,_0xa641d7){let _0x4e6cf2=this['_middlewares']['get'](_0x54aa7e)??[],_0x23ec69=_0xa641d7;_0x4e6cf2['push'](_0x23ec69),this['_middlewares']['set'](_0x54aa7e,_0x4e6cf2);let _0x56abbd=!0x1;return{'dispose':()=>{if(_0x56abbd)return;_0x56abbd=!0x0;let _0x10f87f=_0x4e6cf2['indexOf'](_0x23ec69);_0x10f87f>=0x0&&_0x4e6cf2['splice'](_0x10f87f,0x1);}};}async['run'](_0x4c4c75,_0x402617,_0x10c6b8){let _0x59eaa8=[...this['_middlewares']['get'](_0x4c4c75)??[]],_0x209ceb=async _0x1862e2=>{let _0x52697d=_0x59eaa8[_0x1862e2];if(!_0x52697d){await _0x10c6b8();return;}let _0x1ede98=!0x1,_0x1dcf49,_0x5ad235=()=>{if(_0x1ede98)throw Error('Endpoint\x20middleware\x20for\x20'+String(_0x4c4c75)+'\x20called\x20next()\x20more\x20than\x20once');return _0x1ede98=!0x0,_0x1dcf49=_0x209ceb(_0x1862e2+0x1),_0x1dcf49;};try{await _0x52697d(_0x402617,_0x5ad235);}catch(_0x59549d){if(_0x1dcf49)try{await _0x1dcf49;}catch{}throw _0x59549d;}if(!_0x1ede98)throw Error('Endpoint\x20middleware\x20for\x20'+String(_0x4c4c75)+'\x20completed\x20without\x20calling\x20next()');await _0x1dcf49;};await _0x209ceb(0x0);}['clear'](){this['_middlewares']['clear']();}},h={'code':n['ErrorCode']['OK'],'message':''};function g(_0x1ece8e,_0xf234ee){if(!/^-?\d+$/['test'](_0x1ece8e))throw new t['CollabError']('INVALID_REQUEST',_0xf234ee+'\x20must\x20be\x20an\x20integer');let _0x3699c5=Number(_0x1ece8e);if(!Number['isSafeInteger'](_0x3699c5))throw new t['CollabError']('INVALID_REQUEST',_0xf234ee+'\x20is\x20outside\x20the\x20safe\x20integer\x20range');return _0x3699c5;}function _(_0x552166){if(_0x552166 instanceof r['NodeTransportError'])return _0x552166['code']==='REQUEST_BODY_TOO_LARGE'?{'status':0x19d,'error':{'code':n['ErrorCode']['INVALID_ARGUMENT'],'message':_0x552166['message']}}:_0x552166['code']==='INVALID_JSON'||_0x552166['code']==='REQUEST_ABORTED'?{'status':0x190,'error':{'code':n['ErrorCode']['INVALID_ARGUMENT'],'message':_0x552166['message']}}:{'status':0x1f7,'error':{'code':n['ErrorCode']['INTERNAL_ERROR'],'message':'Transport\x20unavailable'}};if(!(_0x552166 instanceof t['CollabError']))return{'status':0x1f4,'error':{'code':n['ErrorCode']['INTERNAL_ERROR'],'message':'Internal\x20server\x20error'}};let _0x2e748f={'UNAUTHENTICATED':{'status':0x191,'code':n['ErrorCode']['UNAUTHENTICATED']},'INVALID_REQUEST':{'status':0x190,'code':n['ErrorCode']['INVALID_ARGUMENT']},'UNIT_NOT_FOUND':{'status':0x194,'code':n['ErrorCode']['NOT_FOUND']},'PERMISSION_DENIED':{'status':0x193,'code':n['ErrorCode']['PERMISSION_DENIED']},'OT_CONFLICT':{'status':0x199,'code':n['ErrorCode']['APPLY_REJECT']},'REVISION_MISMATCH':{'status':0x199,'code':n['ErrorCode']['CHANGESET_REVISION_CONFILICT']},'ADAPTER_FAILURE':{'status':0x1f7,'code':n['ErrorCode']['INTERNAL_ERROR']},'INTERNAL_ERROR':{'status':0x1f4,'code':n['ErrorCode']['INTERNAL_ERROR']}}[_0x552166['code']];return{'status':_0x2e748f['status'],'error':{'code':_0x2e748f['code'],'message':_0x552166['message']}};}function v(_0x23283c,_0x3180fd,_0x59d3d7){_0x23283c['writableEnded']||_0x23283c['destroyed']||(_0x23283c['statusCode']=_0x3180fd,_0x23283c['setHeader']('content-type','application/json;\x20charset=utf-8'),_0x23283c['end'](JSON['stringify'](_0x59d3d7,(_0x53d4fd,_0x23c9ff)=>_0x23c9ff instanceof Uint8Array?Buffer['from'](_0x23c9ff)['toString']('base64'):_0x23c9ff)));}var y=class{['_tickets']=new Map();['_disposed']=!0x1;async['issue'](_0x390601,_0x320632){if(this['_disposed'])throw Error('Session\x20ticket\x20store\x20is\x20disposed');if(!Number['isSafeInteger'](_0x320632)||_0x320632<=0x0)throw TypeError('Session\x20ticket\x20TTL\x20must\x20be\x20a\x20positive\x20safe\x20integer');this['_purgeExpired']();let _0x5266ad;do _0x5266ad=(0x0,e['randomBytes'])(0x20)['toString']('base64url');while(this['_tickets']['has'](_0x5266ad));return this['_tickets']['set'](_0x5266ad,{'record':_0x390601,'expiresAt':Date['now']()+_0x320632}),_0x5266ad;}async['consume'](_0xa1042a){if(this['_disposed']||!_0xa1042a)return null;let _0x483609=this['_tickets']['get'](_0xa1042a);return!_0x483609||(this['_tickets']['delete'](_0xa1042a),_0x483609['expiresAt']<=Date['now']())?null:_0x483609['record'];}async['dispose'](){this['_disposed']||(this['_disposed']=!0x0,this['_tickets']['clear']());}['_purgeExpired'](){let _0x568ac1=Date['now']();for(let [_0x324ce0,_0x371181]of this['_tickets'])_0x371181['expiresAt']<=_0x568ac1&&this['_tickets']['delete'](_0x324ce0);}},b=0x5*0xea60;function x(_0x345890){try{return decodeURIComponent(_0x345890);}catch{throw new t['CollabError']('INVALID_REQUEST','Route\x20contains\x20invalid\x20encoding');}}function S(_0x13c583,_0x498bf3,_0x9d8369){let _0x1835e3=C(_0x498bf3,_0x9d8369);if(_0x1835e3===null)return null;if(_0x13c583==='GET'&&_0x1835e3==='/user/session-ticket')return{'name':'ticket','params':{}};if(_0x13c583==='DELETE'&&_0x1835e3==='/snapshot/-/units')return{'name':'delete-units','params':{}};if(_0x13c583==='POST'&&_0x1835e3==='/snapshot/-/units/recover')return{'name':'recover-units','params':{}};let _0x532efb=_0x1835e3['split']('/')['filter'](Boolean);if(_0x13c583==='POST'&&_0x532efb['length']===0x5&&_0x532efb[0x0]==='comb'&&_0x532efb[0x2]==='unit'&&_0x532efb[0x4]==='new_changes')return{'name':'new-changes','params':{'type':x(_0x532efb[0x1]),'unitID':x(_0x532efb[0x3])}};if(_0x13c583!=='GET'||_0x532efb[0x0]!=='snapshot')return null;if(_0x532efb['length']===0x7&&_0x532efb[0x1]==='block'&&_0x532efb[0x3]==='unit'&&_0x532efb[0x5]==='block')return{'name':'deserialized-block','params':{'type':x(_0x532efb[0x2]),'unitID':x(_0x532efb[0x4]),'blockID':x(_0x532efb[0x6])}};if(_0x532efb['length']<0x5||_0x532efb[0x2]!=='unit')return null;let _0x55a4fe={'type':x(_0x532efb[0x1]),'unitID':x(_0x532efb[0x3])};return _0x532efb['length']===0x6&&_0x532efb[0x4]==='rev'?{'name':'unit','params':{..._0x55a4fe,'revision':x(_0x532efb[0x5])}}:_0x532efb['length']===0x6&&_0x532efb[0x4]==='block'?{'name':'block','params':{..._0x55a4fe,'blockID':x(_0x532efb[0x5])}}:_0x532efb['length']===0x5&&_0x532efb[0x4]==='fetchmissing'?{'name':'fetch-missing','params':_0x55a4fe}:null;}function C(_0x1bba74,_0x20d8f1){if(_0x1bba74===_0x20d8f1)return'/';let _0x447e60=_0x20d8f1+'/';return _0x1bba74['startsWith'](_0x447e60)?_0x1bba74['slice'](_0x20d8f1['length']):null;}function w(_0x5ba884){if(!_0x5ba884['startsWith']('/')||_0x5ba884==='/'||_0x5ba884['endsWith']('/')||_0x5ba884['includes']('?')||_0x5ba884['includes']('#'))throw TypeError('protocolBasePath\x20must\x20be\x20an\x20absolute\x20path\x20without\x20a\x20trailing\x20slash');return _0x5ba884;}function T(_0xaa63ae){if(!_0xaa63ae||typeof _0xaa63ae!='object')throw new t['CollabError']('INVALID_REQUEST','Request\x20body\x20must\x20be\x20an\x20object');let _0x5c6524=_0xaa63ae;if(typeof _0x5c6524['unitID']!='string'||typeof _0x5c6524['memberID']!='string'||!Number['isSafeInteger'](_0x5c6524['type'])||!_0x5c6524['changeset']||typeof _0x5c6524['changeset']!='object')throw new t['CollabError']('INVALID_REQUEST','Invalid\x20new\x20changes\x20request');}function E(_0x33fab0){if(!Array['isArray'](_0x33fab0)||_0x33fab0['length']===0x0||_0x33fab0['some'](_0x2fdb5d=>typeof _0x2fdb5d!='string'||_0x2fdb5d['trim']()['length']===0x0))throw new t['CollabError']('INVALID_REQUEST','unitIds\x20must\x20be\x20a\x20non-empty\x20string\x20array');}function D(_0x2a9245){let _0x3fc324=_0x2a9245['getAll']('unitIds');E(_0x3fc324);let _0x83b0a2=_0x2a9245['getAll']('hardDelete');if(_0x83b0a2['length']>0x1||_0x83b0a2['length']===0x1&&_0x83b0a2[0x0]!=='true'&&_0x83b0a2[0x0]!=='false')throw new t['CollabError']('INVALID_REQUEST','hardDelete\x20must\x20be\x20true\x20or\x20false');return{'unitIds':_0x3fc324,'hardDelete':_0x83b0a2[0x0]==='true'};}function O(_0x487c3b){if(!_0x487c3b||typeof _0x487c3b!='object')throw new t['CollabError']('INVALID_REQUEST','Request\x20body\x20must\x20be\x20an\x20object');E(_0x487c3b['unitIds']);}var k=class{['_service'];['_middleware']=new m();['_listeners']=new Map();['_bindingsByConnection']=new Map();['_bindingsByMember']=new Map();['_rooms']=new Map();['_outboundQueues']=new Map();['_ticketStore'];['_ownsTicketStore'];['_ticketTtlMs'];['_protocolBasePath'];['_combPath'];['_serviceSubscriptions'];['_deletedUnitIDs']=new Set();['_disposed']=!0x1;constructor(_0x4669ed,_0x268ef1={}){if(this['_service']=_0x4669ed,!_0x4669ed)throw TypeError('UniverCollabEndpoint\x20requires\x20a\x20service');if(this['_ticketStore']=_0x268ef1['ticketStore']??new y(),this['_ownsTicketStore']=_0x268ef1['ticketStore']===void 0x0,this['_ticketTtlMs']=_0x268ef1['sessionTicketTtlMs']??b,!Number['isSafeInteger'](this['_ticketTtlMs'])||this['_ticketTtlMs']<=0x0)throw TypeError('sessionTicketTtlMs\x20must\x20be\x20a\x20positive\x20safe\x20integer');this['_protocolBasePath']=w(_0x268ef1['protocolBasePath']??'/universer-api'),this['_combPath']=this['_protocolBasePath']+'/comb/connect',this['_serviceSubscriptions']=[this['_service']['on']('changesetCommitted',_0x1cc29d=>{this['_enqueueCommittedChangeset'](_0x1cc29d['changeset'],_0x1cc29d['memberID']);}),this['_service']['on']('unitsDeleted',_0x3e409d=>{for(let _0xb906c9 of _0x3e409d['request']['unitIDs'])this['_deletedUnitIDs']['add'](_0xb906c9);for(let _0x12d6e6 of _0x3e409d['request']['unitIDs'])this['_enqueueUnitOutbound'](_0x12d6e6,async()=>{this['_evictUnitRoom'](_0x12d6e6);});}),this['_service']['on']('unitsRecovered',async _0x5111b3=>{await Promise['all'](_0x5111b3['request']['unitIDs']['map'](_0x361122=>this['_enqueueUnitOutbound'](_0x361122,async()=>{this['_deletedUnitIDs']['delete'](_0x361122);})));})];}['use'](_0x3efbd4,_0x19ad79){return this['_assertRunning'](),this['_middleware']['use'](_0x3efbd4,_0x19ad79);}['on'](_0x1ff7e6,_0x1f04c1){this['_assertRunning']();let _0x588695=this['_listeners']['get'](_0x1ff7e6)??new Set(),_0x50c23d=_0x1f04c1;_0x588695['add'](_0x50c23d),this['_listeners']['set'](_0x1ff7e6,_0x588695);let _0x2ea833=!0x1;return{'dispose':()=>{_0x2ea833||(_0x2ea833=!0x0,_0x588695['delete'](_0x50c23d));}};}['findJoinedSession'](_0x4e56b4){if(this['_disposed'])return null;let _0x549f68=this['_bindingsByMember']['get'](_0x4e56b4['memberID']);return!_0x549f68||_0x549f68['session']['userID']!==_0x4e56b4['authenticatedUserID']||!_0x549f68['joinedUnitIDs']['has'](_0x4e56b4['unitID'])?null:_0x549f68['session'];}['findJoinedSessionByUser'](_0x57301c){if(this['_disposed'])return null;for(let _0x304aeb of this['_rooms']['get'](_0x57301c['unitID'])?.['values']()??[])if(_0x304aeb['session']['userID']===_0x57301c['authenticatedUserID'])return _0x304aeb['session'];return null;}async['publishTrustedUnitEvent'](_0x4407ae){this['_assertRunning']();let _0x452056=[...this['_rooms']['get'](_0x4407ae['unitID'])?.['keys']()??[]],_0x20141c=this['_bindingsByMember']['get'](_0x4407ae['memberID'])?.['joinedUnitIDs']['has'](_0x4407ae['unitID'])?_0x4407ae['memberID']:void 0x0,_0x131064=p(_0x4407ae['unitID'],_0x4407ae['eventID'],_0x4407ae['data']);this['_enqueueUnitOutbound'](_0x4407ae['unitID'],async()=>{for(let _0x4aed07 of _0x452056){if(_0x4aed07===_0x20141c)continue;let _0x5529ab=this['_bindingsByMember']['get'](_0x4aed07);_0x5529ab?.['joinedUnitIDs']['has'](_0x4407ae['unitID'])&&await this['_safeSend'](_0x5529ab['connection'],_0x131064);}});}async['handleHttp'](_0x14f7b8,_0x5054f9){if(this['_disposed']){await _0x5054f9();return;}await this['_handleHttp'](_0x14f7b8,_0x5054f9);}async['handleUpgrade'](_0x2e6147,_0x314d21){if(this['_disposed']){await _0x314d21();return;}let _0x53bbd5;try{_0x53bbd5=new URL(_0x2e6147['incomingMessage']['url']??'/','http://localhost');}catch{_0x2e6147['reject'](0x190,'Invalid\x20WebSocket\x20URL');return;}if(_0x53bbd5['pathname']!==this['_combPath']){await _0x314d21();return;}let _0x17f0a1=await this['_ticketStore']['consume'](_0x53bbd5['searchParams']['get']('sessionTicket')??'');if(!_0x17f0a1){_0x2e6147['reject'](0x191,'Invalid\x20or\x20expired\x20session\x20ticket');return;}_0x2e6147['accept']({'open':({connection:_0x1dc489})=>this['_openConnection'](_0x17f0a1,_0x1dc489),'message':_0x502344=>this['_handleMessage'](_0x502344),'close':({connection:_0x5506c8})=>this['_disconnectConnection'](_0x5506c8,'disconnect')});}async['dispose'](){if(this['_disposed'])return;this['_disposed']=!0x0;for(let _0x167da8 of this['_serviceSubscriptions'])_0x167da8['dispose']();await Promise['allSettled']([...this['_outboundQueues']['values']()]);let _0x282353=[...this['_bindingsByConnection']['values']()];for(let _0x241bc6 of _0x282353)await this['_disconnectBinding'](_0x241bc6,'endpoint-dispose'),_0x241bc6['connection']['close'](0x3e9,'Endpoint\x20disposed');this['_bindingsByConnection']['clear'](),this['_bindingsByMember']['clear'](),this['_rooms']['clear'](),this['_outboundQueues']['clear'](),this['_middleware']['clear'](),this['_listeners']['clear'](),this['_ownsTicketStore']&&await this['_ticketStore']['dispose']?.();}async['_handleHttp'](_0x5842ac,_0x1864d4){let _0x4d3bfa,_0x2342a3;try{_0x4d3bfa=new URL(_0x5842ac['incomingMessage']['url']??'/','http://localhost'),_0x2342a3=S(_0x5842ac['incomingMessage']['method'],_0x4d3bfa['pathname'],this['_protocolBasePath']);}catch(_0x4e4da0){let _0x489b6d=_(_0x4e4da0);v(_0x5842ac['response'],_0x489b6d['status'],{'error':_0x489b6d['error']});return;}if(!_0x2342a3){await _0x1864d4();return;}try{if(!_0x5842ac['userID'])throw new t['CollabError']('UNAUTHENTICATED','HTTP\x20user\x20is\x20not\x20authenticated');if(_0x2342a3['name']==='ticket'){let _0x56afac=await this['_ticketStore']['issue']({'userID':_0x5842ac['userID'],'customData':_0x5842ac['customData']},this['_ticketTtlMs']);v(_0x5842ac['response'],0xc8,{'error':h,'ticket':_0x56afac});return;}let _0x3e50dd={'userID':_0x5842ac['userID'],'customData':_0x5842ac['customData']};if(_0x2342a3['name']==='delete-units'){let _0x528b10=D(_0x4d3bfa['searchParams']);await this['_service']['deleteUnits']({'unitIDs':_0x528b10['unitIds'],'hardDelete':_0x528b10['hardDelete']},_0x3e50dd),v(_0x5842ac['response'],0xc8,{'error':h});return;}if(_0x2342a3['name']==='recover-units'){let _0x2554ce=await _0x5842ac['readJson']();O(_0x2554ce),await this['_service']['recoverUnits']({'unitIDs':_0x2554ce['unitIds']},_0x3e50dd),v(_0x5842ac['response'],0xc8,{'error':h});return;}let _0x5d3978=g(_0x2342a3['params']['type'],'type'),_0x509473=_0x2342a3['params']['unitID'];if(_0x2342a3['name']==='new-changes'){await this['_handleNewChanges'](_0x5842ac,_0x509473,_0x5d3978);return;}switch(_0x2342a3['name']){case'unit':{let _0x452dfe=g(_0x2342a3['params']['revision'],'revision'),_0x16fb45=await this['_service']['getUnitLoadData']({'unitID':_0x509473,'type':_0x5d3978,'revision':_0x452dfe},_0x3e50dd);v(_0x5842ac['response'],0xc8,{'error':h,'snapshot':_0x16fb45['snapshot'],'changesets':_0x16fb45['changesets']});return;}case'block':case'deserialized-block':{let _0x19feb4=await this['_service']['getSheetBlock']({'unitID':_0x509473,'type':_0x5d3978,'blockID':_0x2342a3['params']['blockID']},_0x3e50dd);if(!_0x19feb4['block'])throw new t['CollabError']('UNIT_NOT_FOUND','Sheet\x20block\x20was\x20not\x20found');let _0x50cdcc=_0x2342a3['name']==='deserialized-block'?{..._0x19feb4['block'],'data':JSON['parse'](new TextDecoder()['decode'](_0x19feb4['block']['data']))}:_0x19feb4['block'];v(_0x5842ac['response'],0xc8,{'error':h,'block':_0x50cdcc});return;}case'fetch-missing':{let _0x3a5f5e=g(_0x4d3bfa['searchParams']['get']('from')??'','from'),_0x5abb53=g(_0x4d3bfa['searchParams']['get']('to')??'','to'),_0x248bf3=await this['_service']['getChangesets']({'unitID':_0x509473,'type':_0x5d3978,'from':_0x3a5f5e,'to':_0x5abb53},_0x3e50dd);v(_0x5842ac['response'],0xc8,{'error':h,'changesets':_0x248bf3['changesets'],'latestRevision':_0x248bf3['latestRevision']});return;}}}catch(_0x5eb2a9){let _0x1c049c=_(_0x5eb2a9 instanceof SyntaxError?new t['CollabError']('INTERNAL_ERROR','Stored\x20sheet\x20block\x20is\x20not\x20valid\x20JSON',{'cause':_0x5eb2a9}):_0x5eb2a9);v(_0x5842ac['response'],_0x1c049c['status'],{'error':_0x1c049c['error']});}}async['_openConnection'](_0x661447,_0x157f41){try{let _0x55cd43=(0x0,e['randomUUID'])(),_0x33e47f={'memberID':_0x55cd43,'userID':_0x661447['userID'],'customData':_0x661447['customData']},_0x2051dc={'memberID':_0x55cd43,'userID':_0x661447['userID'],'name':_0x661447['userID']},_0x5e6746={'connection':_0x157f41,'session':_0x33e47f,'member':_0x2051dc};await this['_middleware']['run']('connect',_0x5e6746,()=>void 0x0);let _0x45c9af={'connection':_0x157f41,'session':_0x33e47f,'member':_0x2051dc,'joinedUnitIDs':new Set()};this['_bindingsByConnection']['set'](_0x157f41['id'],_0x45c9af),this['_bindingsByMember']['set'](_0x55cd43,_0x45c9af);}catch(_0x50807a){_0x157f41['close'](_0x50807a instanceof t['CollabError']?0x3f0:0x3f3,_0x50807a instanceof t['CollabError']?'Connection\x20rejected':'Endpoint\x20failure');}}async['_handleMessage'](_0x21a502){let _0x2feb97=this['_bindingsByConnection']['get'](_0x21a502['connection']['id']);if(!_0x2feb97){_0x21a502['connection']['close'](0x3f0,'WebSocket\x20Session\x20is\x20not\x20established');return;}if(typeof _0x21a502['data']!='string'){_0x21a502['connection']['close'](0x3eb,'Comb\x20messages\x20must\x20be\x20JSON\x20text');return;}let _0x24caa1;try{let _0x3f83dd=JSON['parse'](_0x21a502['data']);if(!_0x3f83dd||typeof _0x3f83dd!='object')throw Error('invalid');_0x24caa1=_0x3f83dd;}catch{_0x21a502['connection']['close'](0x3eb,'Invalid\x20Comb\x20JSON\x20message');return;}let _0xe3b261=typeof _0x24caa1['routeKey']=='string'?_0x24caa1['routeKey']:'';switch(_0x24caa1['cmd']){case n['CombCmd']['HELLO']:case n['CombCmd']['HEARTBEAT']:await _0x21a502['connection']['send'](JSON['stringify'](a(_0x24caa1['cmd'],_0x2feb97['member']['memberID'],_0xe3b261)));return;case n['CombCmd']['JOIN']:await this['_handleJoin'](_0x2feb97,_0x24caa1,_0xe3b261);return;case n['CombCmd']['LEAVE']:await this['_handleLeave'](_0x2feb97,_0x24caa1,_0xe3b261);return;case n['CombCmd']['INGEST']:if(_0x24caa1['collaMsg']?.['eventID']!==i['UPDATE_CURSOR'])return;await this['_handlePresence'](_0x2feb97,_0x24caa1,_0x21a502['customData']);return;default:return;}}async['_handleJoin'](_0x42a906,_0x388cfc,_0x4bab25){let _0xd2fc9d=_0x388cfc['joinReq']?.['rooms'],_0x52db91=_0xd2fc9d?.['length']?_0xd2fc9d['map'](_0x4429ec=>_0x4429ec['roomID']):_0x4bab25?[_0x4bab25]:[];if(_0x52db91['length']===0x0||_0x52db91['some'](_0x222cc2=>typeof _0x222cc2!='string'||!_0x222cc2)){await _0x42a906['connection']['send'](JSON['stringify'](o(_0x4bab25,n['CmdRspCode']['FAIL'],{},'Invalid\x20JOIN\x20request')));return;}let _0x422a95=[...new Set(_0x52db91)],_0x18b622=_0x422a95['filter'](_0x306da6=>!_0x42a906['joinedUnitIDs']['has'](_0x306da6));try{for(let _0x5bd05c of _0x422a95)this['_assertJoinable'](_0x5bd05c);for(let _0x34c565 of _0x18b622)await this['_middleware']['run']('joinUnit',{'session':_0x42a906['session'],'unitID':_0x34c565},()=>void 0x0);for(let _0x2fc986 of _0x422a95)this['_assertJoinable'](_0x2fc986);}catch(_0x3f9cb0){let _0x4a2ff4=_0x3f9cb0 instanceof t['CollabError']&&_0x3f9cb0['code']==='UNIT_NOT_FOUND'?n['CmdRspCode']['JOIN_ROOM_NOT_EXISTS']:_0x3f9cb0 instanceof t['CollabError']&&(_0x3f9cb0['code']==='PERMISSION_DENIED'||_0x3f9cb0['code']==='UNAUTHENTICATED')?n['CmdRspCode']['JOIN_ROOM_PERMISSION_DENIED']:n['CmdRspCode']['FAIL'];await _0x42a906['connection']['send'](JSON['stringify'](o(_0x4bab25||_0x422a95[0x0],_0x4a2ff4,{},_0x3f9cb0 instanceof t['CollabError']?_0x3f9cb0['message']:'JOIN\x20failed')));return;}for(let _0x406d89 of _0x18b622){let _0x6858a3=this['_rooms']['get'](_0x406d89)??new Map();_0x6858a3['set'](_0x42a906['member']['memberID'],_0x42a906),this['_rooms']['set'](_0x406d89,_0x6858a3),_0x42a906['joinedUnitIDs']['add'](_0x406d89);}let _0x5a324f={};for(let _0x21bbe7 of _0x422a95)_0x5a324f[_0x21bbe7]={'roomID':_0x21bbe7,'members':[...this['_rooms']['get'](_0x21bbe7)?.['values']()??[]]['map'](_0x54e5e9=>s(_0x54e5e9['member']))};await _0x42a906['connection']['send'](JSON['stringify'](o(_0x4bab25||_0x422a95[0x0],n['CmdRspCode']['OK'],_0x5a324f,'success')));for(let _0x5a67ff of _0x18b622)await this['_broadcastToRoom'](_0x5a67ff,l(_0x5a67ff,s(_0x42a906['member'])),_0x42a906['member']['memberID']);}async['_handleLeave'](_0xc7fcdb,_0x4be55d,_0x12ec82){let _0x2e9805=typeof _0x4be55d['leaveReq']?.['roomID']=='string'?_0x4be55d['leaveReq']['roomID']:_0x12ec82;_0x2e9805&&await this['_leaveUnit'](_0xc7fcdb,_0x2e9805,'explicit');}async['_handlePresence'](_0x4e2b64,_0x5cc328,_0x5e5a06){let _0x1e58b9=typeof _0x5cc328['routeKey']=='string'?_0x5cc328['routeKey']:'',_0x48e9ce=_0x5cc328['collaMsg']?.['updateCursorEvent']?.['selection'];if(!_0x1e58b9||!_0x4e2b64['joinedUnitIDs']['has'](_0x1e58b9)||typeof _0x48e9ce!='string')return;let _0x47554d={'unitID':_0x1e58b9,'memberID':_0x4e2b64['member']['memberID'],'selection':_0x48e9ce};try{await this['_middleware']['run']('receivePresence',{'session':_0x4e2b64['session'],'unitID':_0x1e58b9,'payload':_0x47554d,'customData':_0x5e5a06},()=>void 0x0);}catch(_0x1d8c2d){if(_0x1d8c2d instanceof t['CollabError'])return;throw _0x1d8c2d;}if(typeof _0x47554d['selection']!='string')return;let _0x17b059=[...this['_rooms']['get'](_0x1e58b9)?.['values']()??[]]['filter'](_0x1ed5d0=>_0x1ed5d0!==_0x4e2b64);for(let _0x530911 of _0x17b059){let _0x1c2ab1={'unitID':_0x1e58b9,'memberID':_0x4e2b64['member']['memberID'],'selection':_0x47554d['selection']};try{await this['_middleware']['run']('sendPresence',{'session':_0x4e2b64['session'],'unitID':_0x1e58b9,'payload':_0x1c2ab1,'customData':_0x5e5a06,'targetMemberID':_0x530911['member']['memberID']},()=>void 0x0);}catch{continue;}typeof _0x1c2ab1['selection']=='string'&&await this['_safeSend'](_0x530911['connection'],d(_0x1e58b9,{'unitID':_0x1e58b9,'memberID':_0x4e2b64['member']['memberID'],'selection':_0x1c2ab1['selection']}));}}async['_handleNewChanges'](_0x115c1a,_0x65408a,_0x111c51){let _0x5734af=await _0x115c1a['readJson']();if(T(_0x5734af),_0x5734af['unitID']!==_0x65408a||_0x5734af['type']!==_0x111c51||_0x5734af['changeset']?.['unitID']!==_0x65408a||_0x5734af['changeset']['type']!==_0x111c51)throw new t['CollabError']('INVALID_REQUEST','Route,\x20request\x20and\x20changeset\x20identity\x20must\x20match');let _0x21b556=this['_bindingsByMember']['get'](_0x5734af['memberID']);if(!_0x21b556||_0x21b556['session']['userID']!==_0x115c1a['userID'])throw new t['CollabError']('PERMISSION_DENIED','memberID\x20does\x20not\x20belong\x20to\x20the\x20authenticated\x20user');if(!_0x21b556['joinedUnitIDs']['has'](_0x65408a))throw new t['CollabError']('PERMISSION_DENIED','Session\x20has\x20not\x20joined\x20this\x20unit');let _0x366ec6=await this['_service']['submitChangeset']({'changeset':_0x5734af['changeset']},{'userID':_0x115c1a['userID'],'memberID':_0x21b556['session']['memberID'],'customData':_0x115c1a['customData']});if(_0x366ec6['status']==='already-committed')await this['_safeSend'](_0x21b556['connection'],f(_0x65408a,i['CHANGESET_ACK'],_0x366ec6['changeset']));else{if(_0x366ec6['status']==='rejected'){let _0x25a659=_0x366ec6['error']['code']==='PERMISSION_DENIED'||_0x366ec6['error']['code']==='UNAUTHENTICATED'?i['PERMISSION_REJ']:i['CHANGESET_REJ'];await this['_safeSend'](_0x21b556['connection'],f(_0x65408a,_0x25a659,this['_trustedClientChangeset'](_0x5734af['changeset'],_0x21b556)));}else _0x366ec6['status']==='retry'&&await this['_safeSend'](_0x21b556['connection'],f(_0x65408a,i['CHANGESET_SHOULD_RETRY'],this['_trustedClientChangeset'](_0x5734af['changeset'],_0x21b556)));}v(_0x115c1a['response'],0xc8,{'error':h});}['_trustedClientChangeset'](_0x195756,_0x528622){return{..._0x195756,'userID':_0x528622['session']['userID'],'memberID':_0x528622['session']['memberID']};}['_enqueueCommittedChangeset'](_0x451312,_0x24db79){if(this['_disposed'])return;let _0xe286f=_0x451312['unitID'],_0x1f1aa6=[...this['_rooms']['get'](_0xe286f)?.['keys']()??[]];this['_enqueueUnitOutbound'](_0xe286f,async()=>{for(let _0x2cde9b of _0x1f1aa6){let _0x41502b=this['_bindingsByMember']['get'](_0x2cde9b);if(!_0x41502b?.['joinedUnitIDs']['has'](_0xe286f))continue;let _0x1e43a7=_0x2cde9b===_0x24db79?i['CHANGESET_ACK']:i['NEW_CHANGESETS'];await this['_safeSend'](_0x41502b['connection'],f(_0xe286f,_0x1e43a7,_0x451312));}});}['_evictUnitRoom'](_0x58bb3c){let _0x4f1cee=this['_rooms']['get'](_0x58bb3c);if(_0x4f1cee){this['_rooms']['delete'](_0x58bb3c);for(let _0xe46223 of _0x4f1cee['values']())_0xe46223['joinedUnitIDs']['delete'](_0x58bb3c);}}['_enqueueUnitOutbound'](_0xa281c4,_0x561703){let _0x53a7df=(this['_outboundQueues']['get'](_0xa281c4)??Promise['resolve']())['catch'](()=>void 0x0)['then'](_0x561703);return this['_outboundQueues']['set'](_0xa281c4,_0x53a7df),_0x53a7df['finally'](()=>{this['_outboundQueues']['get'](_0xa281c4)===_0x53a7df&&this['_outboundQueues']['delete'](_0xa281c4);}),_0x53a7df;}async['_disconnectConnection'](_0x26d4cc,_0x1e7608){let _0x5ae3ad=this['_bindingsByConnection']['get'](_0x26d4cc['id']);_0x5ae3ad&&await this['_disconnectBinding'](_0x5ae3ad,_0x1e7608);}async['_disconnectBinding'](_0x28cb96,_0x152922){this['_bindingsByConnection']['delete'](_0x28cb96['connection']['id']),this['_bindingsByMember']['delete'](_0x28cb96['member']['memberID']);let _0x1247ef=[..._0x28cb96['joinedUnitIDs']];for(let _0x130a7d of _0x1247ef)await this['_leaveUnit'](_0x28cb96,_0x130a7d,_0x152922);}async['_leaveUnit'](_0x5b36e8,_0x1c46b6,_0x54091c){if(!_0x5b36e8['joinedUnitIDs']['delete'](_0x1c46b6))return!0x1;let _0x582da3=this['_rooms']['get'](_0x1c46b6);return _0x582da3?.['delete'](_0x5b36e8['member']['memberID']),_0x582da3?.['size']===0x0&&this['_rooms']['delete'](_0x1c46b6),await this['_broadcastToRoom'](_0x1c46b6,u(_0x1c46b6,{'memberID':_0x5b36e8['member']['memberID'],'name':_0x5b36e8['member']['name']}),_0x5b36e8['member']['memberID']),await this['_emit']('memberLeftUnit',{'session':_0x5b36e8['session'],'unitID':_0x1c46b6,'reason':_0x54091c}),!0x0;}async['_broadcastToRoom'](_0x1e5b66,_0x4bc0f2,_0x3e38d9){let _0x466145=[...this['_rooms']['get'](_0x1e5b66)?.['values']()??[]];await Promise['allSettled'](_0x466145['filter'](_0x23f0a0=>_0x23f0a0['member']['memberID']!==_0x3e38d9)['map'](_0x15727f=>this['_safeSend'](_0x15727f['connection'],_0x4bc0f2)));}async['_safeSend'](_0x368785,_0x4ef9b1){try{await _0x368785['send'](JSON['stringify'](_0x4ef9b1));}catch{}}async['_emit'](_0x260df5,_0x41e944){let _0x222380=[...this['_listeners']['get'](_0x260df5)??[]];for(let _0x522b58 of _0x222380)try{await _0x522b58(_0x41e944);}catch{}}['_assertRunning'](){if(this['_disposed'])throw Error('UniverCollabEndpoint\x20is\x20disposed');}['_assertJoinable'](_0x133359){if(this['_deletedUnitIDs']['has'](_0x133359))throw new t['CollabError']('UNIT_NOT_FOUND','Unit\x20has\x20been\x20deleted');}};exports['MemorySessionTicketStore']=y,exports['UniverCollabEndpoint']=k;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { CustomData, Disposable, IUniverCollabService } from "@univerjs-pro/collaboration-service";
|
|
2
|
+
import { NodeHttpTransportContext, NodeTransportConnection, NodeTransportEndpoint, NodeTransportNext, NodeWebSocketEndpointContext } from "@univerjs-pro/collaboration-transport-node";
|
|
3
|
+
|
|
4
|
+
//#region src/endpoint.d.ts
|
|
5
|
+
type EndpointListener<K extends keyof EndpointEventMap> = (event: EndpointEventMap[K]) => void | Promise<void>;
|
|
6
|
+
declare class UniverCollabEndpoint implements NodeTransportEndpoint, ITrustedUnitRoomHost {
|
|
7
|
+
private readonly _service;
|
|
8
|
+
private readonly _middleware;
|
|
9
|
+
private readonly _listeners;
|
|
10
|
+
private readonly _bindingsByConnection;
|
|
11
|
+
private readonly _bindingsByMember;
|
|
12
|
+
private readonly _rooms;
|
|
13
|
+
private readonly _outboundQueues;
|
|
14
|
+
private readonly _ticketStore;
|
|
15
|
+
private readonly _ownsTicketStore;
|
|
16
|
+
private readonly _ticketTtlMs;
|
|
17
|
+
private readonly _protocolBasePath;
|
|
18
|
+
private readonly _combPath;
|
|
19
|
+
private readonly _serviceSubscriptions;
|
|
20
|
+
private readonly _deletedUnitIDs;
|
|
21
|
+
private _disposed;
|
|
22
|
+
constructor(_service: IUniverCollabService, options?: UniverCollabEndpointOptions);
|
|
23
|
+
use<K extends EndpointMiddlewareAction>(action: K, middleware: EndpointMiddlewareMap[K]): Disposable;
|
|
24
|
+
on<K extends keyof EndpointEventMap>(event: K, listener: EndpointListener<K>): Disposable;
|
|
25
|
+
findJoinedSession(input: {
|
|
26
|
+
readonly unitID: string;
|
|
27
|
+
readonly memberID: string;
|
|
28
|
+
readonly authenticatedUserID: string;
|
|
29
|
+
}): EndpointSession | null;
|
|
30
|
+
findJoinedSessionByUser(input: {
|
|
31
|
+
readonly unitID: string;
|
|
32
|
+
readonly authenticatedUserID: string;
|
|
33
|
+
}): EndpointSession | null;
|
|
34
|
+
publishTrustedUnitEvent(input: {
|
|
35
|
+
readonly unitID: string;
|
|
36
|
+
readonly memberID: string;
|
|
37
|
+
readonly eventID: string;
|
|
38
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
handleHttp(ctx: NodeHttpTransportContext, next: NodeTransportNext): Promise<void>;
|
|
41
|
+
handleUpgrade(ctx: NodeWebSocketEndpointContext, next: NodeTransportNext): Promise<void>;
|
|
42
|
+
dispose(): Promise<void>;
|
|
43
|
+
private _handleHttp;
|
|
44
|
+
private _openConnection;
|
|
45
|
+
private _handleMessage;
|
|
46
|
+
private _handleJoin;
|
|
47
|
+
private _handleLeave;
|
|
48
|
+
private _handlePresence;
|
|
49
|
+
private _handleNewChanges;
|
|
50
|
+
private _trustedClientChangeset;
|
|
51
|
+
private _enqueueCommittedChangeset;
|
|
52
|
+
private _evictUnitRoom;
|
|
53
|
+
private _enqueueUnitOutbound;
|
|
54
|
+
private _disconnectConnection;
|
|
55
|
+
private _disconnectBinding;
|
|
56
|
+
private _leaveUnit;
|
|
57
|
+
private _broadcastToRoom;
|
|
58
|
+
private _safeSend;
|
|
59
|
+
private _emit;
|
|
60
|
+
private _assertRunning;
|
|
61
|
+
private _assertJoinable;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/ticket-store.d.ts
|
|
65
|
+
declare class MemorySessionTicketStore implements ISessionTicketStore {
|
|
66
|
+
private readonly _tickets;
|
|
67
|
+
private _disposed;
|
|
68
|
+
issue(record: SessionTicketRecord, ttlMs: number): Promise<string>;
|
|
69
|
+
consume(ticket: string): Promise<SessionTicketRecord | null>;
|
|
70
|
+
dispose(): Promise<void>;
|
|
71
|
+
private _purgeExpired;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/univer-collab-endpoint.d.ts
|
|
75
|
+
interface SessionTicketRecord {
|
|
76
|
+
readonly userID: string;
|
|
77
|
+
readonly customData: CustomData;
|
|
78
|
+
}
|
|
79
|
+
interface EndpointSession {
|
|
80
|
+
readonly userID: string;
|
|
81
|
+
readonly memberID: string;
|
|
82
|
+
readonly customData: CustomData;
|
|
83
|
+
}
|
|
84
|
+
interface ISessionTicketStore {
|
|
85
|
+
issue(record: SessionTicketRecord, ttlMs: number): Promise<string>;
|
|
86
|
+
consume(ticket: string): Promise<SessionTicketRecord | null>;
|
|
87
|
+
dispose?(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
interface EndpointSessionBinding {
|
|
90
|
+
readonly connection: NodeTransportConnection;
|
|
91
|
+
readonly session: EndpointSession;
|
|
92
|
+
readonly member: EndpointMember;
|
|
93
|
+
readonly joinedUnitIDs: ReadonlySet<string>;
|
|
94
|
+
}
|
|
95
|
+
interface ITrustedUnitRoomHost {
|
|
96
|
+
findJoinedSession(input: {
|
|
97
|
+
readonly unitID: string;
|
|
98
|
+
readonly memberID: string;
|
|
99
|
+
readonly authenticatedUserID: string;
|
|
100
|
+
}): EndpointSession | null;
|
|
101
|
+
findJoinedSessionByUser(input: {
|
|
102
|
+
readonly unitID: string;
|
|
103
|
+
readonly authenticatedUserID: string;
|
|
104
|
+
}): EndpointSession | null;
|
|
105
|
+
publishTrustedUnitEvent(input: {
|
|
106
|
+
readonly unitID: string;
|
|
107
|
+
readonly memberID: string;
|
|
108
|
+
readonly eventID: string;
|
|
109
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
interface EndpointMember {
|
|
113
|
+
readonly memberID: string;
|
|
114
|
+
readonly userID: string;
|
|
115
|
+
name: string;
|
|
116
|
+
avatar?: string;
|
|
117
|
+
}
|
|
118
|
+
interface BaseEndpointMiddlewareContext {
|
|
119
|
+
readonly session: EndpointSession;
|
|
120
|
+
}
|
|
121
|
+
interface ConnectEndpointMiddlewareContext extends BaseEndpointMiddlewareContext {
|
|
122
|
+
readonly connection: NodeTransportConnection;
|
|
123
|
+
readonly member: EndpointMember;
|
|
124
|
+
}
|
|
125
|
+
interface JoinUnitEndpointMiddlewareContext extends BaseEndpointMiddlewareContext {
|
|
126
|
+
readonly unitID: string;
|
|
127
|
+
}
|
|
128
|
+
interface ReceivePresenceEndpointMiddlewareContext extends BaseEndpointMiddlewareContext {
|
|
129
|
+
readonly unitID: string;
|
|
130
|
+
readonly payload: unknown;
|
|
131
|
+
readonly customData: CustomData;
|
|
132
|
+
}
|
|
133
|
+
interface SendPresenceEndpointMiddlewareContext extends ReceivePresenceEndpointMiddlewareContext {
|
|
134
|
+
readonly targetMemberID: string;
|
|
135
|
+
}
|
|
136
|
+
type EndpointMiddlewareAction = "connect" | "joinUnit" | "receivePresence" | "sendPresence";
|
|
137
|
+
type EndpointMiddlewareNext = () => Promise<void>;
|
|
138
|
+
type EndpointMiddleware<TContext> = (ctx: TContext, next: EndpointMiddlewareNext) => void | Promise<void>;
|
|
139
|
+
interface EndpointMiddlewareMap {
|
|
140
|
+
readonly connect: EndpointMiddleware<ConnectEndpointMiddlewareContext>;
|
|
141
|
+
readonly joinUnit: EndpointMiddleware<JoinUnitEndpointMiddlewareContext>;
|
|
142
|
+
readonly receivePresence: EndpointMiddleware<ReceivePresenceEndpointMiddlewareContext>;
|
|
143
|
+
readonly sendPresence: EndpointMiddleware<SendPresenceEndpointMiddlewareContext>;
|
|
144
|
+
}
|
|
145
|
+
interface MemberLeftUnitEvent {
|
|
146
|
+
readonly session: EndpointSession;
|
|
147
|
+
readonly unitID: string;
|
|
148
|
+
readonly reason: "explicit" | "disconnect" | "endpoint-dispose";
|
|
149
|
+
}
|
|
150
|
+
interface EndpointEventMap {
|
|
151
|
+
readonly memberLeftUnit: MemberLeftUnitEvent;
|
|
152
|
+
}
|
|
153
|
+
interface UniverCollabEndpointOptions {
|
|
154
|
+
readonly ticketStore?: ISessionTicketStore;
|
|
155
|
+
readonly sessionTicketTtlMs?: number;
|
|
156
|
+
readonly protocolBasePath?: string;
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { type BaseEndpointMiddlewareContext, type ConnectEndpointMiddlewareContext, type EndpointEventMap, type EndpointMember, type EndpointMiddleware, type EndpointMiddlewareAction, type EndpointMiddlewareMap, type EndpointMiddlewareNext, type EndpointSession, type EndpointSessionBinding, type ISessionTicketStore, type ITrustedUnitRoomHost, type JoinUnitEndpointMiddlewareContext, type MemberLeftUnitEvent, MemorySessionTicketStore, type ReceivePresenceEndpointMiddlewareContext, type SendPresenceEndpointMiddlewareContext, type SessionTicketRecord, UniverCollabEndpoint, type UniverCollabEndpointOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const _0x4f5406=_0x26c0;(function(_0x510367,_0x2e929c){const _0xb68c15=_0x26c0,_0x28d22b=_0x510367();while(!![]){try{const _0x22a09e=-parseInt(_0xb68c15(0x71))/0x1+-parseInt(_0xb68c15(0x77))/0x2*(-parseInt(_0xb68c15(0x75))/0x3)+parseInt(_0xb68c15(0x79))/0x4+parseInt(_0xb68c15(0x6f))/0x5+parseInt(_0xb68c15(0x76))/0x6*(-parseInt(_0xb68c15(0x74))/0x7)+-parseInt(_0xb68c15(0x73))/0x8*(-parseInt(_0xb68c15(0x72))/0x9)+parseInt(_0xb68c15(0x78))/0xa;if(_0x22a09e===_0x2e929c)break;else _0x28d22b['push'](_0x28d22b['shift']());}catch(_0xd7f2ac){_0x28d22b['push'](_0x28d22b['shift']());}}}(_0x551c,0xf3442));function _0x551c(){const _0xb28bda=['1557OpAAGv','30736enuuZL','7mxNklC','3400602sdaCKa','8892636RjYUFA','2pMdhOB','10386240dogcSz','3023548xgrTSp','MemorySessionTicketStore','2713365TlJCIJ','UniverCollabEndpoint','1656860jaSxVr'];_0x551c=function(){return _0xb28bda;};return _0x551c();}function _0x26c0(_0x1eb160,_0x98ad2d){_0x1eb160=_0x1eb160-0x6f;const _0x551cd5=_0x551c();let _0x26c0b5=_0x551cd5[_0x1eb160];return _0x26c0b5;}import _0x4927eb from'./index.cjs';const facadeExport0=_0x4927eb[_0x4f5406(0x7a)],facadeExport1=_0x4927eb[_0x4f5406(0x70)];export{facadeExport0 as MemorySessionTicketStore,facadeExport1 as UniverCollabEndpoint};
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@univerjs-pro/collaboration-endpoint",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0-beta.2",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@univerjs-pro/collaboration-service": "1.0.0-beta.2",
|
|
20
|
+
"@univerjs-pro/collaboration-transport-node": "1.0.0-beta.2",
|
|
21
|
+
"@univerjs/protocol": "1.0.0-beta.2"
|
|
22
|
+
}
|
|
23
|
+
}
|