@univerjs-pro/collaboration-transport-node 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 ADDED
@@ -0,0 +1,178 @@
1
+ # @univerjs-pro/collaboration-transport-node
2
+
3
+ English | [简体中文](./README.zh-CN.md)
4
+
5
+ The Node.js HTTP/WebSocket ingress for a Univer Collaboration server assembly.
6
+
7
+ Transport receives requests and WebSocket upgrades forwarded by the host Node HTTP server. Applications integrate authentication, logging, and other ingress concerns through HTTP middleware, then registered protocol Endpoints handle Collaboration requests. Transport is not normally used by itself:
8
+
9
+ ```text
10
+ Node HTTP server
11
+ → Transport
12
+ ├─→ HTTP middleware login authentication, CORS, trace, network logs
13
+ │ → Endpoint HTTP protocol routes; calls Service APIs as needed
14
+ │ → Service middleware / Database
15
+ └─→ Endpoint WebSocket Sessions, JOIN, Presence, ACKs, broadcasts
16
+ → Endpoint middleware realtime-room ACL and Presence controls
17
+ ```
18
+
19
+ ## Server assembly
20
+
21
+ ```bash
22
+ pnpm add \
23
+ @univerjs-pro/collaboration-service \
24
+ @univerjs-pro/collaboration-endpoint \
25
+ @univerjs-pro/collaboration-transport-node \
26
+ @univerjs-pro/collaboration-database-memory
27
+ ```
28
+
29
+ ```ts
30
+ import { createServer } from 'node:http';
31
+ import { MemoryDatabaseAdapter } from '@univerjs-pro/collaboration-database-memory';
32
+ import { UniverCollabEndpoint } from '@univerjs-pro/collaboration-endpoint';
33
+ import { UniverCollabService } from '@univerjs-pro/collaboration-service';
34
+ import { createNodeTransport } from '@univerjs-pro/collaboration-transport-node';
35
+
36
+ const database = new MemoryDatabaseAdapter();
37
+ const collabService = new UniverCollabService({ dbAdapter: database });
38
+ const endpoint = new UniverCollabEndpoint(collabService);
39
+ const transport = createNodeTransport();
40
+
41
+ // use() handles HTTP requests only. Authenticate and attach request-scoped data here.
42
+ transport.use(async (ctx, next) => {
43
+ const user = await authenticate(ctx.incomingMessage);
44
+ if (!user) {
45
+ ctx.response.statusCode = 401;
46
+ ctx.response.end('Authentication required');
47
+ return;
48
+ }
49
+
50
+ // Map the application's stable user primary key at the SDK boundary.
51
+ ctx.userID = user.userId;
52
+ ctx.customData.user = user;
53
+ ctx.customData.traceId = readTraceId(ctx.incomingMessage);
54
+ await next();
55
+ });
56
+
57
+ // useUpgrade() runs once per WebSocket handshake and can reject before upgrade.
58
+ transport.useUpgrade(async (ctx, next) => {
59
+ if (!isAllowedOrigin(ctx.incomingMessage.headers.origin)) {
60
+ ctx.reject(403, 'Origin rejected');
61
+ return;
62
+ }
63
+ await next();
64
+ });
65
+
66
+ endpoint.use('joinUnit', async (ctx, next) => {
67
+ // Transport ctx.userID → ctx.session.userID
68
+ // Transport ctx.customData → ctx.session.customData
69
+ console.log(ctx.session.userID, ctx.session.customData.user, ctx.session.customData.traceId);
70
+ await next();
71
+ });
72
+
73
+ collabService.use('readUnitData', async (ctx, next) => {
74
+ // Transport ctx.userID → Service ctx.userID
75
+ // Transport ctx.customData → Service ctx.customData
76
+ console.log(ctx.userID, ctx.customData.user, ctx.customData.traceId);
77
+ await next();
78
+ });
79
+
80
+ transport.register(endpoint);
81
+
82
+ const server = createServer((request, response) => {
83
+ transport.handleRequest(request, response);
84
+ });
85
+ server.on('upgrade', (request, socket, head) => {
86
+ transport.handleUpgrade(request, socket, head);
87
+ });
88
+ server.listen(3010);
89
+ ```
90
+
91
+ The Memory Adapter keeps this Transport example small; process exit loses all data. See
92
+ [Database Adapters](https://office.univer.ai/collaboration/database-adapters) for production
93
+ persistence choices.
94
+
95
+ Transport does not prescribe cookies, bearer tokens, user tables, or user types. The application validates its own credentials in HTTP middleware and explicitly writes the stable business identity to `ctx.userID`. On authentication failure, end the response without calling `next()`.
96
+
97
+ ## How identity and customData reach Endpoint and Service
98
+
99
+ Transport creates a separate `ctx.customData` for every HTTP request. HTTP middleware may add the current request's user, tenant, trace, or ACL query objects.
100
+
101
+ ```text
102
+ regular protocol HTTP request
103
+ → Transport middleware sets userID/customData
104
+ → Endpoint validates the protocol and calls Service
105
+ → Service middleware reads the same userID/customData
106
+ ```
107
+
108
+ WebSocket identity is inherited from a one-time ticket, not directly from the upgrade URL or client payload:
109
+
110
+ ```text
111
+ userID/customData on the ticket HTTP request
112
+ → Endpoint stores the association server-side and returns an opaque ticket string
113
+ → WebSocket open consumes the association once
114
+ → Endpoint creates Session { userID, memberID, customData }
115
+ → Endpoint middleware reads ctx.session
116
+ ```
117
+
118
+ The ticket string contains neither `userID` nor `customData`. Only the session-ticket HTTP route extends the current Transport context into a WebSocket Session; other HTTP routes do not.
119
+
120
+ Transport network-data lifetimes are:
121
+
122
+ - HTTP `ctx.customData` belongs only to the current request; every later HTTP request runs authentication middleware again.
123
+ - `EndpointSession.customData` comes from the HTTP request that issued the ticket and keeps the same reference for the current WebSocket Session.
124
+ - For regular HTTP protocol routes, Service middleware receives that HTTP request's `ctx.customData`, not Session `customData` automatically.
125
+
126
+ `customData` is only passed in memory. It is not automatically sent to browsers, persisted in the collaboration database, or logged.
127
+
128
+ ## Mounting in a host application
129
+
130
+ Frameworks such as Express can continue handling application product routes while forwarding only Collaboration protocol paths to `handleRequest()`. The underlying HTTP server must also forward the corresponding WebSocket `upgrade`:
131
+
132
+ ```ts
133
+ app.use('/universer-api', (request, response) => {
134
+ // Express rewrites request.url when mounted; Transport needs the full protocol path.
135
+ request.url = request.originalUrl;
136
+ transport.handleRequest(request, response);
137
+ });
138
+
139
+ server.on('upgrade', (request, socket, head) => {
140
+ const url = new URL(request.url ?? '/', 'http://localhost');
141
+ if (url.pathname === '/universer-api/comb/connect') {
142
+ transport.handleUpgrade(request, socket, head);
143
+ }
144
+ });
145
+ ```
146
+
147
+ Express must forward protocol paths before body parsers such as `express.json()` consume the request stream. Fastify, Nest, and other frameworks similarly need to forward the raw Node request, socket, and head.
148
+
149
+ Collaboration, History, Comment, and Worktree Endpoints can be registered on the same Transport and share the same HTTP authentication middleware. If an Endpoint does not match a request, it calls `next()`. If no registered Endpoint handles an HTTP request, Transport returns a plain-text 404; an unhandled WebSocket upgrade also receives HTTP 404 before connection establishment.
150
+
151
+ ## API layers
152
+
153
+ ```ts
154
+ transport.use(httpMiddleware);
155
+ transport.useUpgrade(upgradeMiddleware);
156
+ transport.register(endpoint);
157
+ ```
158
+
159
+ - `use()`: regular HTTP requests only; callbacks directly receive `incomingMessage`, `response`, `userID`, and request-scoped `customData`.
160
+ - `useUpgrade()`: WebSocket handshakes only; read `incomingMessage` or call `reject(status, message)`.
161
+ - `register()`: register and own a protocol Endpoint that handles matching HTTP requests and WebSocket connections.
162
+
163
+ ## Request limits and disposal
164
+
165
+ HTTP bodies and WebSocket messages both default to a 16 MiB limit. `readBody()` and `readJson()` enforce the limit and ensure the request stream is consumed only once.
166
+
167
+ Dispose from the network boundary inward during shutdown:
168
+
169
+ ```ts
170
+ await transport.dispose();
171
+ await collabService.dispose();
172
+ await database.dispose();
173
+ ```
174
+
175
+ Transport closes active connections and disposes Endpoints it owns through `register()`. The registration returned by `register()` can also unregister and dispose that Endpoint early. Endpoint does not dispose Service, and Service does not dispose an externally injected Database Adapter.
176
+
177
+ See the [Collaboration documentation](https://office.univer.ai/collaboration/overview) for the
178
+ complete assembly, middleware, and production requirements.
@@ -0,0 +1,204 @@
1
+ # @univerjs-pro/collaboration-transport-node
2
+
3
+ [English](./README.md) | [简体中文](./README.zh-CN.md)
4
+
5
+ Univer Collaboration 服务端组装中的 Node.js HTTP/WebSocket 入口。
6
+
7
+ Transport 接收宿主 Node HTTP server 转交的 request 和 WebSocket upgrade。应用通过 HTTP
8
+ middleware 接入认证、日志等入口逻辑,再由注册的协议 Endpoint 处理 Collaboration 请求。
9
+ 它通常不单独使用:
10
+
11
+ ```text
12
+ Node HTTP server
13
+ → Transport
14
+ ├─→ HTTP middleware 登录认证、CORS、trace 和网络日志
15
+ │ → Endpoint HTTP 协议路由,必要时调用 Service API
16
+ │ → Service middleware / Database
17
+ └─→ Endpoint WebSocket Session、JOIN、Presence、ACK 和广播
18
+ → Endpoint middleware 实时房间 ACL 和 Presence 控制
19
+ ```
20
+
21
+ ## 服务端组装
22
+
23
+ ```bash
24
+ pnpm add \
25
+ @univerjs-pro/collaboration-service \
26
+ @univerjs-pro/collaboration-endpoint \
27
+ @univerjs-pro/collaboration-transport-node \
28
+ @univerjs-pro/collaboration-database-memory
29
+ ```
30
+
31
+ ```ts
32
+ import { createServer } from 'node:http';
33
+ import { MemoryDatabaseAdapter } from '@univerjs-pro/collaboration-database-memory';
34
+ import { UniverCollabEndpoint } from '@univerjs-pro/collaboration-endpoint';
35
+ import { UniverCollabService } from '@univerjs-pro/collaboration-service';
36
+ import { createNodeTransport } from '@univerjs-pro/collaboration-transport-node';
37
+
38
+ const database = new MemoryDatabaseAdapter();
39
+ const collabService = new UniverCollabService({ dbAdapter: database });
40
+ const endpoint = new UniverCollabEndpoint(collabService);
41
+ const transport = createNodeTransport();
42
+
43
+ // use() 只接收 HTTP request;这里由应用认证并挂载身份和请求级数据。
44
+ transport.use(async (ctx, next) => {
45
+ const user = await authenticate(ctx.incomingMessage);
46
+ if (!user) {
47
+ ctx.response.statusCode = 401;
48
+ ctx.response.end('Authentication required');
49
+ return;
50
+ }
51
+
52
+ // 在进入 Collaboration SDK 时映射应用的稳定用户主键。
53
+ ctx.userID = user.userId;
54
+ ctx.customData.user = user;
55
+ ctx.customData.traceId = readTraceId(ctx.incomingMessage);
56
+ await next();
57
+ });
58
+
59
+ // useUpgrade() 每次 WebSocket handshake 运行一次,可在升级前拒绝。
60
+ transport.useUpgrade(async (ctx, next) => {
61
+ if (!isAllowedOrigin(ctx.incomingMessage.headers.origin)) {
62
+ ctx.reject(403, 'Origin rejected');
63
+ return;
64
+ }
65
+ await next();
66
+ });
67
+
68
+ // WebSocket Session 发送 JOIN 消息、尚未加入 Unit 房间时,
69
+ // 会经过 joinUnit middleware;Session 数据继承自签发 ticket 的 HTTP 请求。
70
+ endpoint.use('joinUnit', async (ctx, next) => {
71
+ // Transport 挂载的 ctx.userID → ctx.session.userID
72
+ // Transport 挂载的 ctx.customData → ctx.session.customData
73
+ console.log(ctx.session.userID, ctx.session.customData.user, ctx.session.customData.traceId);
74
+ await next();
75
+ });
76
+
77
+ // Endpoint 通过 HTTP 读取 Unit、changesets 或 Sheet block,
78
+ // Service 尚未访问 Database Adapter 时,会经过 readUnitData middleware。
79
+ collabService.use('readUnitData', async (ctx, next) => {
80
+ // Transport 挂载的 ctx.userID → Service ctx.userID
81
+ // Transport 挂载的 ctx.customData → Service ctx.customData
82
+ console.log(ctx.userID, ctx.customData.user, ctx.customData.traceId);
83
+ await next();
84
+ });
85
+
86
+ // Endpoint 通过独立入口注册,并由 Transport 管理生命周期。
87
+ transport.register(endpoint);
88
+
89
+ const server = createServer((request, response) => {
90
+ transport.handleRequest(request, response);
91
+ });
92
+ server.on('upgrade', (request, socket, head) => {
93
+ transport.handleUpgrade(request, socket, head);
94
+ });
95
+ server.listen(3010);
96
+ ```
97
+
98
+ 这里使用 Memory Adapter 只是为了让 Transport 示例保持最小;进程退出后数据会丢失。
99
+ 生产环境的持久化选择见文档站的
100
+ [Database Adapter](https://office.univer.ai/zh-CN/collaboration/database-adapters)。
101
+
102
+ Transport 不规定 Cookie、Bearer token、用户表或用户类型。应用在 HTTP
103
+ middleware 中验证自己的凭据,再把稳定业务身份显式写入
104
+ `ctx.userID`。认证失败时直接结束 response,不要继续调用 `next()`。
105
+
106
+ ## 身份与 customData 如何进入 Endpoint 和 Service
107
+
108
+ Transport 为每个 HTTP 请求创建独立的 `ctx.customData`。HTTP middleware 可以在
109
+ 其中放入当前请求需要的用户、tenant、trace 或 ACL 查询对象。
110
+
111
+ ```text
112
+ 普通协议 HTTP 请求
113
+ → Transport middleware 设置 userID/customData
114
+ → Endpoint 验证协议并调用 Service
115
+ → Service middleware 读取同一 userID/customData
116
+ ```
117
+
118
+ WebSocket 身份不直接取自 upgrade URL 或客户端 payload,而是由 Endpoint 通过一次性
119
+ ticket 继承:
120
+
121
+ ```text
122
+ ticket HTTP 请求上的 userID/customData
123
+ → Endpoint 在服务端保存关联并返回 opaque ticket 字符串
124
+ → WebSocket open 一次性消费关联记录
125
+ → Endpoint 创建 Session { userID, memberID, customData }
126
+ → Endpoint middleware 通过 ctx.session 读取
127
+ ```
128
+
129
+ ticket 字符串本身不包含 `userID/customData`。只有 session-ticket HTTP 路由会把
130
+ 当前 Transport context 延长到 WebSocket Session;其他 HTTP 路由不会。
131
+
132
+ Transport 的网络数据生命周期如下:
133
+
134
+ - HTTP `ctx.customData` 只属于当前请求;每个后续 HTTP 请求都会重新运行认证
135
+ middleware。
136
+ - `EndpointSession.customData` 来自签发 ticket 的那次 HTTP 请求,在当前
137
+ WebSocket Session 中保持同一引用。
138
+ - Endpoint 处理普通 HTTP 协议路由时,传给 Service middleware 的是该 HTTP 请求的
139
+ `ctx.customData`,不是自动取 Session `customData`。
140
+
141
+ `customData` 只在内存中传递,不会自动发送给浏览器、写入协同数据库或记录日志。
142
+
143
+ ## 在宿主应用中挂载
144
+
145
+ Express 等框架可以继续处理应用自己的产品路由,只把 Collaboration 协议路径转交给
146
+ `handleRequest()`。底层 HTTP server 还必须转交对应的 WebSocket `upgrade`:
147
+
148
+ ```ts
149
+ app.use('/universer-api', (request, response) => {
150
+ // Express 挂载会改写 request.url,Transport 需要看到完整协议路径。
151
+ request.url = request.originalUrl;
152
+ transport.handleRequest(request, response);
153
+ });
154
+
155
+ server.on('upgrade', (request, socket, head) => {
156
+ const url = new URL(request.url ?? '/', 'http://localhost');
157
+ if (url.pathname === '/universer-api/comb/connect') {
158
+ transport.handleUpgrade(request, socket, head);
159
+ }
160
+ });
161
+ ```
162
+
163
+ Express 集成必须在 `express.json()` 等 body parser 消费请求流之前把协议路径交给
164
+ Transport。Fastify、Nest 等框架同样需要转交原始 Node request、socket 和 head。
165
+
166
+ Collaboration、History、Comment 和 Worktree Endpoint 可以按路由前缀注册在同一个
167
+ Transport 上,共享同一组 HTTP 认证 middleware。Endpoint 没有匹配当前请求时会
168
+ 继续调用 `next()`;所有已注册 Endpoint 都未处理的 HTTP 请求由 Transport 返回纯文本
169
+ 404,未处理的 WebSocket upgrade 也在建立连接前返回 HTTP 404。
170
+
171
+ ## API 分层
172
+
173
+ ```ts
174
+ transport.use(httpMiddleware);
175
+ transport.useUpgrade(upgradeMiddleware);
176
+ transport.register(endpoint);
177
+ ```
178
+
179
+ - `use()`:仅普通 HTTP request;回调直接获得 `incomingMessage`、`response`、
180
+ `userID` 和请求级 `customData`。
181
+ - `useUpgrade()`:仅 WebSocket handshake;可读取 `incomingMessage`,或调用
182
+ `reject(status, message)`。
183
+ - `register()`:注册并管理协议 Endpoint,由 Endpoint 处理匹配的 HTTP 请求和 WebSocket
184
+ 连接。
185
+
186
+ ## 请求限制与资源释放
187
+
188
+ HTTP body 和 WebSocket message 限制默认都是 16 MiB。`readBody()` 和 `readJson()` 会
189
+ 执行大小限制,并保证请求流只消费一次。
190
+
191
+ 停止时从网络入口向持久化层反向释放:
192
+
193
+ ```ts
194
+ await transport.dispose();
195
+ await collabService.dispose();
196
+ await database.dispose();
197
+ ```
198
+
199
+ Transport 会终止活动连接并释放它通过 `register()` 拥有的 Endpoint;`register()` 返回的
200
+ registration 也可提前注销并释放该 Endpoint。Endpoint 不释放 Service,Service 也不释放
201
+ 外部注入的 Database Adapter。
202
+
203
+ 完整服务端装配、middleware 和生产环境要求见
204
+ [协同编辑文档](https://office.univer.ai/zh-CN/collaboration/overview)。
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object['defineProperty'](exports,Symbol['toStringTag'],{'value':'Module'});let e=require('node:crypto'),t=require('node:http'),n=require('ws');async function r(_0x4add8d,_0x3f1d20){let _0x431b5b=async _0x37e2b9=>{let _0xec7a57=_0x4add8d[_0x37e2b9];if(!_0xec7a57)return;let _0x18487e=!0x1,_0x435d60,_0x3da116=()=>{if(_0x18487e)throw Error('Transport\x20middleware\x20called\x20next()\x20more\x20than\x20once');return _0x18487e=!0x0,_0x435d60=_0x431b5b(_0x37e2b9+0x1),_0x435d60;};try{await _0xec7a57(_0x3f1d20,_0x3da116);}catch(_0x3d52f6){if(_0x435d60)try{await _0x435d60;}catch{}throw _0x3d52f6;}_0x435d60&&await _0x435d60;};await _0x431b5b(0x0);}async function i(_0x4f8024,_0x506397){await r(_0x4f8024['flatMap'](_0x352ea5=>_0x352ea5['disposed']?[]:_0x352ea5['kind']==='http'?[_0x352ea5['middleware']]:_0x352ea5['kind']==='endpoint'&&_0x352ea5['endpoint']['handleHttp']?[_0x352ea5['endpoint']['handleHttp']['bind'](_0x352ea5['endpoint'])]:[]),_0x506397);}async function a(_0x31ce76,_0x32f4f0){await r(_0x31ce76['flatMap'](_0xfe8ebc=>{if(_0xfe8ebc['disposed'])return[];if(_0xfe8ebc['kind']==='upgrade'){let _0x29e27a=_0xfe8ebc['middleware'];return[(_0x6aed08,_0x5a1be0)=>_0x29e27a(_0x6aed08,_0x5a1be0)];}return _0xfe8ebc['kind']==='endpoint'&&_0xfe8ebc['endpoint']['handleUpgrade']?[_0xfe8ebc['endpoint']['handleUpgrade']['bind'](_0xfe8ebc['endpoint'])]:[];}),_0x32f4f0);}var o=0x10*0x400*0x400,s=class{['socket'];['customData'];['id']=(0x0,e['randomUUID'])();['_closed']=!0x1;constructor(_0x3398a1,_0x2dc078){this['socket']=_0x3398a1,this['customData']=_0x2dc078;}['send'](_0x365761){return this['_closed']||this['socket']['readyState']!==this['socket']['OPEN']?Promise['reject'](Error('WebSocket\x20connection\x20is\x20not\x20open')):new Promise((_0x2d877b,_0x353469)=>{this['socket']['send'](_0x365761,_0x1aae42=>{_0x1aae42?_0x353469(_0x1aae42):_0x2d877b();});});}['close'](_0x3a14e9,_0xd9596e){this['_closed']||(this['_closed']=!0x0,(this['socket']['readyState']===this['socket']['OPEN']||this['socket']['readyState']===this['socket']['CONNECTING'])&&this['socket']['close'](_0x3a14e9,_0xd9596e));}['terminate'](){this['_closed']=!0x0,this['socket']['readyState']!==this['socket']['CLOSED']&&this['socket']['terminate']();}};function c(_0x510f2f,_0x29b4b2){if(_0x510f2f===void 0x0)return _0x29b4b2;if(!Number['isSafeInteger'](_0x510f2f)||_0x510f2f<=0x0)throw TypeError('Transport\x20byte\x20limits\x20must\x20be\x20positive\x20safe\x20integers');return _0x510f2f;}function l(_0x356200,_0x11e3ea){let _0x491b48;return()=>(_0x491b48??=new Promise((_0x5647ba,_0x336978)=>{let _0x219d2e=_0x356200['headers']['content-length'];if(_0x219d2e!==void 0x0&&Number['isFinite'](Number(_0x219d2e))&&Number(_0x219d2e)>_0x11e3ea){_0x356200['resume'](),_0x336978(new m('REQUEST_BODY_TOO_LARGE','Request\x20body\x20exceeds\x20'+_0x11e3ea+'\x20bytes'));return;}let _0x587020=[],_0x17e53e=0x0,_0x437b17=!0x1,_0x2e3e96=_0x1dc6ae=>{_0x437b17||(_0x437b17=!0x0,_0x336978(_0x1dc6ae));};_0x356200['on']('data',_0x2641dc=>{if(_0x437b17)return;let _0x18c8e7=typeof _0x2641dc=='string'?Buffer['from'](_0x2641dc):_0x2641dc;if(_0x17e53e+=_0x18c8e7['byteLength'],_0x17e53e>_0x11e3ea){_0x356200['resume'](),_0x2e3e96(new m('REQUEST_BODY_TOO_LARGE','Request\x20body\x20exceeds\x20'+_0x11e3ea+'\x20bytes'));return;}_0x587020['push'](_0x18c8e7);}),_0x356200['once']('end',()=>{_0x437b17||(_0x437b17=!0x0,_0x5647ba(Buffer['concat'](_0x587020,_0x17e53e)));}),_0x356200['once']('aborted',()=>{_0x2e3e96(new m('REQUEST_ABORTED','Request\x20was\x20aborted'));}),_0x356200['once']('error',_0x9e2c1f=>{_0x2e3e96(new m('REQUEST_ABORTED','Request\x20stream\x20failed',{'cause':_0x9e2c1f}));});}),_0x491b48);}function u(_0x2c3489){return async()=>{let _0x2b8bbb=await _0x2c3489();try{return JSON['parse'](Buffer['from'](_0x2b8bbb)['toString']('utf8'));}catch(_0x4bb115){throw new m('INVALID_JSON','Request\x20body\x20is\x20not\x20valid\x20JSON',{'cause':_0x4bb115});}};}function d(_0x2dd61c){return Array['isArray'](_0x2dd61c)?Buffer['concat'](_0x2dd61c):_0x2dd61c instanceof ArrayBuffer?Buffer['from'](_0x2dd61c):Buffer['from'](_0x2dd61c['buffer'],_0x2dd61c['byteOffset'],_0x2dd61c['byteLength']);}var f=class{['_entries']=[];['_webSocketServer'];['_connections']=new Set();['_eventQueues']=new Map();['_failedConnections']=new Set();['_connectionClosed']=new Map();['_maxRequestBodyBytes'];['_disposed']=!0x1;constructor(_0x490ee5={}){this['_maxRequestBodyBytes']=c(_0x490ee5['maxRequestBodyBytes'],o);let _0x102083=c(_0x490ee5['maxWebSocketMessageBytes'],this['_maxRequestBodyBytes']);this['_webSocketServer']=new n['WebSocketServer']({'noServer':!0x0,'maxPayload':_0x102083});}['use'](_0x9b3aa6){if(this['_assertRunning'](),typeof _0x9b3aa6!='function')throw TypeError('HTTP\x20Transport\x20middleware\x20must\x20be\x20a\x20function');let _0xe4893f={'kind':'http','middleware':_0x9b3aa6,'disposed':!0x1};return this['_entries']['push'](_0xe4893f),this['_registration'](_0xe4893f);}['useUpgrade'](_0x256f8f){if(this['_assertRunning'](),typeof _0x256f8f!='function')throw TypeError('WebSocket\x20upgrade\x20middleware\x20must\x20be\x20a\x20function');let _0x278ca5={'kind':'upgrade','middleware':_0x256f8f,'disposed':!0x1};return this['_entries']['push'](_0x278ca5),this['_registration'](_0x278ca5);}['register'](_0x398d30){if(this['_assertRunning'](),!_0x398d30||typeof _0x398d30!='object')throw TypeError('Transport\x20Endpoint\x20must\x20be\x20an\x20object');let _0x404a2d={'kind':'endpoint','endpoint':_0x398d30,'disposed':!0x1};return this['_entries']['push'](_0x404a2d),this['_registration'](_0x404a2d);}['handleRequest'](_0x5a348a,_0x532bb5){if(this['_disposed']){this['_writeHttpError'](_0x532bb5,new m('TRANSPORT_DISPOSED','Transport\x20is\x20disposed'));return;}let _0xd0a2df=l(_0x5a348a,this['_maxRequestBodyBytes']),_0x46063e={'incomingMessage':_0x5a348a,'response':_0x532bb5,'customData':Object['create'](null),'readBody':_0xd0a2df,'readJson':u(_0xd0a2df)};this['_dispatchHttp'](_0x46063e)['then'](()=>{this['_writeHttpNotFound'](_0x532bb5);})['catch'](_0x28e61c=>{this['_writeHttpError'](_0x532bb5,_0x28e61c);});}['handleUpgrade'](_0x1a61fe,_0x3e2e22,_0x40b499){if(this['_disposed']){_0x3e2e22['destroy']();return;}this['_handleUpgrade'](_0x1a61fe,_0x3e2e22,_0x40b499);}async['dispose'](){if(this['_disposed'])return;this['_disposed']=!0x0;let _0x1ff2ff=[...this['_connectionClosed']['values']()]['map'](_0x23e27d=>_0x23e27d['promise']);for(let _0x4978a6 of this['_connections'])_0x4978a6['terminate']();await Promise['allSettled'](_0x1ff2ff),await Promise['allSettled']([...this['_eventQueues']['values']()]);let _0x3e9c01=[...this['_entries']]['reverse']();for(let _0x6215f of _0x3e9c01)_0x6215f['disposed']||(_0x6215f['disposed']=!0x0,_0x6215f['kind']==='endpoint'&&await _0x6215f['endpoint']['dispose']?.());this['_entries']['length']=0x0;}async['_handleUpgrade'](_0xa912a8,_0x100feb,_0x316625){let _0x249584,_0x1dd2ef=Object['create'](null),_0x39729f=_0x3e68b1=>{if(_0x249584)throw Error('WebSocket\x20upgrade\x20was\x20already\x20accepted\x20or\x20rejected');_0x249584=_0x3e68b1;},_0x169bee={'incomingMessage':_0xa912a8,'customData':_0x1dd2ef,'reject':(_0x5c1301=0x193,_0x48b40b)=>{if(!Number['isSafeInteger'](_0x5c1301)||_0x5c1301<0x190||_0x5c1301>0x257)throw TypeError('WebSocket\x20rejection\x20status\x20must\x20be\x20an\x20integer\x20from\x20400\x20to\x20599');_0x39729f({'kind':'reject','statusCode':_0x5c1301,'message':_0x48b40b??t['STATUS_CODES'][_0x5c1301]??'Upgrade\x20rejected'});},'accept':_0x3751e7=>{if(!_0x3751e7||typeof _0x3751e7!='object')throw TypeError('WebSocket\x20handler\x20must\x20be\x20an\x20object');_0x39729f({'kind':'accept','handler':_0x3751e7});}};try{if(await a([...this['_entries']],_0x169bee),this['_disposed']||_0x100feb['destroyed']){_0x100feb['destroy']();return;}if(!_0x249584){this['_writeUpgradeResponse'](_0x100feb,0x194,'Not\x20Found');return;}if(_0x249584['kind']==='reject'){this['_writeUpgradeResponse'](_0x100feb,_0x249584['statusCode'],_0x249584['message']);return;}let _0x4f8378=_0x249584['handler'];this['_webSocketServer']['handleUpgrade'](_0xa912a8,_0x100feb,_0x316625,_0x10ff1a=>{if(this['_disposed']){_0x10ff1a['terminate']();return;}this['_acceptWebSocket'](_0x10ff1a,_0x1dd2ef,_0x4f8378);});}catch{this['_writeUpgradeResponse'](_0x100feb,0x1f4,'Internal\x20Server\x20Error');}}['_acceptWebSocket'](_0x5892e1,_0x3ea6fe,_0x3ed619){let _0x16bcb7=new s(_0x5892e1,_0x3ea6fe);this['_connections']['add'](_0x16bcb7),this['_eventQueues']['set'](_0x16bcb7,Promise['resolve']());let _0x5abcae,_0xc490fb=new Promise(_0x30ba2f=>{_0x5abcae=_0x30ba2f;});this['_connectionClosed']['set'](_0x16bcb7,{'promise':_0xc490fb,'resolve':_0x5abcae}),_0x5892e1['on']('message',(_0x3f4dba,_0x50d2e3)=>{let _0x37e5da=d(_0x3f4dba),_0x4e6630=_0x50d2e3?new Uint8Array(_0x37e5da):_0x37e5da['toString']('utf8'),_0x441ea7={'connection':_0x16bcb7,'data':_0x4e6630,'customData':Object['create'](null)};this['_enqueue'](_0x16bcb7,()=>_0x3ed619['message']?.(_0x441ea7));}),_0x5892e1['once']('close',(_0x414d02,_0x27545c)=>{let _0x487b3c={'connection':_0x16bcb7,'code':_0x414d02,'reason':_0x27545c['toString']('utf8')};this['_enqueue'](_0x16bcb7,()=>_0x3ed619['close']?.(_0x487b3c),!0x0);}),_0x5892e1['on']('error',()=>{});let _0xe9855={'connection':_0x16bcb7};this['_enqueue'](_0x16bcb7,()=>_0x3ed619['open']?.(_0xe9855));}['_enqueue'](_0x17c8f9,_0x3ec1c3,_0x1120c4=!0x1){let _0x32e455=(this['_eventQueues']['get'](_0x17c8f9)??Promise['resolve']())['catch'](()=>void 0x0)['then'](async()=>{this['_failedConnections']['has'](_0x17c8f9)&&!_0x1120c4||await _0x3ec1c3();})['catch'](()=>{this['_failedConnections']['add'](_0x17c8f9),_0x17c8f9['close'](0x3f3,'WebSocket\x20handler\x20failure');})['finally'](()=>{_0x1120c4&&(this['_connections']['delete'](_0x17c8f9),this['_eventQueues']['delete'](_0x17c8f9),this['_failedConnections']['delete'](_0x17c8f9),this['_connectionClosed']['get'](_0x17c8f9)?.['resolve'](),this['_connectionClosed']['delete'](_0x17c8f9));});this['_eventQueues']['set'](_0x17c8f9,_0x32e455);}async['_dispatchHttp'](_0x111716){if(this['_disposed'])throw new m('TRANSPORT_DISPOSED','Transport\x20is\x20disposed');await i([...this['_entries']],_0x111716);}['_writeHttpError'](_0xb121bc,_0x27ccfa){if(_0xb121bc['writableEnded']||_0xb121bc['destroyed'])return;let _0x5591f7=_0x27ccfa instanceof m?_0x27ccfa['code']==='REQUEST_BODY_TOO_LARGE'?0x19d:_0x27ccfa['code']==='INVALID_JSON'||_0x27ccfa['code']==='REQUEST_ABORTED'?0x190:0x1f7:0x1f4,_0x5aa0de=_0x5591f7===0x19d?'Payload\x20Too\x20Large':_0x5591f7===0x190?'Bad\x20Request':_0x5591f7===0x1f7?'Service\x20Unavailable':'Internal\x20Server\x20Error';_0xb121bc['headersSent']?_0xb121bc['destroy'](_0x27ccfa instanceof Error?_0x27ccfa:void 0x0):(_0xb121bc['statusCode']=_0x5591f7,_0xb121bc['setHeader']('content-type','text/plain;\x20charset=utf-8'),_0xb121bc['end'](_0x5aa0de));}['_writeHttpNotFound'](_0x4e8d5a){_0x4e8d5a['headersSent']||_0x4e8d5a['writableEnded']||_0x4e8d5a['destroyed']||(_0x4e8d5a['statusCode']=0x194,_0x4e8d5a['setHeader']('content-type','text/plain;\x20charset=utf-8'),_0x4e8d5a['end']('Not\x20Found'));}['_writeUpgradeResponse'](_0x201a1f,_0x82f6ca,_0xd14815){if(_0x201a1f['destroyed'])return;let _0x3cd54b=t['STATUS_CODES'][_0x82f6ca]??'Error',_0x39ed0b=Buffer['from'](_0xd14815,'utf8');_0x201a1f['end']('HTTP/1.1\x20'+_0x82f6ca+'\x20'+_0x3cd54b+'\x0d\x0aConnection:\x20close\x0d\x0aContent-Type:\x20text/plain;\x20charset=utf-8\x0d\x0aContent-Length:\x20'+_0x39ed0b['byteLength']+'\x0d\x0a\x0d\x0a'+_0x39ed0b['toString']('utf8'));}['_registration'](_0x2a9541){return{'dispose':async()=>{_0x2a9541['disposed']||(_0x2a9541['disposed']=!0x0,_0x2a9541['kind']==='endpoint'&&await _0x2a9541['endpoint']['dispose']?.());}};}['_assertRunning'](){if(this['_disposed'])throw new m('TRANSPORT_DISPOSED','Transport\x20is\x20disposed');}};function p(_0x3a09ba){return new f(_0x3a09ba);}var m=class extends Error{['code'];constructor(_0x229933,_0x1f76db,_0x2789bd){super(_0x1f76db,_0x2789bd),this['code']=_0x229933,this['name']='NodeTransportError';}};exports['NodeTransportError']=m,exports['createNodeTransport']=p;
@@ -0,0 +1,78 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { Duplex } from "node:stream";
3
+
4
+ //#region src/node-transport.d.ts
5
+ declare function createNodeTransport(options?: NodeTransportOptions): INodeTransport;
6
+ //#endregion
7
+ //#region src/transport.d.ts
8
+ type TransportCustomData = Record<string, unknown>;
9
+ interface TransportDisposable {
10
+ dispose(): void | Promise<void>;
11
+ }
12
+ type NodeTransportErrorCode = "INVALID_JSON" | "REQUEST_BODY_TOO_LARGE" | "REQUEST_ABORTED" | "TRANSPORT_DISPOSED";
13
+ declare class NodeTransportError extends Error {
14
+ readonly code: NodeTransportErrorCode;
15
+ constructor(code: NodeTransportErrorCode, message: string, options?: ErrorOptions);
16
+ }
17
+ interface NodeTransportConnection {
18
+ readonly id: string;
19
+ readonly customData: TransportCustomData;
20
+ send(data: string | Uint8Array): void | Promise<void>;
21
+ close(code?: number, reason?: string): void;
22
+ }
23
+ interface NodeHttpTransportContext {
24
+ readonly incomingMessage: IncomingMessage;
25
+ readonly response: ServerResponse;
26
+ readonly customData: TransportCustomData;
27
+ userID?: string;
28
+ readBody(): Promise<Uint8Array>;
29
+ readJson<T = unknown>(): Promise<T>;
30
+ }
31
+ interface NodeWebSocketUpgradeContext {
32
+ readonly incomingMessage: IncomingMessage;
33
+ readonly customData: TransportCustomData;
34
+ reject(statusCode?: number, message?: string): void;
35
+ }
36
+ interface NodeWebSocketEndpointContext extends NodeWebSocketUpgradeContext {
37
+ accept(handler: NodeWebSocketHandler): void;
38
+ }
39
+ interface NodeWebSocketOpenContext {
40
+ readonly connection: NodeTransportConnection;
41
+ }
42
+ interface NodeWebSocketMessageContext {
43
+ readonly connection: NodeTransportConnection;
44
+ readonly data: string | Uint8Array;
45
+ readonly customData: TransportCustomData;
46
+ }
47
+ interface NodeWebSocketCloseContext {
48
+ readonly connection: NodeTransportConnection;
49
+ readonly code?: number;
50
+ readonly reason?: string;
51
+ }
52
+ interface NodeWebSocketHandler {
53
+ open?(ctx: NodeWebSocketOpenContext): void | Promise<void>;
54
+ message?(ctx: NodeWebSocketMessageContext): void | Promise<void>;
55
+ close?(ctx: NodeWebSocketCloseContext): void | Promise<void>;
56
+ }
57
+ type NodeTransportNext = () => Promise<void>;
58
+ type NodeHttpTransportMiddleware = (ctx: NodeHttpTransportContext, next: NodeTransportNext) => void | Promise<void>;
59
+ type NodeWebSocketUpgradeMiddleware = (ctx: NodeWebSocketUpgradeContext, next: NodeTransportNext) => void | Promise<void>;
60
+ interface NodeTransportEndpoint {
61
+ handleHttp?(ctx: NodeHttpTransportContext, next: NodeTransportNext): void | Promise<void>;
62
+ handleUpgrade?(ctx: NodeWebSocketEndpointContext, next: NodeTransportNext): void | Promise<void>;
63
+ dispose?(): void | Promise<void>;
64
+ }
65
+ interface NodeTransportOptions {
66
+ readonly maxRequestBodyBytes?: number;
67
+ readonly maxWebSocketMessageBytes?: number;
68
+ }
69
+ interface INodeTransport {
70
+ use(middleware: NodeHttpTransportMiddleware): TransportDisposable;
71
+ useUpgrade(middleware: NodeWebSocketUpgradeMiddleware): TransportDisposable;
72
+ register(endpoint: NodeTransportEndpoint): TransportDisposable;
73
+ handleRequest(request: IncomingMessage, response: ServerResponse): void;
74
+ handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
75
+ dispose(): Promise<void>;
76
+ }
77
+ //#endregion
78
+ export { type INodeTransport, type NodeHttpTransportContext, type NodeHttpTransportMiddleware, type NodeTransportConnection, type NodeTransportEndpoint, NodeTransportError, type NodeTransportErrorCode, type NodeTransportNext, type NodeTransportOptions, type NodeWebSocketCloseContext, type NodeWebSocketEndpointContext, type NodeWebSocketHandler, type NodeWebSocketMessageContext, type NodeWebSocketOpenContext, type NodeWebSocketUpgradeContext, type NodeWebSocketUpgradeMiddleware, type TransportCustomData, type TransportDisposable, createNodeTransport };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ const _0x30e403=_0x20a1;(function(_0x25841d,_0x537282){const _0x5891ff=_0x20a1,_0x35878d=_0x25841d();while(!![]){try{const _0x1ee9bd=parseInt(_0x5891ff(0xbd))/0x1*(-parseInt(_0x5891ff(0xb7))/0x2)+parseInt(_0x5891ff(0xb6))/0x3*(-parseInt(_0x5891ff(0xbc))/0x4)+-parseInt(_0x5891ff(0xb3))/0x5+parseInt(_0x5891ff(0xbe))/0x6*(-parseInt(_0x5891ff(0xba))/0x7)+-parseInt(_0x5891ff(0xb5))/0x8*(parseInt(_0x5891ff(0xb4))/0x9)+parseInt(_0x5891ff(0xbb))/0xa*(-parseInt(_0x5891ff(0xb2))/0xb)+parseInt(_0x5891ff(0xb1))/0xc*(parseInt(_0x5891ff(0xb9))/0xd);if(_0x1ee9bd===_0x537282)break;else _0x35878d['push'](_0x35878d['shift']());}catch(_0xe6e2b){_0x35878d['push'](_0x35878d['shift']());}}}(_0x5664,0xe5ee7));import _0x2513cb from'./index.cjs';const facadeExport0=_0x2513cb['NodeTransportError'],facadeExport1=_0x2513cb[_0x30e403(0xb8)];function _0x20a1(_0x31ae8b,_0x148019){_0x31ae8b=_0x31ae8b-0xb1;const _0x56647=_0x5664();let _0x20a12f=_0x56647[_0x31ae8b];return _0x20a12f;}export{facadeExport0 as NodeTransportError,facadeExport1 as createNodeTransport};function _0x5664(){const _0x56e034=['10ovIEwl','18HCyYWy','12OQRLUZ','22kNSzxs','8853035bKykdX','14639472zLKoUo','8qmgvAe','3DFHiBz','138614lVlIIN','createNodeTransport','88085491ixnuCR','2339827kejjnf','2854990UPXMYL','679768wlDTjP'];_0x5664=function(){return _0x56e034;};return _0x5664();}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@univerjs-pro/collaboration-transport-node",
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
+ "ws": "^8.21.1"
20
+ }
21
+ }