@thejrsoft/subway-protocol 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACK_MESSAGES_IMPLEMENTATION_SUMMARY.md +128 -0
- package/ACK_MESSAGE_DESIGN.md +457 -0
- package/CHANGELOG.md +58 -0
- package/COMMAND_VALIDATION_RULES.md +178 -0
- package/DOCUMENTATION_REORGANIZATION_SUMMARY.md +81 -0
- package/DOCUMENTATION_STRUCTURE.md +106 -0
- package/GATEWAY_MIGRATION_GUIDE.md +130 -0
- package/GATEWAY_PROTOCOL_COMPARISON.md +216 -0
- package/INTEGRATION_GUIDE.md +190 -0
- package/OPTIONAL_FIELDS_WITHOUT_DEFAULTS.md +97 -0
- package/PROTOCOL_UTILS_USAGE.md +278 -0
- package/README.md +237 -0
- package/TYPE_FIXES_SUMMARY.md +210 -0
- package/UPDATE_ENUM_VALUES.md +139 -0
- package/dist/asyncapi-sync.d.ts +47 -0
- package/dist/asyncapi-sync.d.ts.map +1 -0
- package/dist/asyncapi-sync.js +85 -0
- package/dist/asyncapi-sync.js.map +1 -0
- package/dist/command-factory.d.ts +62 -0
- package/dist/command-factory.d.ts.map +1 -0
- package/dist/command-factory.js +137 -0
- package/dist/command-factory.js.map +1 -0
- package/dist/command-types.d.ts +27 -0
- package/dist/command-types.d.ts.map +1 -0
- package/dist/command-types.js +31 -0
- package/dist/command-types.js.map +1 -0
- package/dist/index.d.ts +403 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +413 -0
- package/dist/index.js.map +1 -0
- package/dist/message-validator.d.ts +102 -0
- package/dist/message-validator.d.ts.map +1 -0
- package/dist/message-validator.js +640 -0
- package/dist/message-validator.js.map +1 -0
- package/dist/protocol-utils.d.ts +108 -0
- package/dist/protocol-utils.d.ts.map +1 -0
- package/dist/protocol-utils.js +293 -0
- package/dist/protocol-utils.js.map +1 -0
- package/docs/01-protocol/README.md +45 -0
- package/docs/01-protocol/design-rationale.md +198 -0
- package/docs/01-protocol/message-types.md +669 -0
- package/docs/01-protocol/specification.md +1466 -0
- package/docs/02-commands/README.md +56 -0
- package/docs/02-commands/batch-command.md +435 -0
- package/docs/02-commands/complex-command.md +537 -0
- package/docs/02-commands/simple-command.md +332 -0
- package/docs/02-commands/typed-commands.md +362 -0
- package/docs/03-architecture/README.md +66 -0
- package/docs/03-architecture/device-protocol.md +430 -0
- package/docs/03-architecture/edge-proxy.md +727 -0
- package/docs/03-architecture/routing-flow.md +893 -0
- package/docs/04-integration/README.md +144 -0
- package/docs/04-integration/backend-guide.md +551 -0
- package/docs/04-integration/edge-guide.md +684 -0
- package/docs/04-integration/gateway-guide.md +180 -0
- package/docs/04-integration/migration-guide.md +226 -0
- package/docs/05-examples/README.md +141 -0
- package/docs/05-examples/progress-update-examples.md +757 -0
- package/docs/06-reference/README.md +67 -0
- package/docs/06-reference/api.md +572 -0
- package/docs/06-reference/faq.md +302 -0
- package/docs/06-reference/glossary.md +232 -0
- package/examples/backend-upgrade.ts +279 -0
- package/examples/edge-multi-device.ts +513 -0
- package/examples/gateway-upgrade.ts +150 -0
- package/examples/protocol-implementation.ts +715 -0
- package/package.json +48 -0
- package/scripts/validate-asyncapi.ts +78 -0
- package/src/__tests__/protocol.test.ts +297 -0
- package/src/asyncapi-sync.ts +84 -0
- package/src/command-factory.ts +183 -0
- package/src/command-types.ts +72 -0
- package/src/edge-proxy.ts +494 -0
- package/src/gateway-extensions.ts +278 -0
- package/src/index.ts +792 -0
- package/src/message-validator.ts +726 -0
- package/src/protocol-utils.ts +355 -0
- package/tsconfig.json +24 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thejrsoft/subway-protocol",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Shared WebSocket protocol definitions for JRSoft Subway",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"watch": "tsc -w",
|
|
10
|
+
"clean": "rm -rf dist",
|
|
11
|
+
"validate:asyncapi": "ts-node scripts/validate-asyncapi.ts",
|
|
12
|
+
"generate:asyncapi-enums": "ts-node -e \"import {generateAsyncApiEnums} from './src/asyncapi-sync'; console.log(generateAsyncApiEnums())\"",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"lint": "eslint src --ext .ts",
|
|
15
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["websocket", "protocol", "jrsoft", "subway"],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/YOUR_ORG/jrsoft-subway.git"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/**/*",
|
|
29
|
+
"src/**/*",
|
|
30
|
+
"docs/**/*",
|
|
31
|
+
"examples/**/*",
|
|
32
|
+
"scripts/**/*",
|
|
33
|
+
"*.md",
|
|
34
|
+
"tsconfig.json"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^18.0.0",
|
|
38
|
+
"@types/jest": "^29.0.0",
|
|
39
|
+
"@asyncapi/parser": "^2.1.0",
|
|
40
|
+
"typescript": "^5.0.0",
|
|
41
|
+
"jest": "^29.0.0",
|
|
42
|
+
"ts-jest": "^29.0.0",
|
|
43
|
+
"eslint": "^8.0.0",
|
|
44
|
+
"@typescript-eslint/parser": "^5.0.0",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
|
46
|
+
"ts-node": "^10.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 验证 TypeScript 实现与 AsyncAPI 规范的一致性
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Parser } from '@asyncapi/parser';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import {
|
|
10
|
+
MessageType,
|
|
11
|
+
RegisterMessage,
|
|
12
|
+
CommandMessage
|
|
13
|
+
} from '../src/index';
|
|
14
|
+
|
|
15
|
+
async function validateProtocolConsistency() {
|
|
16
|
+
// 1. 解析 AsyncAPI 文档
|
|
17
|
+
const parser = new Parser();
|
|
18
|
+
const asyncApiPath = path.join(__dirname, '../../jrsoft-subway-gateway/asyncapi.yaml');
|
|
19
|
+
const asyncApiContent = fs.readFileSync(asyncApiPath, 'utf8');
|
|
20
|
+
|
|
21
|
+
const { document, diagnostics } = await parser.parse(asyncApiContent);
|
|
22
|
+
|
|
23
|
+
if (diagnostics.length > 0) {
|
|
24
|
+
console.error('AsyncAPI parsing errors:', diagnostics);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. 提取 AsyncAPI 中定义的消息类型
|
|
29
|
+
const asyncApiMessages = new Set<string>();
|
|
30
|
+
const channels = document?.channels();
|
|
31
|
+
|
|
32
|
+
if (channels) {
|
|
33
|
+
for (const [channelName, channel] of Object.entries(channels)) {
|
|
34
|
+
const publish = channel.publish();
|
|
35
|
+
const subscribe = channel.subscribe();
|
|
36
|
+
|
|
37
|
+
[publish, subscribe].forEach(operation => {
|
|
38
|
+
if (operation?.messages()) {
|
|
39
|
+
operation.messages().forEach(msg => {
|
|
40
|
+
const payload = msg.payload();
|
|
41
|
+
if (payload?.properties?.type?.const) {
|
|
42
|
+
asyncApiMessages.add(payload.properties.type.const);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. 比较 TypeScript 枚举与 AsyncAPI 定义
|
|
51
|
+
const tsMessageTypes = Object.values(MessageType);
|
|
52
|
+
const missingInAsyncApi = tsMessageTypes.filter(type => !asyncApiMessages.has(type));
|
|
53
|
+
const missingInTypeScript = Array.from(asyncApiMessages).filter(type => !tsMessageTypes.includes(type as any));
|
|
54
|
+
|
|
55
|
+
// 4. 输出验证结果
|
|
56
|
+
console.log('=== Protocol Validation Results ===');
|
|
57
|
+
console.log(`TypeScript message types: ${tsMessageTypes.length}`);
|
|
58
|
+
console.log(`AsyncAPI message types: ${asyncApiMessages.size}`);
|
|
59
|
+
|
|
60
|
+
if (missingInAsyncApi.length > 0) {
|
|
61
|
+
console.error('❌ Missing in AsyncAPI:', missingInAsyncApi);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (missingInTypeScript.length > 0) {
|
|
65
|
+
console.error('❌ Missing in TypeScript:', missingInTypeScript);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (missingInAsyncApi.length === 0 && missingInTypeScript.length === 0) {
|
|
69
|
+
console.log('✅ All message types are synchronized!');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return missingInAsyncApi.length === 0 && missingInTypeScript.length === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 运行验证
|
|
76
|
+
validateProtocolConsistency().then(isValid => {
|
|
77
|
+
process.exit(isValid ? 0 : 1);
|
|
78
|
+
});
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议规范测试套件
|
|
3
|
+
* 验证实现是否符合 PROTOCOL_SPECIFICATION.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MessageFactory,
|
|
8
|
+
MessageValidator,
|
|
9
|
+
MessageType,
|
|
10
|
+
ClientType,
|
|
11
|
+
Priority,
|
|
12
|
+
OperationType,
|
|
13
|
+
CommandType,
|
|
14
|
+
GatewayUtils,
|
|
15
|
+
GATEWAY_SITE_ID,
|
|
16
|
+
isRegisterMessage,
|
|
17
|
+
isCommandMessage
|
|
18
|
+
} from '../index';
|
|
19
|
+
|
|
20
|
+
describe('JRSoft Subway Protocol v1.0', () => {
|
|
21
|
+
describe('基础消息格式', () => {
|
|
22
|
+
test('所有消息必须包含 type 字段', () => {
|
|
23
|
+
const message = { type: 'register', clientId: 'test' };
|
|
24
|
+
expect(MessageValidator.validateMessage(message)).toBe(true);
|
|
25
|
+
|
|
26
|
+
const invalidMessage = { clientId: 'test' };
|
|
27
|
+
expect(MessageValidator.validateMessage(invalidMessage)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('timestamp 和 version 字段是可选的', () => {
|
|
31
|
+
const minimalMessage = { type: 'register', clientId: 'test' };
|
|
32
|
+
expect(MessageValidator.validateRegisterMessage(minimalMessage)).toBe(true);
|
|
33
|
+
|
|
34
|
+
const fullMessage = {
|
|
35
|
+
type: 'register',
|
|
36
|
+
clientId: 'test',
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
version: '1.0'
|
|
39
|
+
};
|
|
40
|
+
expect(MessageValidator.validateRegisterMessage(fullMessage)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('注册消息兼容性', () => {
|
|
45
|
+
test('支持简单注册格式(v1.0)', () => {
|
|
46
|
+
const simpleRegister = {
|
|
47
|
+
type: 'register',
|
|
48
|
+
clientId: 'device001'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
expect(isRegisterMessage(simpleRegister)).toBe(true);
|
|
52
|
+
expect(MessageValidator.validateRegisterMessage(simpleRegister)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('支持完整注册格式(v1.0)', () => {
|
|
56
|
+
const fullRegister = MessageFactory.createRegisterMessage(
|
|
57
|
+
'backend-server',
|
|
58
|
+
ClientType.BACKEND,
|
|
59
|
+
{
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
platform: 'nodejs',
|
|
62
|
+
capabilities: ['command', 'program']
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(isRegisterMessage(fullRegister)).toBe(true);
|
|
67
|
+
expect(MessageValidator.validateRegisterMessage(fullRegister)).toBe(true);
|
|
68
|
+
expect(fullRegister.clientType).toBe(ClientType.BACKEND);
|
|
69
|
+
expect(fullRegister.clientInfo?.capabilities).toContain('command');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('命令消息格式', () => {
|
|
74
|
+
test('标准命令消息', () => {
|
|
75
|
+
const command = MessageFactory.createCommandMessage(
|
|
76
|
+
'req-123',
|
|
77
|
+
'device001',
|
|
78
|
+
{
|
|
79
|
+
commandType: CommandType.SIMPLE,
|
|
80
|
+
commandCode: 'READ_STATUS',
|
|
81
|
+
deviceType: 'controller',
|
|
82
|
+
operationType: OperationType.READ,
|
|
83
|
+
parameters: {}
|
|
84
|
+
},
|
|
85
|
+
'http://callback.example.com/response'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(isCommandMessage(command)).toBe(true);
|
|
89
|
+
expect(MessageValidator.validateCommandMessage(command)).toBe(true);
|
|
90
|
+
expect(command.priority).toBe(Priority.NORMAL);
|
|
91
|
+
expect(command.timeout).toBe(10000);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('高优先级命令', () => {
|
|
95
|
+
const command = MessageFactory.createCommandMessage(
|
|
96
|
+
'req-urgent',
|
|
97
|
+
'device001',
|
|
98
|
+
{
|
|
99
|
+
commandType: CommandType.SIMPLE,
|
|
100
|
+
commandCode: 'EMERGENCY_STOP',
|
|
101
|
+
deviceType: 'controller',
|
|
102
|
+
operationType: OperationType.WRITE,
|
|
103
|
+
parameters: {}
|
|
104
|
+
},
|
|
105
|
+
'http://callback.example.com/response',
|
|
106
|
+
{
|
|
107
|
+
priority: Priority.CRITICAL,
|
|
108
|
+
timeout: 5000
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(command.priority).toBe(Priority.CRITICAL);
|
|
113
|
+
expect(command.timeout).toBe(5000);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Gateway 特殊命令', () => {
|
|
118
|
+
test('查询设备状态命令', () => {
|
|
119
|
+
const query = GatewayUtils.createDeviceStatusQuery('device001', 'query-001');
|
|
120
|
+
|
|
121
|
+
expect(query.targetClientId).toBe(GATEWAY_SITE_ID);
|
|
122
|
+
expect(query.command.commandCode).toBe('GET_DEVICE_STATUS');
|
|
123
|
+
expect(query.command.operationType).toBe('read');
|
|
124
|
+
expect(query.command.parameters.targetClientId).toBe('device001');
|
|
125
|
+
expect(GatewayUtils.isGatewayCommand(query)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('列出设备命令', () => {
|
|
129
|
+
const listCmd = GatewayUtils.createListDevicesCommand({
|
|
130
|
+
status: 'online',
|
|
131
|
+
clientType: 'device'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(listCmd.clientId).toBe(GATEWAY_SITE_ID);
|
|
135
|
+
expect(listCmd.command.commandCode).toBe('LIST_DEVICES');
|
|
136
|
+
expect(listCmd.command.parameters.filter).toEqual({
|
|
137
|
+
status: 'online',
|
|
138
|
+
clientType: 'device'
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
test('命令链', () => {
|
|
144
|
+
const chainCmd = GatewayUtils.createCommandChain(
|
|
145
|
+
'device001',
|
|
146
|
+
[
|
|
147
|
+
{ commandCode: 'STOP', parameters: {} },
|
|
148
|
+
{ commandCode: 'RESET', parameters: {}, delay: 1000 },
|
|
149
|
+
{ commandCode: 'START', parameters: {} }
|
|
150
|
+
]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(chainCmd.commandChain).toHaveLength(3);
|
|
154
|
+
expect(chainCmd.commandChain![1].delay).toBe(1000);
|
|
155
|
+
expect(chainCmd.timeout).toBeGreaterThan(30000); // 默认超时应考虑延迟
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('消息签名', () => {
|
|
160
|
+
test('签名消息', () => {
|
|
161
|
+
const message = { type: 'command', requestRef: 'test', clientId: 'test' };
|
|
162
|
+
const signedMessage = GatewayUtils.signMessage(message, 'secret-key');
|
|
163
|
+
|
|
164
|
+
expect(signedMessage.signature).toBeDefined();
|
|
165
|
+
expect(signedMessage.signatureMethod).toBe('sha256');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('验证签名', () => {
|
|
169
|
+
const message = { type: 'command', requestRef: 'test', clientId: 'test' };
|
|
170
|
+
const signedMessage = GatewayUtils.signMessage(message, 'secret-key');
|
|
171
|
+
|
|
172
|
+
const isValid = GatewayUtils.verifyMessageSignature(signedMessage, 'secret-key');
|
|
173
|
+
expect(isValid).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('心跳消息', () => {
|
|
178
|
+
test('创建心跳消息', () => {
|
|
179
|
+
const heartbeat = MessageFactory.createHeartbeatMessage('device001', 12345);
|
|
180
|
+
|
|
181
|
+
expect(heartbeat.type).toBe(MessageType.HEARTBEAT);
|
|
182
|
+
expect(heartbeat.clientId).toBe('device001');
|
|
183
|
+
expect(heartbeat.sequence).toBe(12345);
|
|
184
|
+
expect(heartbeat.timestamp).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('错误处理', () => {
|
|
189
|
+
test('创建错误消息', () => {
|
|
190
|
+
const error = MessageFactory.createErrorMessage(
|
|
191
|
+
'DEVICE_OFFLINE',
|
|
192
|
+
'Target device is not connected',
|
|
193
|
+
'medium',
|
|
194
|
+
'device',
|
|
195
|
+
{
|
|
196
|
+
context: {
|
|
197
|
+
clientId: 'device001',
|
|
198
|
+
requestRef: 'req-123'
|
|
199
|
+
},
|
|
200
|
+
retryable: true
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(error.type).toBe(MessageType.ERROR);
|
|
205
|
+
expect(error.code).toBe('DEVICE_OFFLINE');
|
|
206
|
+
expect(error.retryable).toBe(true);
|
|
207
|
+
expect(error.context?.clientId).toBe('device001');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('程序上传', () => {
|
|
212
|
+
test('创建程序上传消息', () => {
|
|
213
|
+
const program = MessageFactory.createProgramMessage(
|
|
214
|
+
'prog-123',
|
|
215
|
+
'device001',
|
|
216
|
+
{
|
|
217
|
+
device_id: 'device001',
|
|
218
|
+
programId: 'prog-v1.0.1',
|
|
219
|
+
downloadUrl: 'https://example.com/program.zip',
|
|
220
|
+
checksum: 'sha256:abcd1234',
|
|
221
|
+
hashAlgorithm: 'sha256'
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(program.type).toBe(MessageType.PROGRAM);
|
|
226
|
+
expect(program.command.commandCode).toBe('UPLOAD_PROGRAM');
|
|
227
|
+
expect(program.command.operationType).toBe('write');
|
|
228
|
+
expect(program.timeout).toBe(1800000); // 30分钟
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('协议版本兼容性', () => {
|
|
233
|
+
test('消息类型支持字符串和枚举', () => {
|
|
234
|
+
const stringType = { type: 'register', clientId: 'test' };
|
|
235
|
+
const enumType = { type: MessageType.REGISTER, clientId: 'test' };
|
|
236
|
+
|
|
237
|
+
expect(isRegisterMessage(stringType)).toBe(true);
|
|
238
|
+
expect(isRegisterMessage(enumType)).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('字段名兼容性(device_id vs deviceId)', () => {
|
|
242
|
+
const oldFormat = {
|
|
243
|
+
device_id: 'device001',
|
|
244
|
+
programId: 'test'
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const newFormat = {
|
|
248
|
+
deviceId: 'device001',
|
|
249
|
+
programId: 'test'
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// 两种格式都应该被接受
|
|
253
|
+
expect(oldFormat.device_id || oldFormat.deviceId).toBe('device001');
|
|
254
|
+
expect(newFormat.deviceId || newFormat.device_id).toBe('device001');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('协议设计原则验证', () => {
|
|
260
|
+
test('简单性优先 - 最小注册消息只需2个字段', () => {
|
|
261
|
+
const minimalRegister = {
|
|
262
|
+
type: 'register',
|
|
263
|
+
clientId: 'device001'
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const keys = Object.keys(minimalRegister);
|
|
267
|
+
expect(keys).toHaveLength(2);
|
|
268
|
+
expect(MessageValidator.validateRegisterMessage(minimalRegister)).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('渐进增强 - 可选字段不影响基础功能', () => {
|
|
272
|
+
const basic = { type: 'register', clientId: 'test' };
|
|
273
|
+
const enhanced = {
|
|
274
|
+
...basic,
|
|
275
|
+
clientType: 'device',
|
|
276
|
+
clientInfo: { version: '1.0' },
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
version: '2.0'
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// 两种格式都有效
|
|
282
|
+
expect(MessageValidator.validateRegisterMessage(basic)).toBe(true);
|
|
283
|
+
expect(MessageValidator.validateRegisterMessage(enhanced)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('明确语义 - 每个消息类型有唯一用途', () => {
|
|
287
|
+
const messageTypes = Object.values(MessageType);
|
|
288
|
+
const uniqueTypes = new Set(messageTypes);
|
|
289
|
+
|
|
290
|
+
// 没有重复的消息类型
|
|
291
|
+
expect(uniqueTypes.size).toBe(messageTypes.length);
|
|
292
|
+
|
|
293
|
+
// 每个类型都有明确的用途
|
|
294
|
+
expect(MessageType.REGISTER).not.toBe(MessageType.COMMAND);
|
|
295
|
+
expect(MessageType.HEARTBEAT).not.toBe(MessageType.PROGRAM);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 保持 TypeScript 实现与 AsyncAPI 文档同步的工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MessageType, ClientType, OperationType, Priority, CommandStatus } from './index';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 生成 AsyncAPI 的枚举定义
|
|
9
|
+
*/
|
|
10
|
+
export function generateAsyncApiEnums() {
|
|
11
|
+
const enums = {
|
|
12
|
+
MessageType: Object.values(MessageType),
|
|
13
|
+
ClientType: Object.values(ClientType),
|
|
14
|
+
OperationType: Object.values(OperationType),
|
|
15
|
+
Priority: Object.values(Priority),
|
|
16
|
+
CommandStatus: Object.values(CommandStatus)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return `
|
|
20
|
+
# AsyncAPI Enum Definitions
|
|
21
|
+
# Generated from TypeScript implementation
|
|
22
|
+
|
|
23
|
+
components:
|
|
24
|
+
schemas:
|
|
25
|
+
MessageType:
|
|
26
|
+
type: string
|
|
27
|
+
enum: ${JSON.stringify(enums.MessageType)}
|
|
28
|
+
|
|
29
|
+
ClientType:
|
|
30
|
+
type: string
|
|
31
|
+
enum: ${JSON.stringify(enums.ClientType)}
|
|
32
|
+
|
|
33
|
+
OperationType:
|
|
34
|
+
type: string
|
|
35
|
+
enum: ${JSON.stringify(enums.OperationType)}
|
|
36
|
+
|
|
37
|
+
Priority:
|
|
38
|
+
type: string
|
|
39
|
+
enum: ${JSON.stringify(enums.Priority)}
|
|
40
|
+
|
|
41
|
+
CommandStatus:
|
|
42
|
+
type: string
|
|
43
|
+
enum: ${JSON.stringify(enums.CommandStatus)}
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 生成消息示例用于 AsyncAPI 文档
|
|
49
|
+
*/
|
|
50
|
+
export function generateMessageExamples() {
|
|
51
|
+
return {
|
|
52
|
+
register_simple: {
|
|
53
|
+
type: 'register',
|
|
54
|
+
clientId: 'device001'
|
|
55
|
+
},
|
|
56
|
+
register_full: {
|
|
57
|
+
type: MessageType.REGISTER,
|
|
58
|
+
clientId: 'backend-server',
|
|
59
|
+
clientType: ClientType.BACKEND,
|
|
60
|
+
clientInfo: {
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
platform: 'nodejs',
|
|
63
|
+
capabilities: ['command', 'program']
|
|
64
|
+
},
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
version: '2.0'
|
|
67
|
+
},
|
|
68
|
+
command: {
|
|
69
|
+
type: MessageType.COMMAND,
|
|
70
|
+
requestRef: 'req-123',
|
|
71
|
+
clientId: 'device001',
|
|
72
|
+
command: {
|
|
73
|
+
commandCode: 'READ_STATUS',
|
|
74
|
+
deviceType: 'controller',
|
|
75
|
+
operationType: OperationType.READ,
|
|
76
|
+
parameters: { register: 'R001' }
|
|
77
|
+
},
|
|
78
|
+
priority: Priority.HIGH,
|
|
79
|
+
timeout: 10000,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
version: '2.0'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 强类型命令工厂
|
|
3
|
+
*
|
|
4
|
+
* 提供类型安全的命令创建和验证功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CommandTypeMap, SpecificCommand } from './command-types';
|
|
8
|
+
import type { CommandMessage, Priority } from './index';
|
|
9
|
+
import { MessageFactory, OperationType, CommandType } from './index';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 强类型命令消息工厂
|
|
13
|
+
*/
|
|
14
|
+
export class TypedCommandFactory {
|
|
15
|
+
/**
|
|
16
|
+
* 创建强类型命令消息
|
|
17
|
+
*/
|
|
18
|
+
static createTypedCommandMessage<T extends keyof CommandTypeMap>(
|
|
19
|
+
requestRef: string,
|
|
20
|
+
targetClientId: string,
|
|
21
|
+
commandCode: T,
|
|
22
|
+
commandProps: Omit<CommandTypeMap[T], 'commandCode'>,
|
|
23
|
+
options?: {
|
|
24
|
+
priority?: Priority;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
callback?: string;
|
|
27
|
+
}
|
|
28
|
+
): CommandMessage {
|
|
29
|
+
const command: CommandTypeMap[T] = {
|
|
30
|
+
commandCode,
|
|
31
|
+
...commandProps
|
|
32
|
+
} as CommandTypeMap[T];
|
|
33
|
+
|
|
34
|
+
return MessageFactory.createCommandMessage(
|
|
35
|
+
requestRef,
|
|
36
|
+
targetClientId,
|
|
37
|
+
command,
|
|
38
|
+
options?.callback || '', // callback 是必需的,如果未提供则使用空字符串
|
|
39
|
+
{
|
|
40
|
+
priority: options?.priority,
|
|
41
|
+
timeout: options?.timeout
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 示例:创建命令的工厂方法
|
|
48
|
+
*
|
|
49
|
+
* 实际使用时,应该为每个从 C# 生成的命令创建对应的工厂方法
|
|
50
|
+
* 例如: createLedSwitchCommand, createBlockPlayCommand 等
|
|
51
|
+
*/
|
|
52
|
+
static createExampleCommand(
|
|
53
|
+
requestRef: string,
|
|
54
|
+
targetClientId: string,
|
|
55
|
+
deviceId: number,
|
|
56
|
+
exampleField: string,
|
|
57
|
+
exampleValue: number,
|
|
58
|
+
operationType: OperationType = OperationType.WRITE,
|
|
59
|
+
options?: {
|
|
60
|
+
priority?: Priority;
|
|
61
|
+
timeout?: number;
|
|
62
|
+
callback?: string;
|
|
63
|
+
}
|
|
64
|
+
): CommandMessage {
|
|
65
|
+
return this.createTypedCommandMessage(
|
|
66
|
+
requestRef,
|
|
67
|
+
targetClientId,
|
|
68
|
+
'ExampleCommand',
|
|
69
|
+
{
|
|
70
|
+
commandType: CommandType.SIMPLE,
|
|
71
|
+
deviceType: 'example',
|
|
72
|
+
deviceId,
|
|
73
|
+
operationType,
|
|
74
|
+
parameters: { exampleField, exampleValue }
|
|
75
|
+
},
|
|
76
|
+
options
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 命令类型验证器
|
|
83
|
+
*/
|
|
84
|
+
export class CommandTypeValidator {
|
|
85
|
+
/**
|
|
86
|
+
* 验证命令是否符合强类型定义
|
|
87
|
+
*/
|
|
88
|
+
static validateCommand(command: any): { valid: boolean; errors: string[] } {
|
|
89
|
+
const errors: string[] = [];
|
|
90
|
+
|
|
91
|
+
if (!command) {
|
|
92
|
+
errors.push('Command is null or undefined');
|
|
93
|
+
return { valid: false, errors };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!command.commandCode || typeof command.commandCode !== 'string') {
|
|
97
|
+
errors.push('commandCode is required and must be a string');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!command.deviceType || typeof command.deviceType !== 'string') {
|
|
101
|
+
errors.push('deviceType is required and must be a string');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!command.operationType || !Object.values(OperationType).includes(command.operationType as OperationType)) {
|
|
105
|
+
errors.push('operationType must be READ or WRITE');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 根据具体的 commandCode 进行详细验证
|
|
109
|
+
switch (command.commandCode) {
|
|
110
|
+
case 'ExampleCommand':
|
|
111
|
+
this.validateExampleCommand(command, errors);
|
|
112
|
+
break;
|
|
113
|
+
// TODO: 实际使用时,为每个真实命令添加验证
|
|
114
|
+
// case 'LedSwitch':
|
|
115
|
+
// this.validateLedSwitchCommand(command, errors);
|
|
116
|
+
// break;
|
|
117
|
+
default:
|
|
118
|
+
// 对于未知的命令类型,只做基本验证
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { valid: errors.length === 0, errors };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private static validateExampleCommand(command: any, errors: string[]): void {
|
|
126
|
+
if (command.deviceType !== 'example') {
|
|
127
|
+
errors.push('ExampleCommand requires deviceType to be "example"');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (command.parameters) {
|
|
131
|
+
if (typeof command.parameters.exampleField !== 'string') {
|
|
132
|
+
errors.push('ExampleCommand parameters.exampleField must be a string');
|
|
133
|
+
}
|
|
134
|
+
if (typeof command.parameters.exampleValue !== 'number') {
|
|
135
|
+
errors.push('ExampleCommand parameters.exampleValue must be a number');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 命令类型转换器
|
|
143
|
+
*/
|
|
144
|
+
export class CommandTypeConverter {
|
|
145
|
+
/**
|
|
146
|
+
* 将通用命令转换为强类型命令
|
|
147
|
+
*/
|
|
148
|
+
static toTypedCommand(genericCommand: any): SpecificCommand | null {
|
|
149
|
+
const validation = CommandTypeValidator.validateCommand(genericCommand);
|
|
150
|
+
if (!validation.valid) {
|
|
151
|
+
console.warn('Command validation failed:', validation.errors);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 根据 commandCode 转换为具体的强类型
|
|
156
|
+
switch (genericCommand.commandCode) {
|
|
157
|
+
case 'ExampleCommand':
|
|
158
|
+
return genericCommand as import('./command-types').ExampleCommand;
|
|
159
|
+
// TODO: 实际使用时,为每个真实命令添加转换
|
|
160
|
+
// case 'LedSwitch':
|
|
161
|
+
// return genericCommand as import('./command-types').LedSwitchCommand;
|
|
162
|
+
default:
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 检查命令是否为强类型命令
|
|
169
|
+
*/
|
|
170
|
+
static isTypedCommand(command: any): command is SpecificCommand {
|
|
171
|
+
return command && this.toTypedCommand(command) !== null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 导出便捷的工厂函数
|
|
176
|
+
export const createExampleCommand = TypedCommandFactory.createExampleCommand.bind(TypedCommandFactory);
|
|
177
|
+
// TODO: 实际使用时,导出真实的命令工厂函数
|
|
178
|
+
// export const createLedSwitchCommand = TypedCommandFactory.createLedSwitchCommand.bind(TypedCommandFactory);
|
|
179
|
+
// export const createBlockPlayCommand = TypedCommandFactory.createBlockPlayCommand.bind(TypedCommandFactory);
|
|
180
|
+
|
|
181
|
+
// 导出验证函数
|
|
182
|
+
export const validateCommand = CommandTypeValidator.validateCommand.bind(CommandTypeValidator);
|
|
183
|
+
export const toTypedCommand = CommandTypeConverter.toTypedCommand.bind(CommandTypeConverter);
|