@cloudpss/message-stream 0.4.20
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 +26 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +49 -0
- package/dist/plugin.js +133 -0
- package/dist/plugin.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +37 -0
- package/src/index.ts +85 -0
- package/src/plugin.ts +163 -0
- package/tests/e2e.js +24 -0
- package/tests/tsconfig.json +9 -0
- package/tsconfig.json +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @cloudpss/message-stream
|
|
2
|
+
|
|
3
|
+
Message stream client for cloudpss APIs.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { HttpClient } from '@cloudpss/http-client';
|
|
9
|
+
import { MessageStreamPlugin } from '@cloudpss/message-stream/plugin';
|
|
10
|
+
import type { Stream } from '@cloudpss/message-stream';
|
|
11
|
+
import { interval } from 'rxjs';
|
|
12
|
+
import { map, take } from 'rxjs/operators';
|
|
13
|
+
|
|
14
|
+
const http = new HttpClient({
|
|
15
|
+
/* config */
|
|
16
|
+
}).use(MessageStreamPlugin());
|
|
17
|
+
const stream = await http.stream.create({ type: 'object', comment: 'test', durability: 0 });
|
|
18
|
+
await http.stream.write(
|
|
19
|
+
stream.token,
|
|
20
|
+
stream.type,
|
|
21
|
+
interval(100)
|
|
22
|
+
.pipe(take(10))
|
|
23
|
+
.pipe(map((i) => ({ i }))),
|
|
24
|
+
);
|
|
25
|
+
await http.stream.freeze(stream.token);
|
|
26
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Opaque } from 'type-fest';
|
|
2
|
+
/** UUID */
|
|
3
|
+
type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
|
4
|
+
/**
|
|
5
|
+
* 标识流的读权限
|
|
6
|
+
*/
|
|
7
|
+
export type StreamId = Opaque<UUID, 'StreamId'>;
|
|
8
|
+
/**
|
|
9
|
+
* 标识流的写权限
|
|
10
|
+
*/
|
|
11
|
+
export type StreamToken = Opaque<UUID, 'StreamToken'>;
|
|
12
|
+
export declare const StreamType: readonly ["object", "binary"];
|
|
13
|
+
/**
|
|
14
|
+
* 流中消息的类型
|
|
15
|
+
* - 'object',在 StreamMessage 中使用 ubjson 存储
|
|
16
|
+
* - 'binary',在 StreamMessage 中原样存储
|
|
17
|
+
*/
|
|
18
|
+
export type StreamType = (typeof StreamType)[number];
|
|
19
|
+
/**
|
|
20
|
+
* 流的状态
|
|
21
|
+
*/
|
|
22
|
+
export type StreamState = 'inactive' | 'active' | 'frozen';
|
|
23
|
+
/** 记录调用 `open` 操作的控制器,对应的 `close` 只能由相同的控制器调用 */
|
|
24
|
+
export type StreamWriteHandler = Opaque<string, 'StreamWriteHandler'>;
|
|
25
|
+
/**
|
|
26
|
+
* 消息流,需要使用 {@link StreamToken} 查询
|
|
27
|
+
*/
|
|
28
|
+
export interface Stream {
|
|
29
|
+
/** id */
|
|
30
|
+
readonly id: StreamId;
|
|
31
|
+
/** token */
|
|
32
|
+
readonly token: StreamToken;
|
|
33
|
+
/** 存储流中消息的类型 */
|
|
34
|
+
readonly type: StreamType;
|
|
35
|
+
/** 可选的注释 */
|
|
36
|
+
readonly comment: string;
|
|
37
|
+
/** 流的状态 */
|
|
38
|
+
state: StreamState;
|
|
39
|
+
/** 打开流的控制器,在 `active` 状态下此项非空 */
|
|
40
|
+
handler: StreamWriteHandler | null;
|
|
41
|
+
/** 创建时间 */
|
|
42
|
+
readonly createdAt: Date;
|
|
43
|
+
/** 最后写入时间 */
|
|
44
|
+
lastWrittenAt?: Date;
|
|
45
|
+
/** 最后读取时间 */
|
|
46
|
+
lastReadAt?: Date;
|
|
47
|
+
/** 流的持久性 */
|
|
48
|
+
readonly durability: number;
|
|
49
|
+
}
|
|
50
|
+
/** 消息流信息,使用 {@link StreamId} 查询 */
|
|
51
|
+
export type StreamInfo = Omit<Stream, 'token' | 'handler'>;
|
|
52
|
+
/** 创建流需要的数据 */
|
|
53
|
+
export type StreamArgs = Pick<Stream, 'type'> & Partial<Pick<Stream, 'comment' | 'durability'>>;
|
|
54
|
+
/** 消息 ID */
|
|
55
|
+
export type MessageId = Opaque<UUID, 'MessageId'>;
|
|
56
|
+
/** 原始消息 */
|
|
57
|
+
export interface RawMessage {
|
|
58
|
+
/** 消息 ID */
|
|
59
|
+
readonly id: MessageId;
|
|
60
|
+
/** 消息创建时间 */
|
|
61
|
+
readonly timestamp: Date;
|
|
62
|
+
/** 序列化的消息体 */
|
|
63
|
+
readonly data: Uint8Array;
|
|
64
|
+
}
|
|
65
|
+
/** 消息数据 */
|
|
66
|
+
interface MessageData extends Record<StreamType, unknown> {
|
|
67
|
+
/** 消息数据 */
|
|
68
|
+
object: unknown;
|
|
69
|
+
/** 消息数据 */
|
|
70
|
+
binary: Uint8Array;
|
|
71
|
+
}
|
|
72
|
+
/** 消息 */
|
|
73
|
+
export interface Message<T extends StreamType = StreamType> extends Omit<RawMessage, 'data'> {
|
|
74
|
+
/** 消息数据 */
|
|
75
|
+
readonly data: MessageData[T];
|
|
76
|
+
}
|
|
77
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAU,CAAC"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HttpClient, HttpClientPlugin } from '@cloudpss/http-client';
|
|
2
|
+
import type { Message, Stream, StreamArgs, StreamId, StreamInfo, StreamToken, StreamType } from './index.js';
|
|
3
|
+
import { Observable, type ObservableInput } from 'rxjs';
|
|
4
|
+
/** 消息流客户端 */
|
|
5
|
+
declare class MessageStreamClient {
|
|
6
|
+
readonly client: HttpClient;
|
|
7
|
+
constructor(client: HttpClient);
|
|
8
|
+
/** 解析响应 */
|
|
9
|
+
private parse;
|
|
10
|
+
/** 创建流 */
|
|
11
|
+
create<T extends StreamType>(args: StreamArgs & {
|
|
12
|
+
type: T;
|
|
13
|
+
}): Promise<Stream & {
|
|
14
|
+
type: T;
|
|
15
|
+
}>;
|
|
16
|
+
/** 批量创建流 */
|
|
17
|
+
create<T extends StreamType>(args: ReadonlyArray<StreamArgs & {
|
|
18
|
+
type: T;
|
|
19
|
+
}>): Promise<Array<Stream & {
|
|
20
|
+
type: T;
|
|
21
|
+
}>>;
|
|
22
|
+
/** 冻结流 */
|
|
23
|
+
freeze(token: StreamToken): Promise<boolean>;
|
|
24
|
+
/** 删除流 */
|
|
25
|
+
delete(token: StreamToken): Promise<void>;
|
|
26
|
+
/** 获取流信息 */
|
|
27
|
+
info(id: StreamId): Promise<StreamInfo>;
|
|
28
|
+
/** 获取流信息 */
|
|
29
|
+
get(token: StreamToken): Promise<Stream>;
|
|
30
|
+
/** 读取消息 */
|
|
31
|
+
read<T extends StreamType>(stream: {
|
|
32
|
+
id: StreamId;
|
|
33
|
+
type: T;
|
|
34
|
+
}, options?: {
|
|
35
|
+
from?: Date;
|
|
36
|
+
}): Observable<Message<T>>;
|
|
37
|
+
/** 写入消息 */
|
|
38
|
+
write<T extends StreamType>(stream: {
|
|
39
|
+
token: StreamToken;
|
|
40
|
+
type: T;
|
|
41
|
+
}, data: ObservableInput<Message<T>['data']>): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 添加消息流插件,使得可以通过 `client.stream` 访问消息流相关接口
|
|
45
|
+
*/
|
|
46
|
+
export declare function MessageStreamPlugin(): HttpClientPlugin<{
|
|
47
|
+
readonly stream: MessageStreamClient;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { WebSocket, connected, disconnected } from '@cloudpss/ws';
|
|
2
|
+
import { Observable, from } from 'rxjs';
|
|
3
|
+
import { decode, encode } from '@cloudpss/ubjson';
|
|
4
|
+
/** 消息流客户端 */
|
|
5
|
+
class MessageStreamClient {
|
|
6
|
+
constructor(client) {
|
|
7
|
+
this.client = client;
|
|
8
|
+
}
|
|
9
|
+
/** 解析响应 */
|
|
10
|
+
parse(data) {
|
|
11
|
+
data.createdAt = new Date(data.createdAt);
|
|
12
|
+
data.lastReadAt = data.lastReadAt ? new Date(data.lastReadAt) : undefined;
|
|
13
|
+
data.lastWrittenAt = data.lastWrittenAt ? new Date(data.lastWrittenAt) : undefined;
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
/** 创建流 */
|
|
17
|
+
async create(args) {
|
|
18
|
+
if (Array.isArray(args)) {
|
|
19
|
+
const list = args;
|
|
20
|
+
if (list.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
const result = await this.client.restful('/streams/bulk', args);
|
|
23
|
+
return result.data.map((item) => this.parse(item));
|
|
24
|
+
}
|
|
25
|
+
const result = await this.client.restful('/streams', args);
|
|
26
|
+
return this.parse(result.data);
|
|
27
|
+
}
|
|
28
|
+
/** 冻结流 */
|
|
29
|
+
async freeze(token) {
|
|
30
|
+
const result = await this.client.restful(`/streams/token/${token}/freeze`, undefined, { method: 'PUT' });
|
|
31
|
+
if (result.response.status === 204)
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** 删除流 */
|
|
36
|
+
async delete(token) {
|
|
37
|
+
await this.client.restful(`/streams/token/${token}`, undefined, { method: 'DELETE' });
|
|
38
|
+
}
|
|
39
|
+
/** 获取流信息 */
|
|
40
|
+
async info(id) {
|
|
41
|
+
const result = await this.client.restful(`/streams/id/${id}/info`);
|
|
42
|
+
return this.parse(result.data);
|
|
43
|
+
}
|
|
44
|
+
/** 获取流信息 */
|
|
45
|
+
async get(token) {
|
|
46
|
+
const result = await this.client.restful(`/streams/token/${token}/info`);
|
|
47
|
+
return this.parse(result.data);
|
|
48
|
+
}
|
|
49
|
+
/** 读取消息 */
|
|
50
|
+
read(stream, options) {
|
|
51
|
+
const { id, type } = stream;
|
|
52
|
+
const from = options?.from && Number(options.from) ? `?from=${options.from.toISOString()}` : '';
|
|
53
|
+
const url = this.client.config.apiUrl.replace(/^http/, 'ws') + `/streams/id/${id}${from}`;
|
|
54
|
+
return new Observable((subscriber) => {
|
|
55
|
+
const ws = new WebSocket(url);
|
|
56
|
+
if (ws.binaryType === 'blob')
|
|
57
|
+
ws.binaryType = 'arraybuffer';
|
|
58
|
+
ws.addEventListener('message', (event) => {
|
|
59
|
+
if (typeof event.data === 'string' || !event.data)
|
|
60
|
+
return;
|
|
61
|
+
const message = decode(event.data);
|
|
62
|
+
message.timestamp = new Date(message.timestamp);
|
|
63
|
+
if (type === 'object') {
|
|
64
|
+
message.data = decode(message.data);
|
|
65
|
+
}
|
|
66
|
+
subscriber.next(message);
|
|
67
|
+
});
|
|
68
|
+
ws.addEventListener('close', (ev) => {
|
|
69
|
+
if (ev.code !== 1000) {
|
|
70
|
+
subscriber.error(new Error(`WebSocket closed with code ${ev.code}: ${ev.reason}`));
|
|
71
|
+
}
|
|
72
|
+
else if (ev.reason) {
|
|
73
|
+
subscriber.error(new Error(ev.reason));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
subscriber.complete();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
ws.addEventListener('error', (ev) => {
|
|
80
|
+
const error = ev.error ?? new Error(`WebSocket error`);
|
|
81
|
+
subscriber.error(error);
|
|
82
|
+
});
|
|
83
|
+
return () => {
|
|
84
|
+
if (ws.readyState === ws.OPEN)
|
|
85
|
+
ws.close(1000);
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** 写入消息 */
|
|
90
|
+
async write(stream, data) {
|
|
91
|
+
const { token, type } = stream;
|
|
92
|
+
const url = this.client.config.apiUrl.replace(/^http/, 'ws') + `/streams/token/${token}`;
|
|
93
|
+
const ws = new WebSocket(url);
|
|
94
|
+
await connected(ws);
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const sub = from(data).subscribe({
|
|
97
|
+
next: (data) => {
|
|
98
|
+
const bin = type === 'binary' ? data : encode(data);
|
|
99
|
+
if (!ArrayBuffer.isView(bin)) {
|
|
100
|
+
reject(new Error(`Invalid data type: ${typeof data}`));
|
|
101
|
+
sub.unsubscribe();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
ws.send(bin);
|
|
105
|
+
},
|
|
106
|
+
error: (error) => {
|
|
107
|
+
// error from source observable
|
|
108
|
+
reject(error);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
sub.add(() => {
|
|
112
|
+
if (ws.readyState === ws.OPEN) {
|
|
113
|
+
ws.close(1000);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
disconnected(ws)
|
|
117
|
+
.then(resolve, reject)
|
|
118
|
+
.finally(() => sub.unsubscribe());
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** 消息流插件 */
|
|
123
|
+
const MessageStream = (client) => {
|
|
124
|
+
const stream = new MessageStreamClient(client);
|
|
125
|
+
Object.defineProperty(client, 'stream', { value: stream });
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* 添加消息流插件,使得可以通过 `client.stream` 访问消息流相关接口
|
|
129
|
+
*/
|
|
130
|
+
export function MessageStreamPlugin() {
|
|
131
|
+
return MessageStream;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudpss/message-stream",
|
|
3
|
+
"version": "0.4.20",
|
|
4
|
+
"author": "CloudPSS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./*": {
|
|
16
|
+
"types": "./dist/*.d.ts",
|
|
17
|
+
"default": "./dist/*.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "yarn clean && tsc --watch",
|
|
22
|
+
"build": "yarn clean && tsc",
|
|
23
|
+
"prepublishOnly": "yarn build",
|
|
24
|
+
"clean": "rimraf dist",
|
|
25
|
+
"test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@cloudpss/ubjson": "~0.4.20",
|
|
29
|
+
"@cloudpss/ws": "~0.4.20",
|
|
30
|
+
"rxjs": "^7.8.1",
|
|
31
|
+
"type-fest": "^3.10.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@cloudpss/http-client": "~0.4.19"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {}
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Opaque } from 'type-fest';
|
|
2
|
+
|
|
3
|
+
/** UUID */
|
|
4
|
+
type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
|
5
|
+
/**
|
|
6
|
+
* 标识流的读权限
|
|
7
|
+
*/
|
|
8
|
+
export type StreamId = Opaque<UUID, 'StreamId'>;
|
|
9
|
+
/**
|
|
10
|
+
* 标识流的写权限
|
|
11
|
+
*/
|
|
12
|
+
export type StreamToken = Opaque<UUID, 'StreamToken'>;
|
|
13
|
+
export const StreamType = ['object', 'binary'] as const;
|
|
14
|
+
/**
|
|
15
|
+
* 流中消息的类型
|
|
16
|
+
* - 'object',在 StreamMessage 中使用 ubjson 存储
|
|
17
|
+
* - 'binary',在 StreamMessage 中原样存储
|
|
18
|
+
*/
|
|
19
|
+
export type StreamType = (typeof StreamType)[number];
|
|
20
|
+
/**
|
|
21
|
+
* 流的状态
|
|
22
|
+
*/
|
|
23
|
+
export type StreamState = 'inactive' | 'active' | 'frozen';
|
|
24
|
+
/** 记录调用 `open` 操作的控制器,对应的 `close` 只能由相同的控制器调用 */
|
|
25
|
+
export type StreamWriteHandler = Opaque<string, 'StreamWriteHandler'>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 消息流,需要使用 {@link StreamToken} 查询
|
|
29
|
+
*/
|
|
30
|
+
export interface Stream {
|
|
31
|
+
/** id */
|
|
32
|
+
readonly id: StreamId;
|
|
33
|
+
/** token */
|
|
34
|
+
readonly token: StreamToken;
|
|
35
|
+
|
|
36
|
+
/** 存储流中消息的类型 */
|
|
37
|
+
readonly type: StreamType;
|
|
38
|
+
/** 可选的注释 */
|
|
39
|
+
readonly comment: string;
|
|
40
|
+
/** 流的状态 */
|
|
41
|
+
state: StreamState;
|
|
42
|
+
/** 打开流的控制器,在 `active` 状态下此项非空 */
|
|
43
|
+
handler: StreamWriteHandler | null;
|
|
44
|
+
|
|
45
|
+
/** 创建时间 */
|
|
46
|
+
readonly createdAt: Date;
|
|
47
|
+
/** 最后写入时间 */
|
|
48
|
+
lastWrittenAt?: Date;
|
|
49
|
+
/** 最后读取时间 */
|
|
50
|
+
lastReadAt?: Date;
|
|
51
|
+
/** 流的持久性 */
|
|
52
|
+
readonly durability: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 消息流信息,使用 {@link StreamId} 查询 */
|
|
56
|
+
export type StreamInfo = Omit<Stream, 'token' | 'handler'>;
|
|
57
|
+
/** 创建流需要的数据 */
|
|
58
|
+
export type StreamArgs = Pick<Stream, 'type'> & Partial<Pick<Stream, 'comment' | 'durability'>>;
|
|
59
|
+
|
|
60
|
+
/** 消息 ID */
|
|
61
|
+
export type MessageId = Opaque<UUID, 'MessageId'>;
|
|
62
|
+
|
|
63
|
+
/** 原始消息 */
|
|
64
|
+
export interface RawMessage {
|
|
65
|
+
/** 消息 ID */
|
|
66
|
+
readonly id: MessageId;
|
|
67
|
+
/** 消息创建时间 */
|
|
68
|
+
readonly timestamp: Date;
|
|
69
|
+
/** 序列化的消息体 */
|
|
70
|
+
readonly data: Uint8Array;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 消息数据 */
|
|
74
|
+
interface MessageData extends Record<StreamType, unknown> {
|
|
75
|
+
/** 消息数据 */
|
|
76
|
+
object: unknown;
|
|
77
|
+
/** 消息数据 */
|
|
78
|
+
binary: Uint8Array;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 消息 */
|
|
82
|
+
export interface Message<T extends StreamType = StreamType> extends Omit<RawMessage, 'data'> {
|
|
83
|
+
/** 消息数据 */
|
|
84
|
+
readonly data: MessageData[T];
|
|
85
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { HttpClient, HttpClientPlugin } from '@cloudpss/http-client';
|
|
2
|
+
import { WebSocket, connected, disconnected } from '@cloudpss/ws';
|
|
3
|
+
import type { Writable } from 'type-fest';
|
|
4
|
+
import type {
|
|
5
|
+
Message,
|
|
6
|
+
RawMessage,
|
|
7
|
+
Stream,
|
|
8
|
+
StreamArgs,
|
|
9
|
+
StreamId,
|
|
10
|
+
StreamInfo,
|
|
11
|
+
StreamToken,
|
|
12
|
+
StreamType,
|
|
13
|
+
} from './index.js';
|
|
14
|
+
import { Observable, type ObservableInput, from } from 'rxjs';
|
|
15
|
+
import { decode, encode } from '@cloudpss/ubjson';
|
|
16
|
+
|
|
17
|
+
/** 消息流客户端 */
|
|
18
|
+
class MessageStreamClient {
|
|
19
|
+
constructor(readonly client: HttpClient) {}
|
|
20
|
+
|
|
21
|
+
/** 解析响应 */
|
|
22
|
+
private parse<T extends Stream | StreamInfo>(data: T): T {
|
|
23
|
+
(data as Writable<T>).createdAt = new Date(data.createdAt);
|
|
24
|
+
data.lastReadAt = data.lastReadAt ? new Date(data.lastReadAt) : undefined;
|
|
25
|
+
data.lastWrittenAt = data.lastWrittenAt ? new Date(data.lastWrittenAt) : undefined;
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 创建流 */
|
|
30
|
+
async create<T extends StreamType>(args: StreamArgs & { type: T }): Promise<Stream & { type: T }>;
|
|
31
|
+
/** 批量创建流 */
|
|
32
|
+
async create<T extends StreamType>(
|
|
33
|
+
args: ReadonlyArray<StreamArgs & { type: T }>,
|
|
34
|
+
): Promise<Array<Stream & { type: T }>>;
|
|
35
|
+
/** 创建流 */
|
|
36
|
+
async create(args: StreamArgs | readonly StreamArgs[]): Promise<Stream | Stream[]> {
|
|
37
|
+
if (Array.isArray(args)) {
|
|
38
|
+
const list = args as readonly StreamArgs[];
|
|
39
|
+
if (list.length === 0) return [];
|
|
40
|
+
const result = await this.client.restful<Stream[]>('/streams/bulk', args);
|
|
41
|
+
return result.data.map((item) => this.parse(item));
|
|
42
|
+
}
|
|
43
|
+
const result = await this.client.restful<Stream>('/streams', args);
|
|
44
|
+
return this.parse(result.data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 冻结流 */
|
|
48
|
+
async freeze(token: StreamToken): Promise<boolean> {
|
|
49
|
+
const result = await this.client.restful(`/streams/token/${token}/freeze`, undefined, { method: 'PUT' });
|
|
50
|
+
if (result.response.status === 204) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 删除流 */
|
|
55
|
+
async delete(token: StreamToken): Promise<void> {
|
|
56
|
+
await this.client.restful(`/streams/token/${token}`, undefined, { method: 'DELETE' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 获取流信息 */
|
|
60
|
+
async info(id: StreamId): Promise<StreamInfo> {
|
|
61
|
+
const result = await this.client.restful<StreamInfo>(`/streams/id/${id}/info`);
|
|
62
|
+
return this.parse(result.data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 获取流信息 */
|
|
66
|
+
async get(token: StreamToken): Promise<Stream> {
|
|
67
|
+
const result = await this.client.restful<Stream>(`/streams/token/${token}/info`);
|
|
68
|
+
return this.parse(result.data);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 读取消息 */
|
|
72
|
+
read<T extends StreamType>(
|
|
73
|
+
stream: {
|
|
74
|
+
id: StreamId;
|
|
75
|
+
type: T;
|
|
76
|
+
},
|
|
77
|
+
options?: { from?: Date },
|
|
78
|
+
): Observable<Message<T>> {
|
|
79
|
+
const { id, type } = stream;
|
|
80
|
+
const from = options?.from && Number(options.from) ? `?from=${options.from.toISOString()}` : '';
|
|
81
|
+
const url = this.client.config.apiUrl.replace(/^http/, 'ws') + `/streams/id/${id}${from}`;
|
|
82
|
+
return new Observable<Message<T>>((subscriber) => {
|
|
83
|
+
const ws = new WebSocket(url);
|
|
84
|
+
if (ws.binaryType === 'blob') ws.binaryType = 'arraybuffer';
|
|
85
|
+
ws.addEventListener('message', (event) => {
|
|
86
|
+
if (typeof event.data === 'string' || !event.data) return;
|
|
87
|
+
const message = decode(event.data as BinaryData) as Writable<RawMessage>;
|
|
88
|
+
message.timestamp = new Date(message.timestamp);
|
|
89
|
+
if (type === 'object') {
|
|
90
|
+
(message as Writable<Message<'object'>>).data = decode(message.data);
|
|
91
|
+
}
|
|
92
|
+
subscriber.next(message);
|
|
93
|
+
});
|
|
94
|
+
ws.addEventListener('close', (ev) => {
|
|
95
|
+
if (ev.code !== 1000) {
|
|
96
|
+
subscriber.error(new Error(`WebSocket closed with code ${ev.code}: ${ev.reason}`));
|
|
97
|
+
} else if (ev.reason) {
|
|
98
|
+
subscriber.error(new Error(ev.reason));
|
|
99
|
+
} else {
|
|
100
|
+
subscriber.complete();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
ws.addEventListener('error', (ev) => {
|
|
104
|
+
const error = ((ev as ErrorEvent).error as Error) ?? new Error(`WebSocket error`);
|
|
105
|
+
subscriber.error(error);
|
|
106
|
+
});
|
|
107
|
+
return () => {
|
|
108
|
+
if (ws.readyState === ws.OPEN) ws.close(1000);
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 写入消息 */
|
|
114
|
+
async write<T extends StreamType>(
|
|
115
|
+
stream: {
|
|
116
|
+
token: StreamToken;
|
|
117
|
+
type: T;
|
|
118
|
+
},
|
|
119
|
+
data: ObservableInput<Message<T>['data']>,
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const { token, type } = stream;
|
|
122
|
+
const url = this.client.config.apiUrl.replace(/^http/, 'ws') + `/streams/token/${token}`;
|
|
123
|
+
const ws = new WebSocket(url);
|
|
124
|
+
await connected(ws);
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const sub = from(data).subscribe({
|
|
127
|
+
next: (data) => {
|
|
128
|
+
const bin = type === 'binary' ? (data as Message<'binary'>['data']) : encode(data);
|
|
129
|
+
if (!ArrayBuffer.isView(bin)) {
|
|
130
|
+
reject(new Error(`Invalid data type: ${typeof data}`));
|
|
131
|
+
sub.unsubscribe();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
ws.send(bin);
|
|
135
|
+
},
|
|
136
|
+
error: (error) => {
|
|
137
|
+
// error from source observable
|
|
138
|
+
reject(error);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
sub.add(() => {
|
|
142
|
+
if (ws.readyState === ws.OPEN) {
|
|
143
|
+
ws.close(1000);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
disconnected(ws)
|
|
147
|
+
.then(resolve, reject)
|
|
148
|
+
.finally(() => sub.unsubscribe());
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** 消息流插件 */
|
|
153
|
+
const MessageStream: HttpClientPlugin<{ readonly stream: MessageStreamClient }> = (client) => {
|
|
154
|
+
const stream = new MessageStreamClient(client);
|
|
155
|
+
Object.defineProperty(client, 'stream', { value: stream });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 添加消息流插件,使得可以通过 `client.stream` 访问消息流相关接口
|
|
160
|
+
*/
|
|
161
|
+
export function MessageStreamPlugin(): HttpClientPlugin<{ readonly stream: MessageStreamClient }> {
|
|
162
|
+
return MessageStream;
|
|
163
|
+
}
|
package/tests/e2e.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { setTimeout } from 'node:timers/promises';
|
|
3
|
+
import { HttpClient } from '@cloudpss/http-client';
|
|
4
|
+
import { MessageStreamPlugin } from '@cloudpss/message-stream/plugin';
|
|
5
|
+
import { interval, map, take } from 'rxjs';
|
|
6
|
+
|
|
7
|
+
const http = new HttpClient({
|
|
8
|
+
apiUrl: 'http://dev.local.ddns.cloudpss.net/api/',
|
|
9
|
+
}).use(MessageStreamPlugin());
|
|
10
|
+
console.log(http);
|
|
11
|
+
const stream = await http.stream.create({ type: 'binary', comment: 'test', durability: 0 });
|
|
12
|
+
await http.stream.write(
|
|
13
|
+
stream,
|
|
14
|
+
interval(100)
|
|
15
|
+
.pipe(take(10))
|
|
16
|
+
.pipe(map((i) => new Uint32Array([i]))),
|
|
17
|
+
);
|
|
18
|
+
await http.stream.freeze(stream.token);
|
|
19
|
+
console.log(stream);
|
|
20
|
+
http.stream.read(stream).subscribe((x) => {
|
|
21
|
+
console.log(x.data);
|
|
22
|
+
});
|
|
23
|
+
await setTimeout(10);
|
|
24
|
+
await http.stream.delete(stream.token);
|