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