@tier0/sdk-v2 0.9.10 → 0.10.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/CHANGELOG.md +96 -0
- package/LICENSE +21 -21
- package/README.md +160 -24
- package/dist/cjs/client.d.ts +79 -3
- package/dist/cjs/client.js +4 -6
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/cloud-server.d.ts +33 -26
- package/dist/cjs/cloud-server.js +102 -100
- package/dist/cjs/cloud-server.js.map +1 -1
- package/dist/cjs/cloud-transport.d.ts +15 -20
- package/dist/cjs/cloud-transport.js +87 -68
- package/dist/cjs/cloud-transport.js.map +1 -1
- package/dist/cjs/data/bootstrap.js +3 -4
- package/dist/cjs/data/bootstrap.js.map +1 -1
- package/dist/cjs/data/call.d.ts +1 -3
- package/dist/cjs/data/call.js +8 -12
- package/dist/cjs/data/call.js.map +1 -1
- package/dist/cjs/data/client.d.ts +8 -7
- package/dist/cjs/data/client.js +24 -11
- package/dist/cjs/data/client.js.map +1 -1
- package/dist/cjs/data/collection.d.ts +3 -5
- package/dist/cjs/data/collection.js +3 -4
- package/dist/cjs/data/collection.js.map +1 -1
- package/dist/cjs/data/context.d.ts +5 -11
- package/dist/cjs/data/context.js +21 -36
- package/dist/cjs/data/context.js.map +1 -1
- package/dist/cjs/data/error.js +1 -1
- package/dist/cjs/data/error.js.map +1 -1
- package/dist/cjs/data/index.d.ts +5 -3
- package/dist/cjs/data/index.js +5 -5
- package/dist/cjs/data/index.js.map +1 -1
- package/dist/cjs/data/relay.d.ts +51 -0
- package/dist/cjs/data/relay.js +371 -0
- package/dist/cjs/data/relay.js.map +1 -0
- package/dist/cjs/data/subscribe.d.ts +22 -38
- package/dist/cjs/data/subscribe.js +22 -28
- package/dist/cjs/data/subscribe.js.map +1 -1
- package/dist/cjs/event-stream.d.ts +28 -0
- package/dist/cjs/event-stream.js +222 -0
- package/dist/cjs/event-stream.js.map +1 -0
- package/dist/cjs/mq/client.d.ts +32 -1
- package/dist/cjs/mq/client.js +404 -69
- package/dist/cjs/mq/client.js.map +1 -1
- package/dist/cjs/mq/index.d.ts +1 -0
- package/dist/cjs/mq/index.js +3 -1
- package/dist/cjs/mq/index.js.map +1 -1
- package/dist/cjs/mq/topic.d.ts +29 -0
- package/dist/cjs/mq/topic.js +48 -0
- package/dist/cjs/mq/topic.js.map +1 -0
- package/dist/cjs/mq/types.d.ts +21 -1
- package/dist/cjs/mq/types.js.map +1 -1
- package/dist/cjs/openapi/api.generated.d.ts +20 -0
- package/dist/cjs/openapi/api.generated.js +11 -0
- package/dist/cjs/openapi/api.generated.js.map +1 -1
- package/dist/cjs/openapi/client.d.ts +46 -0
- package/dist/cjs/openapi/client.js +158 -10
- package/dist/cjs/openapi/client.js.map +1 -1
- package/dist/cjs/openapi/codecs.generated.d.ts +74 -0
- package/dist/cjs/openapi/codecs.generated.js +1 -1
- package/dist/cjs/openapi/codecs.generated.js.map +1 -1
- package/dist/cjs/openapi/types.generated.d.ts +205 -1
- package/dist/cjs/openapi/types.generated.js.map +1 -1
- package/dist/cjs/page-runtime.d.ts +48 -0
- package/dist/cjs/page-runtime.js +74 -0
- package/dist/cjs/page-runtime.js.map +1 -0
- package/dist/cjs/platform-proxy.d.ts +17 -0
- package/dist/cjs/platform-proxy.js +52 -0
- package/dist/cjs/platform-proxy.js.map +1 -0
- package/dist/cjs/runtime-context.d.ts +2 -1
- package/dist/cjs/runtime-context.js +6 -2
- package/dist/cjs/runtime-context.js.map +1 -1
- package/dist/cjs/runtime-env.js.map +1 -1
- package/dist/cjs/source.d.ts +19 -1
- package/dist/cjs/source.js +9 -3
- package/dist/cjs/source.js.map +1 -1
- package/dist/cjs/stream.js +3 -2
- package/dist/cjs/stream.js.map +1 -1
- package/dist/cjs/streams/contract.generated.d.ts +7 -1
- package/dist/cjs/streams/contract.generated.js.map +1 -1
- package/dist/cjs/streams/messages.js +1 -10
- package/dist/cjs/streams/messages.js.map +1 -1
- package/dist/cjs/streams/requests.d.ts +3 -0
- package/dist/cjs/streams/requests.js +5 -2
- package/dist/cjs/streams/requests.js.map +1 -1
- package/dist/cjs/streams/transport.d.ts +1 -13
- package/dist/cjs/streams/transport.js +2 -18
- package/dist/cjs/streams/transport.js.map +1 -1
- package/dist/cjs/uns-v2/client.d.ts +1 -6
- package/dist/cjs/uns-v2/client.js +30 -6
- package/dist/cjs/uns-v2/client.js.map +1 -1
- package/dist/cjs/uns-v2/data-client.d.ts +5 -2
- package/dist/cjs/uns-v2/data-client.js +7 -4
- package/dist/cjs/uns-v2/data-client.js.map +1 -1
- package/dist/cjs/uns-v2/generated.d.ts +6783 -4655
- package/dist/cjs/uns-v2/generated.js +221 -65
- package/dist/cjs/uns-v2/generated.js.map +1 -1
- package/dist/cjs/uns-v2/sse.d.ts +7 -1
- package/dist/cjs/uns-v2/sse.js +12 -2
- package/dist/cjs/uns-v2/sse.js.map +1 -1
- package/dist/esm/client.d.ts +79 -3
- package/dist/esm/client.js +5 -7
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cloud-server.d.ts +33 -26
- package/dist/esm/cloud-server.js +99 -97
- package/dist/esm/cloud-server.js.map +1 -1
- package/dist/esm/cloud-transport.d.ts +15 -20
- package/dist/esm/cloud-transport.js +86 -67
- package/dist/esm/cloud-transport.js.map +1 -1
- package/dist/esm/data/bootstrap.js +3 -4
- package/dist/esm/data/bootstrap.js.map +1 -1
- package/dist/esm/data/call.d.ts +1 -3
- package/dist/esm/data/call.js +9 -13
- package/dist/esm/data/call.js.map +1 -1
- package/dist/esm/data/client.d.ts +8 -7
- package/dist/esm/data/client.js +22 -9
- package/dist/esm/data/client.js.map +1 -1
- package/dist/esm/data/collection.d.ts +3 -5
- package/dist/esm/data/collection.js +3 -4
- package/dist/esm/data/collection.js.map +1 -1
- package/dist/esm/data/context.d.ts +5 -11
- package/dist/esm/data/context.js +19 -33
- package/dist/esm/data/context.js.map +1 -1
- package/dist/esm/data/error.js +1 -1
- package/dist/esm/data/error.js.map +1 -1
- package/dist/esm/data/index.d.ts +5 -3
- package/dist/esm/data/index.js +3 -2
- package/dist/esm/data/index.js.map +1 -1
- package/dist/esm/data/relay.d.ts +51 -0
- package/dist/esm/data/relay.js +368 -0
- package/dist/esm/data/relay.js.map +1 -0
- package/dist/esm/data/subscribe.d.ts +22 -38
- package/dist/esm/data/subscribe.js +22 -27
- package/dist/esm/data/subscribe.js.map +1 -1
- package/dist/esm/event-stream.d.ts +28 -0
- package/dist/esm/event-stream.js +217 -0
- package/dist/esm/event-stream.js.map +1 -0
- package/dist/esm/mq/client.d.ts +32 -1
- package/dist/esm/mq/client.js +404 -69
- package/dist/esm/mq/client.js.map +1 -1
- package/dist/esm/mq/index.d.ts +1 -0
- package/dist/esm/mq/index.js +1 -0
- package/dist/esm/mq/index.js.map +1 -1
- package/dist/esm/mq/topic.d.ts +29 -0
- package/dist/esm/mq/topic.js +44 -0
- package/dist/esm/mq/topic.js.map +1 -0
- package/dist/esm/mq/types.d.ts +21 -1
- package/dist/esm/mq/types.js.map +1 -1
- package/dist/esm/openapi/api.generated.d.ts +20 -0
- package/dist/esm/openapi/api.generated.js +11 -0
- package/dist/esm/openapi/api.generated.js.map +1 -1
- package/dist/esm/openapi/client.d.ts +46 -0
- package/dist/esm/openapi/client.js +157 -10
- package/dist/esm/openapi/client.js.map +1 -1
- package/dist/esm/openapi/codecs.generated.d.ts +74 -0
- package/dist/esm/openapi/codecs.generated.js +1 -1
- package/dist/esm/openapi/codecs.generated.js.map +1 -1
- package/dist/esm/openapi/types.generated.d.ts +205 -1
- package/dist/esm/openapi/types.generated.js.map +1 -1
- package/dist/esm/page-runtime.d.ts +48 -0
- package/dist/esm/page-runtime.js +65 -0
- package/dist/esm/page-runtime.js.map +1 -0
- package/dist/esm/platform-proxy.d.ts +17 -0
- package/dist/esm/platform-proxy.js +47 -0
- package/dist/esm/platform-proxy.js.map +1 -0
- package/dist/esm/runtime-context.d.ts +2 -1
- package/dist/esm/runtime-context.js +6 -2
- package/dist/esm/runtime-context.js.map +1 -1
- package/dist/esm/runtime-env.js.map +1 -1
- package/dist/esm/source.d.ts +19 -1
- package/dist/esm/source.js +10 -4
- package/dist/esm/source.js.map +1 -1
- package/dist/esm/stream.js +4 -3
- package/dist/esm/stream.js.map +1 -1
- package/dist/esm/streams/contract.generated.d.ts +7 -1
- package/dist/esm/streams/contract.generated.js.map +1 -1
- package/dist/esm/streams/messages.js +1 -10
- package/dist/esm/streams/messages.js.map +1 -1
- package/dist/esm/streams/requests.d.ts +3 -0
- package/dist/esm/streams/requests.js +5 -2
- package/dist/esm/streams/requests.js.map +1 -1
- package/dist/esm/streams/transport.d.ts +1 -13
- package/dist/esm/streams/transport.js +1 -17
- package/dist/esm/streams/transport.js.map +1 -1
- package/dist/esm/uns-v2/client.d.ts +1 -6
- package/dist/esm/uns-v2/client.js +30 -6
- package/dist/esm/uns-v2/client.js.map +1 -1
- package/dist/esm/uns-v2/data-client.d.ts +5 -2
- package/dist/esm/uns-v2/data-client.js +8 -5
- package/dist/esm/uns-v2/data-client.js.map +1 -1
- package/dist/esm/uns-v2/generated.d.ts +6783 -4655
- package/dist/esm/uns-v2/generated.js +221 -65
- package/dist/esm/uns-v2/generated.js.map +1 -1
- package/dist/esm/uns-v2/sse.d.ts +7 -1
- package/dist/esm/uns-v2/sse.js +12 -2
- package/dist/esm/uns-v2/sse.js.map +1 -1
- package/package.json +25 -12
- package/skills/tier0-sdk-v2/SKILL.md +11 -5
- package/skills/tier0-sdk-v2/references/integration.md +14 -24
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/SKILL.md +11 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/consistency.md +6 -10
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/errors.md +20 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +46 -13
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/SKILL.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/references/app-members.md +5 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/SKILL.md +25 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/references/quickstart.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/references/send.md +6 -6
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/SKILL.md +4 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/changes.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/create.md +26 -36
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/delete.md +9 -23
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/protocols.md +12 -29
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/read.md +4 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +5 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/create.md +23 -9
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/deploy.md +14 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/http-server.md +4 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/read.md +4 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-streams/SKILL.md +12 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/SKILL.md +2 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/delete.md +22 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/read.md +5 -6
- package/dist/cjs/app-identity-contract.generated.d.ts +0 -22
- package/dist/cjs/app-identity-contract.generated.js +0 -51
- package/dist/cjs/app-identity-contract.generated.js.map +0 -1
- package/dist/cjs/runtime.d.ts +0 -13
- package/dist/cjs/runtime.js +0 -51
- package/dist/cjs/runtime.js.map +0 -1
- package/dist/esm/app-identity-contract.generated.d.ts +0 -22
- package/dist/esm/app-identity-contract.generated.js +0 -48
- package/dist/esm/app-identity-contract.generated.js.map +0 -1
- package/dist/esm/runtime.d.ts +0 -13
- package/dist/esm/runtime.js +0 -46
- package/dist/esm/runtime.js.map +0 -1
package/dist/cjs/mq/client.js
CHANGED
|
@@ -5,8 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Tier0MQClient = void 0;
|
|
7
7
|
const mqtt_1 = __importDefault(require("mqtt"));
|
|
8
|
+
const platform_proxy_js_1 = require("../platform-proxy.js");
|
|
8
9
|
const runtime_env_js_1 = require("../runtime-env.js");
|
|
9
10
|
const broker_js_1 = require("./broker.js");
|
|
11
|
+
const topic_js_1 = require("./topic.js");
|
|
12
|
+
/** Design 21.6: backoff ceiling of a client that never gives up. */
|
|
13
|
+
const RECONNECT_MAX_MS = 5 * 60_000;
|
|
14
|
+
/** The first reconnect after a drop is spread over this window. */
|
|
15
|
+
const FIRST_RECONNECT_MS = 500;
|
|
10
16
|
function parseWorkspaceIDFromApiKey(apiKey) {
|
|
11
17
|
// Match the backend's sk-<type>-ws<base36>_<secret> contract, not only
|
|
12
18
|
// service keys. Other workspace-encoded keys use the same MQTT identity.
|
|
@@ -30,6 +36,16 @@ function generateRandomString(length = 8) {
|
|
|
30
36
|
}
|
|
31
37
|
return result;
|
|
32
38
|
}
|
|
39
|
+
function snapshotConfig(config) {
|
|
40
|
+
if (!config)
|
|
41
|
+
return undefined;
|
|
42
|
+
const snapshot = { ...config };
|
|
43
|
+
// Preserve an explicitly supplied non-enumerable password as well.
|
|
44
|
+
if (Object.hasOwn(config, 'password') && !Object.hasOwn(snapshot, 'password'))
|
|
45
|
+
snapshot.password = config.password;
|
|
46
|
+
return snapshot;
|
|
47
|
+
}
|
|
48
|
+
const CANCELLED_BY_DISCONNECT = 'MQTT connection cancelled by disconnect';
|
|
33
49
|
class Tier0MQClient {
|
|
34
50
|
client = null;
|
|
35
51
|
config;
|
|
@@ -37,22 +53,44 @@ class Tier0MQClient {
|
|
|
37
53
|
explicitUsername;
|
|
38
54
|
listeners = {};
|
|
39
55
|
subscriptions = [];
|
|
56
|
+
// One broker subscription (including its pending SUBACK) per topic.
|
|
57
|
+
topicSubscriptions = new Map();
|
|
58
|
+
pendingTopicPackets = new Map();
|
|
59
|
+
sentTopicPackets = new Map();
|
|
40
60
|
connectingPromise = null;
|
|
61
|
+
rejectPendingConnect = null;
|
|
41
62
|
_connected = false;
|
|
63
|
+
/** Bumped by disconnect(); callbacks of an older generation are ignored. */
|
|
64
|
+
generation = 0;
|
|
65
|
+
/** Random clientId segment; a persistent session is found again through it. */
|
|
66
|
+
clientSuffix = generateRandomString(16);
|
|
67
|
+
reconnectTimer = null;
|
|
68
|
+
reconnectAttempts = 0;
|
|
69
|
+
connectWaiters = [];
|
|
42
70
|
constructor(config) {
|
|
71
|
+
config = snapshotConfig(config);
|
|
43
72
|
this.explicitClientId = config?.clientId !== undefined;
|
|
44
73
|
this.explicitUsername = config?.username !== undefined;
|
|
45
|
-
const envHost = (0, runtime_env_js_1.getEnvVar)('TIER0_MQTT_HOST');
|
|
74
|
+
const envHost = (0, runtime_env_js_1.getEnvVar)('TIER0_MQTT_URL') || (0, runtime_env_js_1.getEnvVar)('TIER0_MQTT_HOST');
|
|
46
75
|
const envPort = (0, runtime_env_js_1.getEnvVar)('TIER0_MQTT_PORT');
|
|
47
76
|
const host = config?.host || envHost || '';
|
|
48
77
|
const port = config?.port || parseInt(envPort || '8084') || 8084;
|
|
49
|
-
|
|
78
|
+
// An explicitly given password, even undefined, is never replaced by the environment key.
|
|
79
|
+
const explicitPassword = !!config && Object.hasOwn(config, 'password');
|
|
80
|
+
const password = explicitPassword
|
|
81
|
+
? config.password
|
|
82
|
+
: (this.tokenSource() ? '' : (0, runtime_env_js_1.getEnvVar)('TIER0_API_KEY')) || '';
|
|
50
83
|
// 从 apiKey 解析 workspaceID,自动生成 clientId 和 username
|
|
51
84
|
const workspaceID = config?.clientId && config?.username
|
|
52
85
|
? undefined
|
|
53
|
-
: parseWorkspaceIDFromApiKey(password);
|
|
86
|
+
: parseWorkspaceIDFromApiKey(password ?? '');
|
|
87
|
+
// In an App's runtime the platform injects the App's key as TIER0_API_KEY: Backend accepts that hosted key
|
|
88
|
+
// only with clientId `<ws>&h.<app>.<random>` (app = TIER0_APP_ID). Any other key keeps `<ws>&<random>`.
|
|
89
|
+
const appId = (0, runtime_env_js_1.getEnvVar)('TIER0_APP_ID')?.trim();
|
|
90
|
+
const hosted = !explicitPassword && !!password && config?.clientId === undefined && config?.username === undefined &&
|
|
91
|
+
!!appId && topic_js_1.APP_SEGMENT.test(appId);
|
|
54
92
|
const autoClientId = workspaceID
|
|
55
|
-
? `${workspaceID}&${generateRandomString(8)}`
|
|
93
|
+
? hosted ? `${workspaceID}&h.${appId}.${this.clientSuffix}` : `${workspaceID}&${generateRandomString(8)}`
|
|
56
94
|
: undefined;
|
|
57
95
|
const autoUsername = workspaceID
|
|
58
96
|
? `${workspaceID}&open`
|
|
@@ -66,12 +104,12 @@ class Tier0MQClient {
|
|
|
66
104
|
keepAlive: config?.keepAlive ?? 60,
|
|
67
105
|
reconnectPeriod: config?.reconnectPeriod ?? 5000,
|
|
68
106
|
connectTimeout: config?.connectTimeout ?? 30000,
|
|
69
|
-
clean: config?.clean ?? true,
|
|
70
107
|
...config,
|
|
108
|
+
clean: config?.clean ?? false,
|
|
71
109
|
};
|
|
72
110
|
}
|
|
73
|
-
|
|
74
|
-
const { host, port, secure } =
|
|
111
|
+
mqttUrlFor(config) {
|
|
112
|
+
const { host, port, secure } = config;
|
|
75
113
|
if (!host) {
|
|
76
114
|
throw new Error('MQTT host is required. Provide it via MQTTConfig or TIER0_MQTT_HOST environment variable.');
|
|
77
115
|
}
|
|
@@ -93,59 +131,234 @@ class Tier0MQClient {
|
|
|
93
131
|
const useSecure = secure ?? ((0, broker_js_1.isBrowserHttps)() || effectivePort === broker_js_1.DEFAULT_WSS_PORT);
|
|
94
132
|
return `${useSecure ? 'wss' : 'ws'}://${hostname}:${effectivePort}/mqtt`;
|
|
95
133
|
}
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Where the credential comes from; undefined when a fixed password is used.
|
|
136
|
+
* `proxy`: platform proxy mode, the node proxy puts in the App's key and
|
|
137
|
+
* the SDK sends none.
|
|
138
|
+
*/
|
|
139
|
+
tokenSource(config = this.config) {
|
|
140
|
+
if (config?.password)
|
|
141
|
+
return undefined;
|
|
142
|
+
return (0, platform_proxy_js_1.platformProxyMode)() ? 'proxy' : undefined;
|
|
143
|
+
}
|
|
144
|
+
/** Local preflight before a transport exists: address and credentials must be usable. */
|
|
145
|
+
validateConnectionConfig(config) {
|
|
146
|
+
void this.connectionParams(config);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* What the next connection presents. Through the node proxy the same
|
|
150
|
+
* identity reconnects, and the proxy puts in the App's current key.
|
|
151
|
+
*/
|
|
152
|
+
connectionParams(config = this.config) {
|
|
153
|
+
if (this.tokenSource(config) === 'proxy') {
|
|
154
|
+
const identity = (0, platform_proxy_js_1.platformIdentity)();
|
|
155
|
+
if (!identity) {
|
|
156
|
+
throw new Error('Tier0 SDK v2: TIER0_WORKSPACE_ID and TIER0_APP_ID are required for MQTT in the platform.');
|
|
157
|
+
}
|
|
158
|
+
// Only `mqtt://` from the platform goes over TCP to the proxy; an explicit host means WebSocket.
|
|
159
|
+
const envUrl = (0, runtime_env_js_1.getEnvVar)('TIER0_MQTT_URL')?.trim();
|
|
160
|
+
const tcp = envUrl !== undefined && /^mqtt:\/\//i.test(envUrl) && config.host === envUrl;
|
|
161
|
+
// The proxy accepts only `<ws>&open` and `<ws>&h.<app>.<random>`, and puts the App's key in as the password.
|
|
162
|
+
return {
|
|
163
|
+
url: tcp ? envUrl : this.mqttUrlFor(config),
|
|
164
|
+
username: config.username ?? `${identity.ws}&open`,
|
|
165
|
+
clientId: config.clientId ?? `${identity.ws}&h.${identity.app}.${this.clientSuffix}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const { username, clientId, password } = config;
|
|
169
|
+
const url = this.mqttUrlFor(config);
|
|
170
|
+
if (!password || !username || !clientId) {
|
|
171
|
+
throw new Error('Tier0 SDK v2: provide a workspace-encoded API key or explicit MQTT username, clientId and password.');
|
|
172
|
+
}
|
|
173
|
+
return { url, username, clientId, password };
|
|
174
|
+
}
|
|
175
|
+
// 内部确保已连接(懒连接)。已有客户端时只等它重连,绝不另建:
|
|
176
|
+
// 同一 clientId 的新旧客户端会互相顶掉。
|
|
177
|
+
async ensureConnected(onTransportCreationFailure) {
|
|
98
178
|
if (this._connected)
|
|
99
179
|
return;
|
|
100
180
|
if (this.connectingPromise)
|
|
101
181
|
return this.connectingPromise;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new Error('Tier0 SDK v2: provide a workspace-encoded API key or explicit MQTT username, clientId and password.');
|
|
182
|
+
if (this.client) {
|
|
183
|
+
return new Promise((resolve, reject) => this.connectWaiters.push({ resolve, reject }));
|
|
105
184
|
}
|
|
106
|
-
|
|
107
|
-
|
|
185
|
+
const pending = this.doConnect(this.connectionParams(), onTransportCreationFailure);
|
|
186
|
+
// mqtt.connect may throw synchronously before creating a client. Do not
|
|
187
|
+
// retain that already-failed attempt until the rejection microtask runs.
|
|
188
|
+
if (!this.client)
|
|
189
|
+
return pending;
|
|
190
|
+
this.connectingPromise = pending;
|
|
191
|
+
void pending.catch(() => {
|
|
192
|
+
if (this.connectingPromise === pending)
|
|
193
|
+
this.connectingPromise = null;
|
|
194
|
+
});
|
|
195
|
+
return pending;
|
|
196
|
+
}
|
|
197
|
+
clearReconnect() {
|
|
198
|
+
if (this.reconnectTimer !== null)
|
|
199
|
+
clearTimeout(this.reconnectTimer);
|
|
200
|
+
this.reconnectTimer = null;
|
|
108
201
|
}
|
|
109
|
-
|
|
202
|
+
reconnectDelay() {
|
|
203
|
+
const attempt = this.reconnectAttempts;
|
|
204
|
+
this.reconnectAttempts += 1;
|
|
205
|
+
if (attempt === 0)
|
|
206
|
+
return Math.random() * FIRST_RECONNECT_MS;
|
|
207
|
+
const base = this.config.reconnectPeriod ?? 5000;
|
|
208
|
+
const ceiling = Math.min(RECONNECT_MAX_MS, base * 2 ** (attempt - 1));
|
|
209
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
210
|
+
}
|
|
211
|
+
scheduleReconnect() {
|
|
212
|
+
if (!this.client || this._connected || this.reconnectTimer !== null)
|
|
213
|
+
return;
|
|
214
|
+
if (this.config.reconnectPeriod === 0)
|
|
215
|
+
return;
|
|
216
|
+
this.reconnectTimer = setTimeout(() => {
|
|
217
|
+
this.reconnectTimer = null;
|
|
218
|
+
this.reconnect();
|
|
219
|
+
}, this.reconnectDelay());
|
|
220
|
+
}
|
|
221
|
+
/** The same client: mqtt.js keeps its session and offline queue. */
|
|
222
|
+
reconnect() {
|
|
223
|
+
const client = this.client;
|
|
224
|
+
if (!client || this._connected)
|
|
225
|
+
return;
|
|
226
|
+
let params;
|
|
227
|
+
try {
|
|
228
|
+
params = this.connectionParams();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this.emit('error', error);
|
|
232
|
+
this.scheduleReconnect();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
client.options.username = params.username;
|
|
236
|
+
client.options.clientId = params.clientId;
|
|
237
|
+
if (params.password === undefined)
|
|
238
|
+
delete client.options.password;
|
|
239
|
+
else
|
|
240
|
+
client.options.password = params.password;
|
|
241
|
+
client.reconnect();
|
|
242
|
+
}
|
|
243
|
+
doConnect(params, onTransportCreationFailure) {
|
|
110
244
|
return new Promise((resolve, reject) => {
|
|
245
|
+
const generation = this.generation;
|
|
246
|
+
this.rejectPendingConnect = reject;
|
|
111
247
|
const options = {
|
|
112
|
-
clientId:
|
|
113
|
-
username:
|
|
114
|
-
password:
|
|
248
|
+
clientId: params.clientId,
|
|
249
|
+
username: params.username,
|
|
250
|
+
...(params.password === undefined ? {} : { password: params.password }),
|
|
115
251
|
keepalive: this.config.keepAlive,
|
|
116
|
-
|
|
252
|
+
// Reconnects are ours, with their own backoff.
|
|
253
|
+
reconnectPeriod: 0,
|
|
254
|
+
// Without it a refused CONNACK leaves the client disconnected for good.
|
|
255
|
+
reconnectOnConnackError: true,
|
|
117
256
|
connectTimeout: this.config.connectTimeout,
|
|
118
257
|
clean: this.config.clean,
|
|
258
|
+
// The SDK owns resubscription so every SUBACK reaches its callback.
|
|
259
|
+
resubscribe: false,
|
|
119
260
|
};
|
|
120
|
-
|
|
121
|
-
|
|
261
|
+
let client;
|
|
262
|
+
try {
|
|
263
|
+
client = mqtt_1.default.connect(params.url, options);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
this.rejectPendingConnect = null;
|
|
267
|
+
onTransportCreationFailure?.();
|
|
268
|
+
reject(err);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.client = client;
|
|
272
|
+
const current = () => this.client === client && this.generation === generation;
|
|
273
|
+
// mqtt.js 5.3 synthesizes the requested QoS if a malformed SUBACK has
|
|
274
|
+
// no grants, and can throw when it contains too many. Track wire packet
|
|
275
|
+
// ids so malformed acknowledgements never become apparent success.
|
|
276
|
+
client.on('packetsend', (packet) => {
|
|
277
|
+
if (!current() || packet.cmd !== 'subscribe' || packet.subscriptions.length !== 1)
|
|
278
|
+
return;
|
|
279
|
+
const topic = packet.subscriptions[0]?.topic;
|
|
280
|
+
if (!topic || typeof packet.messageId !== 'number')
|
|
281
|
+
return;
|
|
282
|
+
const queue = this.pendingTopicPackets.get(topic);
|
|
283
|
+
const attempt = queue?.shift();
|
|
284
|
+
if (queue?.length === 0)
|
|
285
|
+
this.pendingTopicPackets.delete(topic);
|
|
286
|
+
if (attempt)
|
|
287
|
+
this.sentTopicPackets.set(packet.messageId, attempt);
|
|
288
|
+
});
|
|
289
|
+
client.on('packetreceive', (packet) => {
|
|
290
|
+
if (!current() || packet.cmd !== 'suback' || typeof packet.messageId !== 'number')
|
|
291
|
+
return;
|
|
292
|
+
const attempt = this.sentTopicPackets.get(packet.messageId);
|
|
293
|
+
this.sentTopicPackets.delete(packet.messageId);
|
|
294
|
+
if (!attempt)
|
|
295
|
+
return;
|
|
296
|
+
if (!Array.isArray(packet.granted) || packet.granted.length !== 1 ||
|
|
297
|
+
(packet.granted[0] !== 0 && packet.granted[0] !== 1)) {
|
|
298
|
+
attempt.invalidSuback = true;
|
|
299
|
+
// Keep old mqtt.js from indexing past its single requested topic.
|
|
300
|
+
packet.granted = [128];
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
client.on('connect', () => {
|
|
304
|
+
if (!current())
|
|
305
|
+
return;
|
|
122
306
|
this._connected = true;
|
|
307
|
+
this.clearTopicState();
|
|
308
|
+
for (const sub of this.subscriptions)
|
|
309
|
+
sub.pendingConnect = false;
|
|
123
310
|
this.connectingPromise = null;
|
|
311
|
+
this.rejectPendingConnect = null;
|
|
312
|
+
this.clearReconnect();
|
|
313
|
+
this.reconnectAttempts = 0;
|
|
314
|
+
resolve();
|
|
124
315
|
this.emit('connect');
|
|
125
316
|
// 断连重连后自动恢复所有订阅
|
|
126
|
-
|
|
127
|
-
|
|
317
|
+
if (current())
|
|
318
|
+
this.resubscribeAll();
|
|
319
|
+
const waiters = this.connectWaiters;
|
|
320
|
+
this.connectWaiters = [];
|
|
321
|
+
waiters.forEach((waiter) => waiter.resolve());
|
|
128
322
|
});
|
|
129
|
-
|
|
130
|
-
|
|
323
|
+
client.on('error', (err) => {
|
|
324
|
+
if (!current())
|
|
325
|
+
return;
|
|
326
|
+
// Remove failed pending subscriptions before an error listener can
|
|
327
|
+
// synchronously cause this same client to reconnect and resubscribe.
|
|
328
|
+
this.subscriptions = this.subscriptions.filter((sub) => !sub.pendingConnect);
|
|
131
329
|
this.connectingPromise = null;
|
|
330
|
+
this.rejectPendingConnect = null;
|
|
132
331
|
reject(err);
|
|
332
|
+
this.emit('error', err);
|
|
133
333
|
});
|
|
134
|
-
|
|
334
|
+
client.on('disconnect', () => {
|
|
335
|
+
if (!current())
|
|
336
|
+
return;
|
|
135
337
|
this._connected = false;
|
|
338
|
+
this.clearTopicState();
|
|
136
339
|
this.emit('disconnect');
|
|
137
340
|
});
|
|
138
|
-
|
|
341
|
+
client.on('close', () => {
|
|
342
|
+
if (!current())
|
|
343
|
+
return;
|
|
139
344
|
this._connected = false;
|
|
345
|
+
this.clearTopicState();
|
|
346
|
+
this.scheduleReconnect();
|
|
140
347
|
});
|
|
141
|
-
|
|
348
|
+
client.on('message', (topic, payload, _packet) => {
|
|
349
|
+
if (!current())
|
|
350
|
+
return;
|
|
142
351
|
const payloadStr = payload.toString();
|
|
143
352
|
// 复制一份快照:handler 触发背压自动退订时会重建 this.subscriptions
|
|
144
353
|
this.subscriptions.slice().forEach((sub) => {
|
|
354
|
+
if (!current() || !this.subscriptions.includes(sub))
|
|
355
|
+
return;
|
|
145
356
|
if (this.topicMatch(sub.topic, topic)) {
|
|
146
357
|
try {
|
|
147
358
|
// 公开类型为 void 返回(兼容 async/简写 handler),运行时仅识别严格的 false
|
|
148
359
|
const accepted = sub.handler(topic, payloadStr);
|
|
360
|
+
if (!current() || !this.subscriptions.includes(sub))
|
|
361
|
+
return;
|
|
149
362
|
if (accepted === false) {
|
|
150
363
|
sub.backpressuredCount = (sub.backpressuredCount ?? 0) + 1;
|
|
151
364
|
const limit = this.config.maxBackpressuredDeliveries ?? 0;
|
|
@@ -162,7 +375,8 @@ class Tier0MQClient {
|
|
|
162
375
|
}
|
|
163
376
|
}
|
|
164
377
|
catch (e) {
|
|
165
|
-
|
|
378
|
+
if (current())
|
|
379
|
+
this.emit('error', e);
|
|
166
380
|
}
|
|
167
381
|
}
|
|
168
382
|
});
|
|
@@ -171,25 +385,85 @@ class Tier0MQClient {
|
|
|
171
385
|
}
|
|
172
386
|
// 显式连接(需要等待连接完成时使用)
|
|
173
387
|
async connect(config) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
388
|
+
const snapshot = snapshotConfig(config);
|
|
389
|
+
// An explicitly missing password must never reuse a previous or environment key.
|
|
390
|
+
if (snapshot && Object.hasOwn(snapshot, 'password') && snapshot.password === undefined) {
|
|
391
|
+
throw new TypeError('MQTT password was explicitly undefined; provide a credential or omit the password field.');
|
|
392
|
+
}
|
|
393
|
+
// Other undefined fields mean no override, not a request to erase settings.
|
|
394
|
+
const update = snapshot
|
|
395
|
+
? Object.fromEntries(Object.entries(snapshot).filter(([, value]) => value !== undefined))
|
|
396
|
+
: undefined;
|
|
397
|
+
if (this.client || this.connectingPromise) {
|
|
398
|
+
// mqtt.js snapshots its connection options. Changing our config while a
|
|
399
|
+
// client exists (connecting, connected or reconnecting) would leave the
|
|
400
|
+
// old credentials in use, including on reconnect. Refuse the change
|
|
401
|
+
// before mutating anything.
|
|
402
|
+
const connectionFields = [
|
|
403
|
+
'host', 'port', 'secure', 'clientId', 'username', 'password',
|
|
404
|
+
'keepAlive', 'reconnectPeriod', 'connectTimeout', 'clean',
|
|
405
|
+
];
|
|
406
|
+
if (update && connectionFields.some((field) => update[field] !== undefined && update[field] !== this.config[field])) {
|
|
407
|
+
throw new Error('MQTT connection settings cannot change while a client exists; disconnect and create a new client');
|
|
408
|
+
}
|
|
409
|
+
const generation = this.generation;
|
|
410
|
+
// A reconnecting client is waited for, never replaced: the SDK keeps reconnecting it.
|
|
411
|
+
await this.ensureConnected();
|
|
412
|
+
if (this.generation !== generation || !this.client || !this._connected) {
|
|
413
|
+
throw new Error('MQTT connection changed before connect completed');
|
|
414
|
+
}
|
|
415
|
+
if (update) {
|
|
416
|
+
this.config = { ...this.config, ...update };
|
|
417
|
+
if (update.clientId !== undefined)
|
|
418
|
+
this.explicitClientId = true;
|
|
419
|
+
if (update.username !== undefined)
|
|
420
|
+
this.explicitUsername = true;
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const previousConfig = this.config;
|
|
425
|
+
const previousExplicitClientId = this.explicitClientId;
|
|
426
|
+
const previousExplicitUsername = this.explicitUsername;
|
|
427
|
+
const candidate = update ? { ...previousConfig, ...update } : previousConfig;
|
|
428
|
+
const explicitClientId = previousExplicitClientId || update?.clientId !== undefined;
|
|
429
|
+
const explicitUsername = previousExplicitUsername || update?.username !== undefined;
|
|
430
|
+
if (update) {
|
|
431
|
+
const previousPassword = previousConfig.password;
|
|
181
432
|
// A connect-time key may belong to a different workspace. Preserve only
|
|
182
433
|
// caller-supplied identity fields, not values derived from the previous key.
|
|
183
|
-
if (
|
|
184
|
-
const workspaceID = parseWorkspaceIDFromApiKey(
|
|
185
|
-
if (!
|
|
186
|
-
|
|
434
|
+
if (update.password !== undefined && update.password !== previousPassword) {
|
|
435
|
+
const workspaceID = parseWorkspaceIDFromApiKey(update.password);
|
|
436
|
+
if (!explicitClientId) {
|
|
437
|
+
candidate.clientId = workspaceID ? workspaceID + '&' + generateRandomString(8) : undefined;
|
|
187
438
|
}
|
|
188
|
-
if (!
|
|
189
|
-
|
|
439
|
+
if (!explicitUsername)
|
|
440
|
+
candidate.username = workspaceID ? workspaceID + '&open' : undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this.validateConnectionConfig(candidate);
|
|
444
|
+
const generation = this.generation;
|
|
445
|
+
this.config = candidate;
|
|
446
|
+
this.explicitClientId = explicitClientId;
|
|
447
|
+
this.explicitUsername = explicitUsername;
|
|
448
|
+
const rollbackBeforeTransport = () => {
|
|
449
|
+
if (!this.client && this.generation === generation && this.config === candidate) {
|
|
450
|
+
this.config = previousConfig;
|
|
451
|
+
this.explicitClientId = previousExplicitClientId;
|
|
452
|
+
this.explicitUsername = previousExplicitUsername;
|
|
190
453
|
}
|
|
454
|
+
};
|
|
455
|
+
try {
|
|
456
|
+
await this.ensureConnected(rollbackBeforeTransport);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
// If no transport was created, the attempted settings have no owner.
|
|
460
|
+
// Do not roll back a newer generation or another caller's settings.
|
|
461
|
+
rollbackBeforeTransport();
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
if (this.generation !== generation || !this.client || !this._connected) {
|
|
465
|
+
throw new Error('MQTT connection changed before connect completed');
|
|
191
466
|
}
|
|
192
|
-
return this.ensureConnected();
|
|
193
467
|
}
|
|
194
468
|
// 订阅 topic,内部 qos 固定为 1,handler 接收 (topic, payload)
|
|
195
469
|
// 无需先调用 connect(),自动处理连接
|
|
@@ -199,20 +473,25 @@ class Tier0MQClient {
|
|
|
199
473
|
if (exists) {
|
|
200
474
|
return;
|
|
201
475
|
}
|
|
202
|
-
|
|
476
|
+
// Only a subscription made before the first connection completes is
|
|
477
|
+
// undone when that connection fails. One made while the SDK reconnects
|
|
478
|
+
// an existing client is kept and restored after the reconnect.
|
|
479
|
+
const pendingConnect = !this._connected && (!this.client || this.connectingPromise !== null);
|
|
480
|
+
const sub = { topic, handler, pendingConnect };
|
|
481
|
+
this.subscriptions.push(sub);
|
|
482
|
+
const generation = this.generation;
|
|
203
483
|
if (this._connected && this.client) {
|
|
204
|
-
this.
|
|
205
|
-
|
|
484
|
+
this.subscribeTopic(topic, this.client, generation);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
// 未连接时触发懒连接(已有客户端时等待它重连),连接成功后 resubscribeAll 会自动 subscribe
|
|
488
|
+
this.ensureConnected().catch((err) => {
|
|
489
|
+
if (this.generation === generation && this.subscriptions.includes(sub)) {
|
|
490
|
+
this.subscriptions = this.subscriptions.filter((item) => item !== sub);
|
|
206
491
|
this.emit('error', err);
|
|
207
|
-
// 订阅失败时回滚
|
|
208
|
-
this.subscriptions = this.subscriptions.filter((s) => !(s.topic === topic && s.handler === handler));
|
|
209
492
|
}
|
|
210
493
|
});
|
|
211
494
|
}
|
|
212
|
-
else {
|
|
213
|
-
// 未连接时触发懒连接,连接成功后 resubscribeAll 会自动 subscribe
|
|
214
|
-
this.ensureConnected().catch((err) => this.emit('error', err));
|
|
215
|
-
}
|
|
216
495
|
}
|
|
217
496
|
// 取消订阅:如果提供了 handler 则仅移除该 handler,否则移除该 topic 下所有 handler
|
|
218
497
|
unsubscribe(topic, handler) {
|
|
@@ -224,10 +503,15 @@ class Tier0MQClient {
|
|
|
224
503
|
}
|
|
225
504
|
if (!this.client)
|
|
226
505
|
return;
|
|
506
|
+
const client = this.client;
|
|
507
|
+
const generation = this.generation;
|
|
227
508
|
// 如果该 topic 下已无任何 handler,则向 broker 发送 unsubscribe
|
|
228
509
|
const stillHasHandler = this.subscriptions.some((s) => s.topic === topic);
|
|
229
510
|
if (!stillHasHandler) {
|
|
230
|
-
this.
|
|
511
|
+
this.topicSubscriptions.delete(topic);
|
|
512
|
+
client.unsubscribe(topic, (err) => {
|
|
513
|
+
if (this.client !== client || this.generation !== generation)
|
|
514
|
+
return;
|
|
231
515
|
if (err) {
|
|
232
516
|
this.emit('error', err);
|
|
233
517
|
}
|
|
@@ -236,23 +520,41 @@ class Tier0MQClient {
|
|
|
236
520
|
}
|
|
237
521
|
// 发布消息,无需先调用 connect(),内部自动处理连接
|
|
238
522
|
async publish(topic, payload, options) {
|
|
239
|
-
|
|
523
|
+
const generation = this.generation;
|
|
524
|
+
// A disconnected client queues the message and sends it after reconnecting.
|
|
525
|
+
if (!this.client)
|
|
526
|
+
await this.ensureConnected();
|
|
240
527
|
const message = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
528
|
+
const qos = (options?.qos ?? 0);
|
|
529
|
+
const retain = options?.retain ?? false;
|
|
530
|
+
const client = this.client;
|
|
531
|
+
if (this.generation !== generation || !client) {
|
|
532
|
+
throw new Error('MQTT connection changed before publish; message was not submitted');
|
|
533
|
+
}
|
|
534
|
+
client.publish(topic, message, { qos, retain });
|
|
245
535
|
}
|
|
246
536
|
disconnect() {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.client
|
|
250
|
-
this.emit('disconnect');
|
|
251
|
-
});
|
|
537
|
+
this.clearReconnect();
|
|
538
|
+
const generation = ++this.generation;
|
|
539
|
+
const client = this.client;
|
|
252
540
|
this.client = null;
|
|
253
541
|
this._connected = false;
|
|
254
542
|
this.connectingPromise = null;
|
|
543
|
+
const rejectPendingConnect = this.rejectPendingConnect;
|
|
544
|
+
this.rejectPendingConnect = null;
|
|
545
|
+
const waiters = this.connectWaiters;
|
|
546
|
+
this.connectWaiters = [];
|
|
255
547
|
this.subscriptions = [];
|
|
548
|
+
this.clearTopicState();
|
|
549
|
+
const cancelled = new Error(CANCELLED_BY_DISCONNECT);
|
|
550
|
+
rejectPendingConnect?.(cancelled);
|
|
551
|
+
waiters.forEach((waiter) => waiter.reject(cancelled));
|
|
552
|
+
if (!client)
|
|
553
|
+
return;
|
|
554
|
+
client.end(true, () => {
|
|
555
|
+
if (!this.client && this.generation === generation)
|
|
556
|
+
this.emit('disconnect');
|
|
557
|
+
});
|
|
256
558
|
}
|
|
257
559
|
on(event, handler) {
|
|
258
560
|
if (!this.listeners[event]) {
|
|
@@ -279,14 +581,47 @@ class Tier0MQClient {
|
|
|
279
581
|
resubscribeAll() {
|
|
280
582
|
if (!this.client)
|
|
281
583
|
return;
|
|
584
|
+
const client = this.client;
|
|
585
|
+
const generation = this.generation;
|
|
282
586
|
// 按 topic 去重,每个 topic 只订阅一次
|
|
283
587
|
const uniqueTopics = Array.from(new Set(this.subscriptions.map((s) => s.topic)));
|
|
284
|
-
uniqueTopics.forEach((topic) =>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
588
|
+
uniqueTopics.forEach((topic) => this.subscribeTopic(topic, client, generation));
|
|
589
|
+
}
|
|
590
|
+
clearTopicState() {
|
|
591
|
+
this.topicSubscriptions.clear();
|
|
592
|
+
this.pendingTopicPackets.clear();
|
|
593
|
+
this.sentTopicPackets.clear();
|
|
594
|
+
}
|
|
595
|
+
subscribeTopic(topic, client, generation) {
|
|
596
|
+
if (this.client !== client || this.generation !== generation || !this._connected ||
|
|
597
|
+
this.topicSubscriptions.has(topic) || !this.subscriptions.some((sub) => sub.topic === topic))
|
|
598
|
+
return;
|
|
599
|
+
const attempt = {};
|
|
600
|
+
this.topicSubscriptions.set(topic, attempt);
|
|
601
|
+
const pending = this.pendingTopicPackets.get(topic) ?? [];
|
|
602
|
+
pending.push(attempt);
|
|
603
|
+
this.pendingTopicPackets.set(topic, pending);
|
|
604
|
+
client.subscribe(topic, { qos: 1 }, (err, granted) => {
|
|
605
|
+
const queue = this.pendingTopicPackets.get(topic);
|
|
606
|
+
if (queue) {
|
|
607
|
+
const index = queue.indexOf(attempt);
|
|
608
|
+
if (index >= 0)
|
|
609
|
+
queue.splice(index, 1);
|
|
610
|
+
if (!queue.length)
|
|
611
|
+
this.pendingTopicPackets.delete(topic);
|
|
612
|
+
}
|
|
613
|
+
if (this.client !== client || this.generation !== generation || this.topicSubscriptions.get(topic) !== attempt)
|
|
614
|
+
return;
|
|
615
|
+
// mqtt.js 5.3 reports SUBACK failure codes in granted with err=null.
|
|
616
|
+
// A QoS 1 request may only be granted QoS 0 or 1, for this one topic.
|
|
617
|
+
const accepted = Array.isArray(granted) && granted.length === 1 &&
|
|
618
|
+
granted[0]?.topic === topic && (granted[0]?.qos === 0 || granted[0]?.qos === 1);
|
|
619
|
+
if (err || attempt.invalidSuback || !accepted) {
|
|
620
|
+
this.topicSubscriptions.delete(topic);
|
|
621
|
+
// Every handler shares this broker subscription and its SUBACK.
|
|
622
|
+
this.subscriptions = this.subscriptions.filter((sub) => sub.topic !== topic);
|
|
623
|
+
this.emit('error', new Error(`MQTT subscription failed for topic ${topic}`, { cause: err ?? undefined }));
|
|
624
|
+
}
|
|
290
625
|
});
|
|
291
626
|
}
|
|
292
627
|
// 判断接收到的 topic 是否匹配订阅的 topic(支持通配符 # 和 +)
|