@cloudpss/message-stream 0.5.36 → 0.5.39
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 +23 -1
- package/dist/index.d.ts +1 -6
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +47 -0
- package/dist/plugin.js +184 -0
- package/dist/plugin.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +20 -5
- package/src/index.ts +1 -8
- package/src/plugin.ts +209 -0
- package/tests/e2e.js +33 -0
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
1
|
# @cloudpss/message-stream
|
|
2
2
|
|
|
3
|
-
Message stream type definitions for cloudpss APIs.
|
|
3
|
+
Message stream type definitions for cloudpss APIs. And plugin for `@cloudpss/http-client`, provides cloudpss message stream APIs.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { HttpClient } from '@cloudpss/http-client';
|
|
9
|
+
import { MessageStreamPlugin } from '@cloudpss/message-stream/plugin';
|
|
10
|
+
import { interval } from 'rxjs';
|
|
11
|
+
import { map, take } from 'rxjs/operators';
|
|
12
|
+
|
|
13
|
+
const http = new HttpClient({
|
|
14
|
+
/* config */
|
|
15
|
+
}).use(MessageStreamPlugin());
|
|
16
|
+
const stream = await http.stream.create({ type: 'object', comment: 'test', durability: 0 });
|
|
17
|
+
await http.stream.write(
|
|
18
|
+
stream.token,
|
|
19
|
+
stream.type,
|
|
20
|
+
interval(100)
|
|
21
|
+
.pipe(take(10))
|
|
22
|
+
.pipe(map((i) => ({ i }))),
|
|
23
|
+
);
|
|
24
|
+
await http.stream.freeze(stream.token);
|
|
25
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { Tagged, Simplify } from 'type-fest';
|
|
2
|
-
|
|
3
|
-
type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
|
4
|
-
/** 空 UUID */
|
|
5
|
-
type NIL_UUID = typeof NIL_UUID;
|
|
6
|
-
/** 空 UUID */
|
|
7
|
-
declare const NIL_UUID: "00000000-0000-0000-0000-000000000000";
|
|
2
|
+
import { type UUID, NIL_UUID } from '@cloudpss/id';
|
|
8
3
|
/**
|
|
9
4
|
* 标识流的读权限
|
|
10
5
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
3
|
-
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
1
|
+
import { NIL_UUID } from '@cloudpss/id';
|
|
4
2
|
/** 表示一个空白的流 ID,读取此 ID 对应的流会得到不包含消息的且已冻结的空白流 */
|
|
5
3
|
export const EMPTY_STREAM_ID = NIL_UUID;
|
|
6
4
|
/** 表示一个空白的流 Token,写入此 Token 的消息会被直接丢弃,不会产生错误 */
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAC;AAMnD,+CAA+C;AAC/C,MAAM,CAAC,MAAM,eAAe,GAAG,QAAwC,CAAC;AAMxE,gDAAgD;AAChD,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAA2C,CAAC;AAC9E,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAU,CAAC,CAAC"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Observable, type ObservableInput } from 'rxjs';
|
|
2
|
+
import type { HttpClient, HttpClientPlugin } from '@cloudpss/http-client';
|
|
3
|
+
import type { Message, RawMessage, Stream, StreamArgs, StreamId, StreamInfo, StreamToken, StreamType } from './index.js';
|
|
4
|
+
export type { Message, RawMessage, Stream, StreamArgs, StreamId, StreamInfo, StreamToken, StreamType };
|
|
5
|
+
/** 消息流客户端 */
|
|
6
|
+
declare class MessageStreamClient {
|
|
7
|
+
readonly client: HttpClient;
|
|
8
|
+
constructor(client: HttpClient);
|
|
9
|
+
/** 创建流 */
|
|
10
|
+
create<T extends StreamType>(args: StreamArgs & {
|
|
11
|
+
readonly type: T;
|
|
12
|
+
}): Promise<Stream & {
|
|
13
|
+
readonly type: T;
|
|
14
|
+
}>;
|
|
15
|
+
/** 批量创建流 */
|
|
16
|
+
create<T extends StreamType>(args: ReadonlyArray<StreamArgs & {
|
|
17
|
+
readonly type: T;
|
|
18
|
+
}>): Promise<Array<Stream & {
|
|
19
|
+
readonly type: T;
|
|
20
|
+
}>>;
|
|
21
|
+
/** 冻结流 */
|
|
22
|
+
freeze(token: StreamToken): Promise<boolean>;
|
|
23
|
+
/** 删除流 */
|
|
24
|
+
delete(token: StreamToken): Promise<void>;
|
|
25
|
+
/** 获取流信息 */
|
|
26
|
+
info(id: StreamId): Promise<StreamInfo>;
|
|
27
|
+
/** 获取流信息 */
|
|
28
|
+
get(token: StreamToken): Promise<Stream>;
|
|
29
|
+
/** 读取消息 */
|
|
30
|
+
read<T extends StreamType>(stream: {
|
|
31
|
+
readonly id: StreamId;
|
|
32
|
+
readonly type: T;
|
|
33
|
+
}, options?: {
|
|
34
|
+
from?: Date;
|
|
35
|
+
}): Observable<Message<T>>;
|
|
36
|
+
/** 写入消息 */
|
|
37
|
+
write<T extends StreamType>(stream: {
|
|
38
|
+
readonly token: StreamToken;
|
|
39
|
+
readonly type: T;
|
|
40
|
+
}, data: ObservableInput<Message<T>['data']>): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 添加消息流插件,使得可以通过 `client.stream` 访问消息流相关接口
|
|
44
|
+
*/
|
|
45
|
+
export declare function MessageStreamPlugin(): HttpClientPlugin<{
|
|
46
|
+
readonly stream: MessageStreamClient;
|
|
47
|
+
}>;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Observable, from } from 'rxjs';
|
|
2
|
+
import { connected, disconnected } from '@cloudpss/fetch';
|
|
3
|
+
import { decode, decodeMany, encodeMany } from '@cloudpss/ubjson';
|
|
4
|
+
/** 解析响应 */
|
|
5
|
+
function parseStreamInfo(data) {
|
|
6
|
+
data.createdAt = new Date(data.createdAt);
|
|
7
|
+
data.lastReadAt = data.lastReadAt != null ? new Date(data.lastReadAt) : undefined;
|
|
8
|
+
data.lastWrittenAt = data.lastWrittenAt != null ? new Date(data.lastWrittenAt) : undefined;
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
/** 解析响应 */
|
|
12
|
+
function parseMessage(type, data) {
|
|
13
|
+
const message = decode(data);
|
|
14
|
+
message.timestamp = new Date(message.timestamp);
|
|
15
|
+
if (type === 'object') {
|
|
16
|
+
message.data = [...decodeMany(message.data)];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// reallocate the buffer to make it non-shared
|
|
20
|
+
// (message as Omit<RawMessage, 'data'> as Writable<Message<'binary'>>).data = Uint8Array.from(message.data);
|
|
21
|
+
}
|
|
22
|
+
return message;
|
|
23
|
+
}
|
|
24
|
+
/** 消息流客户端 */
|
|
25
|
+
class MessageStreamClient {
|
|
26
|
+
client;
|
|
27
|
+
constructor(client) {
|
|
28
|
+
this.client = client;
|
|
29
|
+
}
|
|
30
|
+
/** 创建流 */
|
|
31
|
+
async create(args) {
|
|
32
|
+
if (Array.isArray(args)) {
|
|
33
|
+
const list = args;
|
|
34
|
+
if (list.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const result = await this.client.restful('/streams/bulk', args);
|
|
37
|
+
return result.data.map(parseStreamInfo);
|
|
38
|
+
}
|
|
39
|
+
const result = await this.client.restful('/streams', args);
|
|
40
|
+
return parseStreamInfo(result.data);
|
|
41
|
+
}
|
|
42
|
+
/** 冻结流 */
|
|
43
|
+
async freeze(token) {
|
|
44
|
+
const result = await this.client.restful(`/streams/token/${token}/freeze`, undefined, { method: 'PUT' });
|
|
45
|
+
if (result.response.status === 204)
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** 删除流 */
|
|
50
|
+
async delete(token) {
|
|
51
|
+
await this.client.restful(`/streams/token/${token}`, undefined, { method: 'DELETE' });
|
|
52
|
+
}
|
|
53
|
+
/** 获取流信息 */
|
|
54
|
+
async info(id) {
|
|
55
|
+
const result = await this.client.restful(`/streams/id/${id}/info`);
|
|
56
|
+
return parseStreamInfo(result.data);
|
|
57
|
+
}
|
|
58
|
+
/** 获取流信息 */
|
|
59
|
+
async get(token) {
|
|
60
|
+
const result = await this.client.restful(`/streams/token/${token}/info`);
|
|
61
|
+
return parseStreamInfo(result.data);
|
|
62
|
+
}
|
|
63
|
+
/** 读取消息 */
|
|
64
|
+
read(stream, options) {
|
|
65
|
+
const { id, type } = stream;
|
|
66
|
+
const from = options?.from && Number(options.from) ? `?from=${options.from.toISOString()}` : '';
|
|
67
|
+
return new Observable((subscriber) => {
|
|
68
|
+
const ws = this.client.ws(`/streams/id/${id}${from}`);
|
|
69
|
+
ws.binaryType = 'arraybuffer';
|
|
70
|
+
ws.addEventListener('message', (event) => {
|
|
71
|
+
if (typeof event.data === 'string' || !event.data)
|
|
72
|
+
return;
|
|
73
|
+
subscriber.next(parseMessage(type, event.data));
|
|
74
|
+
});
|
|
75
|
+
ws.addEventListener('close', (ev) => {
|
|
76
|
+
if (ev.code !== 1000) {
|
|
77
|
+
subscriber.error(new Error(`WebSocket closed with code ${ev.code}: ${ev.reason}`));
|
|
78
|
+
}
|
|
79
|
+
else if (ev.reason) {
|
|
80
|
+
subscriber.error(new Error(ev.reason));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
subscriber.complete();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
ws.addEventListener('error', (ev) => {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (!subscriber.closed) {
|
|
89
|
+
// make sure the error event is not dispatched before the close event
|
|
90
|
+
const error = ev.error ?? new Error(`WebSocket error`);
|
|
91
|
+
subscriber.error(error);
|
|
92
|
+
}
|
|
93
|
+
}, 1);
|
|
94
|
+
});
|
|
95
|
+
return () => {
|
|
96
|
+
if (ws.readyState === ws.OPEN)
|
|
97
|
+
ws.close(1000);
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** 写入消息 */
|
|
102
|
+
async write(stream, data) {
|
|
103
|
+
const { token, type } = stream;
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const ws = this.client.ws(`/streams/token/${token}`);
|
|
106
|
+
let queue = connected(ws).catch((error) => {
|
|
107
|
+
sub.unsubscribe();
|
|
108
|
+
throw error;
|
|
109
|
+
});
|
|
110
|
+
const sub = from(data).subscribe({
|
|
111
|
+
next: (data) => {
|
|
112
|
+
if (data === undefined)
|
|
113
|
+
return;
|
|
114
|
+
queue = queue.then(() => {
|
|
115
|
+
try {
|
|
116
|
+
if (ws.readyState !== ws.OPEN) {
|
|
117
|
+
throw new Error(`WebSocket is already in CLOSING or CLOSED state.`);
|
|
118
|
+
}
|
|
119
|
+
if (type === 'object') {
|
|
120
|
+
if (!Array.isArray(data)) {
|
|
121
|
+
throw new TypeError(`Invalid data type, object stream only accepts array.`);
|
|
122
|
+
}
|
|
123
|
+
if (data.length)
|
|
124
|
+
ws.send(encodeMany(data));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (!ArrayBuffer.isView(data)) {
|
|
129
|
+
throw new TypeError(`Invalid data type, binary stream only accepts Uint8Array.`);
|
|
130
|
+
}
|
|
131
|
+
ws.send(data);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (ex) {
|
|
135
|
+
sub.unsubscribe();
|
|
136
|
+
throw ex;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
error: (error) => {
|
|
141
|
+
// error from source observable
|
|
142
|
+
queue = queue.then(() => {
|
|
143
|
+
throw error;
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
sub.add(() => {
|
|
148
|
+
void (async () => {
|
|
149
|
+
let err;
|
|
150
|
+
try {
|
|
151
|
+
await queue;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
err = e;
|
|
155
|
+
}
|
|
156
|
+
ws.close(1000);
|
|
157
|
+
try {
|
|
158
|
+
await disconnected(ws);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
err ??= e;
|
|
162
|
+
}
|
|
163
|
+
if (err === undefined) {
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
reject(err);
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 添加消息流插件,使得可以通过 `client.stream` 访问消息流相关接口
|
|
176
|
+
*/
|
|
177
|
+
export function MessageStreamPlugin() {
|
|
178
|
+
return (client) => {
|
|
179
|
+
const stream = new MessageStreamClient(client);
|
|
180
|
+
Object.defineProperty(client, 'stream', { value: stream, configurable: true });
|
|
181
|
+
return client;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//# 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudpss/message-stream",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.39",
|
|
4
4
|
"author": "CloudPSS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,16 +8,31 @@
|
|
|
8
8
|
"module": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./plugin": "./dist/plugin.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"
|
|
15
|
+
"rxjs": "^7.8.1",
|
|
16
|
+
"type-fest": "^4.26.0",
|
|
17
|
+
"@cloudpss/fetch": "~0.5.39",
|
|
18
|
+
"@cloudpss/id": "~0.5.39",
|
|
19
|
+
"@cloudpss/ubjson": "~0.5.39"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@cloudpss/http-client": "~0.5.39"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"@cloudpss/http-client": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@cloudpss/http-client": "~0.5.39"
|
|
16
31
|
},
|
|
17
32
|
"scripts": {
|
|
18
33
|
"start": "pnpm clean && tsc --watch",
|
|
19
34
|
"build": "pnpm clean && tsc",
|
|
20
35
|
"clean": "rimraf dist",
|
|
21
|
-
"test": "echo
|
|
36
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --experimental-vm-modules\" echo no tests"
|
|
22
37
|
}
|
|
23
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import type { Tagged, Simplify } from 'type-fest';
|
|
2
|
+
import { type UUID, NIL_UUID } from '@cloudpss/id';
|
|
2
3
|
|
|
3
|
-
/** UUID */
|
|
4
|
-
type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
|
5
|
-
|
|
6
|
-
/** 空 UUID */
|
|
7
|
-
type NIL_UUID = typeof NIL_UUID;
|
|
8
|
-
/** 空 UUID */
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
10
|
-
const NIL_UUID = '00000000-0000-0000-0000-000000000000' as const;
|
|
11
4
|
/**
|
|
12
5
|
* 标识流的读权限
|
|
13
6
|
*/
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Writable } from 'type-fest';
|
|
2
|
+
import { Observable, type ObservableInput, from } from 'rxjs';
|
|
3
|
+
import type { HttpClient, HttpClientPlugin } from '@cloudpss/http-client';
|
|
4
|
+
import { connected, disconnected } from '@cloudpss/fetch';
|
|
5
|
+
import { decode, decodeMany, encodeMany } from '@cloudpss/ubjson';
|
|
6
|
+
import type {
|
|
7
|
+
Message,
|
|
8
|
+
RawMessage,
|
|
9
|
+
Stream,
|
|
10
|
+
StreamArgs,
|
|
11
|
+
StreamId,
|
|
12
|
+
StreamInfo,
|
|
13
|
+
StreamToken,
|
|
14
|
+
StreamType,
|
|
15
|
+
} from './index.js';
|
|
16
|
+
|
|
17
|
+
export type { Message, RawMessage, Stream, StreamArgs, StreamId, StreamInfo, StreamToken, StreamType };
|
|
18
|
+
|
|
19
|
+
/** 解析响应 */
|
|
20
|
+
function parseStreamInfo<T extends Stream | StreamInfo>(data: T): T {
|
|
21
|
+
(data as Writable<T>).createdAt = new Date(data.createdAt);
|
|
22
|
+
data.lastReadAt = data.lastReadAt != null ? new Date(data.lastReadAt) : undefined;
|
|
23
|
+
data.lastWrittenAt = data.lastWrittenAt != null ? new Date(data.lastWrittenAt) : undefined;
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 解析响应 */
|
|
28
|
+
function parseMessage<T extends StreamType>(type: T, data: ArrayBuffer): Message<T> {
|
|
29
|
+
const message = decode(data) as RawMessage;
|
|
30
|
+
(message as Writable<Message>).timestamp = new Date(message.timestamp);
|
|
31
|
+
if (type === 'object') {
|
|
32
|
+
(message as Omit<RawMessage, 'data'> as Writable<Message<'object'>>).data = [...decodeMany(message.data)];
|
|
33
|
+
} else {
|
|
34
|
+
// reallocate the buffer to make it non-shared
|
|
35
|
+
// (message as Omit<RawMessage, 'data'> as Writable<Message<'binary'>>).data = Uint8Array.from(message.data);
|
|
36
|
+
}
|
|
37
|
+
return message as Message<T>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 消息流客户端 */
|
|
41
|
+
class MessageStreamClient {
|
|
42
|
+
constructor(readonly client: HttpClient) {}
|
|
43
|
+
|
|
44
|
+
/** 创建流 */
|
|
45
|
+
async create<T extends StreamType>(args: StreamArgs & { readonly type: T }): Promise<Stream & { readonly type: T }>;
|
|
46
|
+
/** 批量创建流 */
|
|
47
|
+
async create<T extends StreamType>(
|
|
48
|
+
args: ReadonlyArray<StreamArgs & { readonly type: T }>,
|
|
49
|
+
): Promise<Array<Stream & { readonly type: T }>>;
|
|
50
|
+
/** 创建流 */
|
|
51
|
+
async create(args: StreamArgs | readonly StreamArgs[]): Promise<Stream | Stream[]> {
|
|
52
|
+
if (Array.isArray(args)) {
|
|
53
|
+
const list = args as readonly StreamArgs[];
|
|
54
|
+
if (list.length === 0) return [];
|
|
55
|
+
const result = await this.client.restful<Stream[]>('/streams/bulk', args);
|
|
56
|
+
return result.data.map(parseStreamInfo);
|
|
57
|
+
}
|
|
58
|
+
const result = await this.client.restful<Stream>('/streams', args);
|
|
59
|
+
return parseStreamInfo(result.data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 冻结流 */
|
|
63
|
+
async freeze(token: StreamToken): Promise<boolean> {
|
|
64
|
+
const result = await this.client.restful(`/streams/token/${token}/freeze`, undefined, { method: 'PUT' });
|
|
65
|
+
if (result.response.status === 204) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 删除流 */
|
|
70
|
+
async delete(token: StreamToken): Promise<void> {
|
|
71
|
+
await this.client.restful(`/streams/token/${token}`, undefined, { method: 'DELETE' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 获取流信息 */
|
|
75
|
+
async info(id: StreamId): Promise<StreamInfo> {
|
|
76
|
+
const result = await this.client.restful<StreamInfo>(`/streams/id/${id}/info`);
|
|
77
|
+
return parseStreamInfo(result.data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 获取流信息 */
|
|
81
|
+
async get(token: StreamToken): Promise<Stream> {
|
|
82
|
+
const result = await this.client.restful<Stream>(`/streams/token/${token}/info`);
|
|
83
|
+
return parseStreamInfo(result.data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 读取消息 */
|
|
87
|
+
read<T extends StreamType>(
|
|
88
|
+
stream: {
|
|
89
|
+
readonly id: StreamId;
|
|
90
|
+
readonly type: T;
|
|
91
|
+
},
|
|
92
|
+
options?: { from?: Date },
|
|
93
|
+
): Observable<Message<T>> {
|
|
94
|
+
const { id, type } = stream;
|
|
95
|
+
const from = options?.from && Number(options.from) ? `?from=${options.from.toISOString()}` : '';
|
|
96
|
+
return new Observable<Message<T>>((subscriber) => {
|
|
97
|
+
const ws = this.client.ws(`/streams/id/${id}${from}`);
|
|
98
|
+
ws.binaryType = 'arraybuffer';
|
|
99
|
+
ws.addEventListener('message', (event) => {
|
|
100
|
+
if (typeof event.data === 'string' || !event.data) return;
|
|
101
|
+
subscriber.next(parseMessage(type, event.data as ArrayBuffer));
|
|
102
|
+
});
|
|
103
|
+
ws.addEventListener('close', (ev) => {
|
|
104
|
+
if (ev.code !== 1000) {
|
|
105
|
+
subscriber.error(new Error(`WebSocket closed with code ${ev.code}: ${ev.reason}`));
|
|
106
|
+
} else if (ev.reason) {
|
|
107
|
+
subscriber.error(new Error(ev.reason));
|
|
108
|
+
} else {
|
|
109
|
+
subscriber.complete();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
ws.addEventListener('error', (ev) => {
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
if (!subscriber.closed) {
|
|
115
|
+
// make sure the error event is not dispatched before the close event
|
|
116
|
+
const error = ((ev as ErrorEvent).error as Error) ?? new Error(`WebSocket error`);
|
|
117
|
+
subscriber.error(error);
|
|
118
|
+
}
|
|
119
|
+
}, 1);
|
|
120
|
+
});
|
|
121
|
+
return () => {
|
|
122
|
+
if (ws.readyState === ws.OPEN) ws.close(1000);
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 写入消息 */
|
|
128
|
+
async write<T extends StreamType>(
|
|
129
|
+
stream: {
|
|
130
|
+
readonly token: StreamToken;
|
|
131
|
+
readonly type: T;
|
|
132
|
+
},
|
|
133
|
+
data: ObservableInput<Message<T>['data']>,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const { token, type } = stream;
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const ws = this.client.ws(`/streams/token/${token}`);
|
|
138
|
+
let queue = connected(ws).catch((error) => {
|
|
139
|
+
sub.unsubscribe();
|
|
140
|
+
throw error;
|
|
141
|
+
});
|
|
142
|
+
const sub = from(data).subscribe({
|
|
143
|
+
next: (data) => {
|
|
144
|
+
if (data === undefined) return;
|
|
145
|
+
queue = queue.then(() => {
|
|
146
|
+
try {
|
|
147
|
+
if (ws.readyState !== ws.OPEN) {
|
|
148
|
+
throw new Error(`WebSocket is already in CLOSING or CLOSED state.`);
|
|
149
|
+
}
|
|
150
|
+
if (type === 'object') {
|
|
151
|
+
if (!Array.isArray(data)) {
|
|
152
|
+
throw new TypeError(`Invalid data type, object stream only accepts array.`);
|
|
153
|
+
}
|
|
154
|
+
if (data.length) ws.send(encodeMany(data));
|
|
155
|
+
return;
|
|
156
|
+
} else {
|
|
157
|
+
if (!ArrayBuffer.isView(data)) {
|
|
158
|
+
throw new TypeError(`Invalid data type, binary stream only accepts Uint8Array.`);
|
|
159
|
+
}
|
|
160
|
+
ws.send(data);
|
|
161
|
+
}
|
|
162
|
+
} catch (ex) {
|
|
163
|
+
sub.unsubscribe();
|
|
164
|
+
throw ex;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
error: (error: Error) => {
|
|
169
|
+
// error from source observable
|
|
170
|
+
queue = queue.then(() => {
|
|
171
|
+
throw error;
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
sub.add(() => {
|
|
176
|
+
void (async () => {
|
|
177
|
+
let err: unknown;
|
|
178
|
+
try {
|
|
179
|
+
await queue;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
err = e;
|
|
182
|
+
}
|
|
183
|
+
ws.close(1000);
|
|
184
|
+
try {
|
|
185
|
+
await disconnected(ws);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
err ??= e;
|
|
188
|
+
}
|
|
189
|
+
if (err === undefined) {
|
|
190
|
+
resolve();
|
|
191
|
+
} else {
|
|
192
|
+
reject(err as Error);
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 添加消息流插件,使得可以通过 `client.stream` 访问消息流相关接口
|
|
202
|
+
*/
|
|
203
|
+
export function MessageStreamPlugin(): HttpClientPlugin<{ readonly stream: MessageStreamClient }> {
|
|
204
|
+
return (client) => {
|
|
205
|
+
const stream = new MessageStreamClient(client);
|
|
206
|
+
Object.defineProperty(client, 'stream', { value: stream, configurable: true });
|
|
207
|
+
return client as HttpClient & { readonly stream: MessageStreamClient };
|
|
208
|
+
};
|
|
209
|
+
}
|
package/tests/e2e.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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 { Subject } 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
|
+
console.log(stream);
|
|
13
|
+
const source = new Subject();
|
|
14
|
+
|
|
15
|
+
const w = http.stream.write(stream, source);
|
|
16
|
+
for (const i of Array.from({ length: 5 }).keys()) source.next(new Uint8Array([i]));
|
|
17
|
+
source.next(10);
|
|
18
|
+
for (const i of Array.from({ length: 5 }).keys()) source.next(new Uint8Array([i + 5]));
|
|
19
|
+
source.error('xxs');
|
|
20
|
+
source.complete();
|
|
21
|
+
try {
|
|
22
|
+
await w;
|
|
23
|
+
console.log('write ok');
|
|
24
|
+
} catch (ex) {
|
|
25
|
+
console.log('write error', ex);
|
|
26
|
+
}
|
|
27
|
+
await http.stream.freeze(stream.token);
|
|
28
|
+
console.log(stream);
|
|
29
|
+
http.stream.read(stream).subscribe((x) => {
|
|
30
|
+
console.log(x.data);
|
|
31
|
+
});
|
|
32
|
+
await setTimeout(10);
|
|
33
|
+
await http.stream.delete(stream.token);
|