@tier0/sdk-v2 0.1.0 → 0.2.2

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.
Files changed (170) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -11
  3. package/dist/cjs/app-identity-contract.generated.js.map +1 -1
  4. package/dist/cjs/capabilities.js.map +1 -1
  5. package/dist/cjs/cloud-server.js.map +1 -1
  6. package/dist/cjs/cloud-transport.d.ts +21 -0
  7. package/dist/cjs/cloud-transport.js +81 -1
  8. package/dist/cjs/cloud-transport.js.map +1 -1
  9. package/dist/cjs/controls.js.map +1 -1
  10. package/dist/cjs/data/bootstrap.d.ts +4 -0
  11. package/dist/cjs/data/bootstrap.js +15 -6
  12. package/dist/cjs/data/bootstrap.js.map +1 -1
  13. package/dist/cjs/data/call.d.ts +26 -9
  14. package/dist/cjs/data/call.js +179 -14
  15. package/dist/cjs/data/call.js.map +1 -1
  16. package/dist/cjs/data/client.d.ts +44 -2
  17. package/dist/cjs/data/client.js +87 -4
  18. package/dist/cjs/data/client.js.map +1 -1
  19. package/dist/cjs/data/collection.d.ts +151 -14
  20. package/dist/cjs/data/collection.js +213 -50
  21. package/dist/cjs/data/collection.js.map +1 -1
  22. package/dist/cjs/data/context.d.ts +4 -2
  23. package/dist/cjs/data/context.js +14 -3
  24. package/dist/cjs/data/context.js.map +1 -1
  25. package/dist/cjs/data/define.d.ts +9 -1
  26. package/dist/cjs/data/define.js.map +1 -1
  27. package/dist/cjs/data/error.d.ts +30 -12
  28. package/dist/cjs/data/error.js +61 -63
  29. package/dist/cjs/data/error.js.map +1 -1
  30. package/dist/cjs/data/index.d.ts +6 -3
  31. package/dist/cjs/data/index.js +7 -1
  32. package/dist/cjs/data/index.js.map +1 -1
  33. package/dist/cjs/data/schema.d.ts +7 -1
  34. package/dist/cjs/data/schema.js +54 -4
  35. package/dist/cjs/data/schema.js.map +1 -1
  36. package/dist/cjs/data/subscribe.d.ts +108 -0
  37. package/dist/cjs/data/subscribe.js +270 -0
  38. package/dist/cjs/data/subscribe.js.map +1 -0
  39. package/dist/cjs/mq/broker.js.map +1 -1
  40. package/dist/cjs/mq/client.js.map +1 -1
  41. package/dist/cjs/mq/index.js.map +1 -1
  42. package/dist/cjs/mq/types.js.map +1 -1
  43. package/dist/cjs/runtime-context.js.map +1 -1
  44. package/dist/cjs/runtime-env.js.map +1 -1
  45. package/dist/cjs/uns-v2/client.d.ts +90 -12
  46. package/dist/cjs/uns-v2/client.js +238 -63
  47. package/dist/cjs/uns-v2/client.js.map +1 -1
  48. package/dist/cjs/uns-v2/codec.js.map +1 -1
  49. package/dist/cjs/uns-v2/generated.d.ts +9619 -6923
  50. package/dist/cjs/uns-v2/generated.js +124 -119
  51. package/dist/cjs/uns-v2/generated.js.map +1 -1
  52. package/dist/cjs/uns-v2/index.js.map +1 -1
  53. package/dist/cjs/uns-v2/sse.d.ts +20 -0
  54. package/dist/cjs/uns-v2/sse.js +65 -0
  55. package/dist/cjs/uns-v2/sse.js.map +1 -0
  56. package/dist/esm/app-identity-contract.generated.js.map +1 -1
  57. package/dist/esm/capabilities.js.map +1 -1
  58. package/dist/esm/cloud-server.js.map +1 -1
  59. package/dist/esm/cloud-transport.d.ts +21 -0
  60. package/dist/esm/cloud-transport.js +81 -1
  61. package/dist/esm/cloud-transport.js.map +1 -1
  62. package/dist/esm/controls.js.map +1 -1
  63. package/dist/esm/data/bootstrap.d.ts +4 -0
  64. package/dist/esm/data/bootstrap.js +14 -5
  65. package/dist/esm/data/bootstrap.js.map +1 -1
  66. package/dist/esm/data/call.d.ts +26 -9
  67. package/dist/esm/data/call.js +177 -13
  68. package/dist/esm/data/call.js.map +1 -1
  69. package/dist/esm/data/client.d.ts +44 -2
  70. package/dist/esm/data/client.js +89 -7
  71. package/dist/esm/data/client.js.map +1 -1
  72. package/dist/esm/data/collection.d.ts +151 -14
  73. package/dist/esm/data/collection.js +212 -49
  74. package/dist/esm/data/collection.js.map +1 -1
  75. package/dist/esm/data/context.d.ts +4 -2
  76. package/dist/esm/data/context.js +13 -3
  77. package/dist/esm/data/context.js.map +1 -1
  78. package/dist/esm/data/define.d.ts +9 -1
  79. package/dist/esm/data/define.js.map +1 -1
  80. package/dist/esm/data/error.d.ts +30 -12
  81. package/dist/esm/data/error.js +61 -63
  82. package/dist/esm/data/error.js.map +1 -1
  83. package/dist/esm/data/index.d.ts +6 -3
  84. package/dist/esm/data/index.js +3 -1
  85. package/dist/esm/data/index.js.map +1 -1
  86. package/dist/esm/data/schema.d.ts +7 -1
  87. package/dist/esm/data/schema.js +54 -4
  88. package/dist/esm/data/schema.js.map +1 -1
  89. package/dist/esm/data/subscribe.d.ts +108 -0
  90. package/dist/esm/data/subscribe.js +265 -0
  91. package/dist/esm/data/subscribe.js.map +1 -0
  92. package/dist/esm/mq/broker.js.map +1 -1
  93. package/dist/esm/mq/client.js.map +1 -1
  94. package/dist/esm/mq/index.js.map +1 -1
  95. package/dist/esm/mq/types.js.map +1 -1
  96. package/dist/esm/runtime-context.js.map +1 -1
  97. package/dist/esm/runtime-env.js.map +1 -1
  98. package/dist/esm/uns-v2/client.d.ts +90 -12
  99. package/dist/esm/uns-v2/client.js +236 -63
  100. package/dist/esm/uns-v2/client.js.map +1 -1
  101. package/dist/esm/uns-v2/codec.js.map +1 -1
  102. package/dist/esm/uns-v2/generated.d.ts +9619 -6923
  103. package/dist/esm/uns-v2/generated.js +123 -118
  104. package/dist/esm/uns-v2/generated.js.map +1 -1
  105. package/dist/esm/uns-v2/index.js.map +1 -1
  106. package/dist/esm/uns-v2/sse.d.ts +20 -0
  107. package/dist/esm/uns-v2/sse.js +60 -0
  108. package/dist/esm/uns-v2/sse.js.map +1 -0
  109. package/package.json +29 -16
  110. package/skills/tier0-sdk-v2/SKILL.md +14 -14
  111. package/skills/{tier0-sdk-v2-data → tier0-sdk-v2/tier0-sdk-v2-data}/SKILL.md +3 -2
  112. package/skills/{tier0-sdk-v2-data → tier0-sdk-v2/tier0-sdk-v2-data}/references/consistency.md +3 -3
  113. package/skills/{tier0-sdk-v2-data → tier0-sdk-v2/tier0-sdk-v2-data}/references/errors.md +23 -10
  114. package/skills/{tier0-sdk-v2-data → tier0-sdk-v2/tier0-sdk-v2-data}/references/modeling.md +36 -0
  115. package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +172 -0
  116. package/skills/{tier0-sdk-v2-mq → tier0-sdk-v2/tier0-sdk-v2-mq}/references/quickstart.md +1 -1
  117. package/skills/{tier0-sdk-v2-source → tier0-sdk-v2/tier0-sdk-v2-source}/SKILL.md +3 -3
  118. package/skills/{tier0-sdk-v2-source → tier0-sdk-v2/tier0-sdk-v2-source}/references/create.md +3 -6
  119. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/delete.md +39 -0
  120. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/state.md +36 -0
  121. package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +80 -0
  122. package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/SKILL.md +6 -6
  123. package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/references/create.md +15 -14
  124. package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/references/deploy.md +11 -9
  125. package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/references/lifecycle.md +2 -1
  126. package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/references/update.md +5 -6
  127. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/SKILL.md +55 -0
  128. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +57 -0
  129. package/skills/{tier0-sdk-v2-uns → tier0-sdk-v2/tier0-sdk-v2-uns}/references/data-workflows.md +3 -7
  130. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/delete.md +53 -0
  131. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/namespace.md +38 -0
  132. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/read.md +34 -0
  133. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/restore.md +39 -0
  134. package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/update.md +62 -0
  135. package/skills/tier0-sdk-v2-data/references/read-write.md +0 -72
  136. package/skills/tier0-sdk-v2-source/references/delete.md +0 -47
  137. package/skills/tier0-sdk-v2-source/references/state.md +0 -37
  138. package/skills/tier0-sdk-v2-source/references/update.md +0 -61
  139. package/skills/tier0-sdk-v2-uns/SKILL.md +0 -53
  140. package/skills/tier0-sdk-v2-uns/references/create.md +0 -85
  141. package/skills/tier0-sdk-v2-uns/references/delete.md +0 -69
  142. package/skills/tier0-sdk-v2-uns/references/namespace.md +0 -75
  143. package/skills/tier0-sdk-v2-uns/references/read.md +0 -32
  144. package/skills/tier0-sdk-v2-uns/references/restore.md +0 -41
  145. package/skills/tier0-sdk-v2-uns/references/update.md +0 -86
  146. /package/skills/{tier0-sdk-v2-agent → tier0-sdk-v2/tier0-sdk-v2-agent}/SKILL.md +0 -0
  147. /package/skills/{tier0-sdk-v2-agent → tier0-sdk-v2/tier0-sdk-v2-agent}/references/results.md +0 -0
  148. /package/skills/{tier0-sdk-v2-controls → tier0-sdk-v2/tier0-sdk-v2-controls}/SKILL.md +0 -0
  149. /package/skills/{tier0-sdk-v2-data → tier0-sdk-v2/tier0-sdk-v2-data}/references/schema-evolution.md +0 -0
  150. /package/skills/{tier0-sdk-v2-files → tier0-sdk-v2/tier0-sdk-v2-files}/SKILL.md +0 -0
  151. /package/skills/{tier0-sdk-v2-files → tier0-sdk-v2/tier0-sdk-v2-files}/references/delete.md +0 -0
  152. /package/skills/{tier0-sdk-v2-files → tier0-sdk-v2/tier0-sdk-v2-files}/references/download.md +0 -0
  153. /package/skills/{tier0-sdk-v2-files → tier0-sdk-v2/tier0-sdk-v2-files}/references/multipart.md +0 -0
  154. /package/skills/{tier0-sdk-v2-files → tier0-sdk-v2/tier0-sdk-v2-files}/references/upload.md +0 -0
  155. /package/skills/{tier0-sdk-v2-files → tier0-sdk-v2/tier0-sdk-v2-files}/references/url.md +0 -0
  156. /package/skills/{tier0-sdk-v2-members → tier0-sdk-v2/tier0-sdk-v2-members}/SKILL.md +0 -0
  157. /package/skills/{tier0-sdk-v2-members → tier0-sdk-v2/tier0-sdk-v2-members}/references/app-members.md +0 -0
  158. /package/skills/{tier0-sdk-v2-members → tier0-sdk-v2/tier0-sdk-v2-members}/references/platform-members.md +0 -0
  159. /package/skills/{tier0-sdk-v2-mq → tier0-sdk-v2/tier0-sdk-v2-mq}/SKILL.md +0 -0
  160. /package/skills/{tier0-sdk-v2-notifications → tier0-sdk-v2/tier0-sdk-v2-notifications}/SKILL.md +0 -0
  161. /package/skills/{tier0-sdk-v2-notifications → tier0-sdk-v2/tier0-sdk-v2-notifications}/references/get.md +0 -0
  162. /package/skills/{tier0-sdk-v2-notifications → tier0-sdk-v2/tier0-sdk-v2-notifications}/references/send.md +0 -0
  163. /package/skills/{tier0-sdk-v2-source → tier0-sdk-v2/tier0-sdk-v2-source}/references/changes.md +0 -0
  164. /package/skills/{tier0-sdk-v2-source → tier0-sdk-v2/tier0-sdk-v2-source}/references/protocols.md +0 -0
  165. /package/skills/{tier0-sdk-v2-source → tier0-sdk-v2/tier0-sdk-v2-source}/references/read.md +0 -0
  166. /package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/references/delete.md +0 -0
  167. /package/skills/{tier0-sdk-v2-stream → tier0-sdk-v2/tier0-sdk-v2-stream}/references/read.md +0 -0
  168. /package/skills/{tier0-sdk-v2-system → tier0-sdk-v2/tier0-sdk-v2-system}/SKILL.md +0 -0
  169. /package/skills/{tier0-sdk-v2-system → tier0-sdk-v2/tier0-sdk-v2-system}/references/info.md +0 -0
  170. /package/skills/{tier0-sdk-v2-system → tier0-sdk-v2/tier0-sdk-v2-system}/references/whoami.md +0 -0
@@ -0,0 +1,265 @@
1
+ /**
2
+ * `data.subscribe`: committed record changes of declared collections over
3
+ * UNS `records:subscribe` (server-sent events), as one async iterable that
4
+ * outlives single connections.
5
+ *
6
+ * - Reconnects with `Last-Event-ID` when the stream ends, breaks, or stays
7
+ * silent for `idleTimeoutMs` (UNS sends a heartbeat every 15 s). A stream
8
+ * that dies young without a change backs off, so a server dropping every
9
+ * connection is not hammered.
10
+ * - Failures marked `retry.safe` (429 over the subscription limit, 503,
11
+ * `APP_NOT_READY` from ensure …) are retried after `retry.afterMs` or a
12
+ * backoff of up to 30 s, without end; any other failure ends the
13
+ * iteration with that DataError.
14
+ * - `reset` is passed on: `CURSOR_EXPIRED` (the position was cleaned up after
15
+ * 24 h, cannot be decoded after a key rotation, or the tenant database
16
+ * moved) or `SCOPE_CHANGED` (access changed, e.g. a collection is no longer
17
+ * readable). The consumer reloads what it shows; the next connection
18
+ * starts from now, without a resume position. A collection recreated
19
+ * under a new ID (see bootstrap self-healing) yields `COLLECTION_RECREATED`.
20
+ * - UNS delivers changes in write-transaction order, not commit order, so
21
+ * revisions of one record may arrive out of order. A non-delete change at
22
+ * or below the revision already passed on for its record is dropped; a
23
+ * `delete` always passes, a late one too, and forgets the record's
24
+ * revision, since a record written again under the same ID may start
25
+ * over at revision 1.
26
+ * - Changes in collections with employee data rules (`restricted`) come as
27
+ * collection-level notices unless `restricted: "record"` is asked for.
28
+ * - Aborting `signal` or leaving the loop ends the iteration without error.
29
+ *
30
+ * Events carry identities only, never record data; read the record to show it.
31
+ */
32
+ import { isStaleCollectionError } from './bootstrap.js';
33
+ import { assertEventId, UNS_V2_SUBSCRIBE_MAX_COLLECTIONS, UNS_V2_SUBSCRIBE_MAX_RECORD_IDS } from '../uns-v2/client.js';
34
+ import { DataError } from './error.js';
35
+ /** Three missed 15 s heartbeats. */
36
+ export const SUBSCRIBE_IDLE_TIMEOUT_MS = 45_000;
37
+ const RECONNECT_BASE_MS = 500;
38
+ const RECONNECT_MAX_MS = 30_000;
39
+ /** After a reset, the next connection waits at least this long. */
40
+ const RESET_PAUSE_MS = 1_000;
41
+ /** A connection alive this long counts as healthy even without changes. */
42
+ const HEALTHY_CONNECTION_MS = 30_000;
43
+ /** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */
44
+ const SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;
45
+ /**
46
+ * The event as an employee's browser may see it: a change in a collection
47
+ * with employee data rules becomes a collection-level notice without record
48
+ * ID or revision, so it does not reveal records the employee may not read.
49
+ * Other events pass unchanged.
50
+ */
51
+ export function redactRestricted(event) {
52
+ if (event.type !== 'change' || !event.restricted || event.change === 'collection')
53
+ return event;
54
+ const { eventId, collection, collectionId, occurredAt, restricted } = event;
55
+ return { type: 'change', eventId, collection, collectionId, occurredAt, restricted, change: 'collection' };
56
+ }
57
+ function reconnectDelay(failures, afterMs) {
58
+ if (afterMs !== undefined)
59
+ return Math.min(afterMs, RECONNECT_MAX_MS);
60
+ const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.max(0, failures - 1));
61
+ return ceiling / 2 + Math.random() * (ceiling / 2);
62
+ }
63
+ /**
64
+ * The highest revision passed on per record, so changes delivered out of
65
+ * order are dropped (UNS orders by write transaction, not by commit).
66
+ */
67
+ class SeenRevisions {
68
+ max;
69
+ revisions = new Map();
70
+ constructor(max) {
71
+ this.max = max;
72
+ }
73
+ /** Whether the change is new; remembers its revision, or forgets the record on delete. */
74
+ admit(collectionId, recordId, revision, change) {
75
+ const key = `${collectionId}\u0000${recordId}`;
76
+ const seen = this.revisions.get(key);
77
+ // A delete always passes, a late one too: the record may be written again under the same ID, from revision 1.
78
+ if (change === 'delete') {
79
+ this.revisions.delete(key);
80
+ return true;
81
+ }
82
+ if (seen !== undefined && revision <= seen)
83
+ return false;
84
+ this.revisions.delete(key);
85
+ this.revisions.set(key, revision);
86
+ if (this.revisions.size > this.max)
87
+ this.revisions.delete(this.revisions.keys().next().value);
88
+ return true;
89
+ }
90
+ clear() {
91
+ this.revisions.clear();
92
+ }
93
+ }
94
+ function pause(ms, signal) {
95
+ return new Promise((resolve) => {
96
+ if (signal.aborted)
97
+ return resolve();
98
+ const done = () => {
99
+ clearTimeout(timer);
100
+ signal.removeEventListener('abort', done);
101
+ resolve();
102
+ };
103
+ const timer = setTimeout(done, ms);
104
+ signal.addEventListener('abort', done, { once: true });
105
+ });
106
+ }
107
+ export function createSubscribe(runtime) {
108
+ return async function* subscribe(options = {}) {
109
+ const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];
110
+ if (definitions.length < 1 || definitions.length > UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {
111
+ throw new TypeError(`data.subscribe watches 1–${UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);
112
+ }
113
+ const recordIds = options.recordIds === undefined ? undefined : [...new Set(options.recordIds)];
114
+ if (recordIds && (recordIds.length < 1 || recordIds.length > UNS_V2_SUBSCRIBE_MAX_RECORD_IDS || definitions.length !== 1)) {
115
+ throw new TypeError(`data.subscribe takes 1–${UNS_V2_SUBSCRIBE_MAX_RECORD_IDS} recordIds, of exactly one collection.`);
116
+ }
117
+ if (options.lastEventId !== undefined)
118
+ assertEventId(options.lastEventId);
119
+ const redact = (options.restricted ?? 'collection') === 'collection';
120
+ const idleMs = options.idleTimeoutMs ?? SUBSCRIBE_IDLE_TIMEOUT_MS;
121
+ // The subject of the request that subscribed; an App-level subscription carries none.
122
+ const call = { dataSubject: runtime.subject() };
123
+ const stop = new AbortController();
124
+ const onAbort = () => stop.abort();
125
+ options.signal?.addEventListener('abort', onAbort, { once: true });
126
+ if (options.signal?.aborted)
127
+ stop.abort();
128
+ let lastEventId = options.lastEventId;
129
+ let failures = 0;
130
+ const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);
131
+ /** Waits, longer each time, before the next attempt. */
132
+ const backOff = async (afterMs) => {
133
+ failures += 1;
134
+ await pause(reconnectDelay(failures, afterMs), stop.signal);
135
+ };
136
+ try {
137
+ while (!stop.signal.aborted) {
138
+ let state;
139
+ try {
140
+ state = await runtime.ensure();
141
+ }
142
+ catch (error) {
143
+ // A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.
144
+ if (!(error instanceof DataError) || !error.retry.safe || stop.signal.aborted)
145
+ throw error;
146
+ await backOff(error.retry.afterMs);
147
+ continue;
148
+ }
149
+ const aliases = new Map();
150
+ for (const definition of definitions) {
151
+ const id = state.collectionIds.get(definition.alias.toLowerCase());
152
+ if (!id) {
153
+ throw new DataError({
154
+ message: `Collection "${definition.alias}" is unavailable: ensure did not return it (see the server log).`,
155
+ status: 503,
156
+ code: 'COLLECTION_UNAVAILABLE',
157
+ });
158
+ }
159
+ aliases.set(id, definition.alias);
160
+ }
161
+ const connection = new AbortController();
162
+ const closeConnection = () => connection.abort();
163
+ stop.signal.addEventListener('abort', closeConnection, { once: true });
164
+ let idle;
165
+ const arm = () => {
166
+ clearTimeout(idle);
167
+ idle = setTimeout(closeConnection, idleMs);
168
+ };
169
+ const openedAt = Date.now();
170
+ let healthy = false;
171
+ let reset = false;
172
+ try {
173
+ arm();
174
+ const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, { ...call, lastEventId, signal: connection.signal });
175
+ for await (const event of stream) {
176
+ arm();
177
+ if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
178
+ healthy = true;
179
+ if (event.type === 'heartbeat') {
180
+ // The connect heartbeat names the current position: resuming from it loses nothing.
181
+ if (event.eventId !== undefined)
182
+ lastEventId = event.eventId;
183
+ continue;
184
+ }
185
+ if (event.type === 'reset') {
186
+ lastEventId = undefined;
187
+ reset = true;
188
+ // The consumer rereads everything; revisions seen before mean nothing for what follows.
189
+ seen.clear();
190
+ yield event.message === undefined ? { type: 'reset', reason: event.reason } : { type: 'reset', reason: event.reason, message: event.message };
191
+ continue;
192
+ }
193
+ healthy = true;
194
+ // A dropped change is still a resume position.
195
+ lastEventId = event.eventId;
196
+ if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change))
197
+ continue;
198
+ const change = {
199
+ type: 'change',
200
+ eventId: event.eventId,
201
+ collection: aliases.get(event.collectionId) ?? event.collectionId,
202
+ collectionId: event.collectionId,
203
+ occurredAt: event.occurredAt,
204
+ restricted: event.restricted,
205
+ change: event.change,
206
+ recordId: event.recordId,
207
+ revision: event.revision,
208
+ };
209
+ yield redact ? redactRestricted(change) : change;
210
+ }
211
+ }
212
+ catch (error) {
213
+ if (stop.signal.aborted)
214
+ return;
215
+ if (!(error instanceof DataError))
216
+ throw error;
217
+ if (isStaleCollectionError(error)) {
218
+ let changed;
219
+ try {
220
+ changed = await runtime.rebuild(state, error);
221
+ }
222
+ catch (cause) {
223
+ if (!(cause instanceof DataError) || !cause.retry.safe)
224
+ throw cause;
225
+ await backOff(cause.retry.afterMs);
226
+ continue;
227
+ }
228
+ if (!changed)
229
+ throw error;
230
+ // Recreated collections have new IDs; their old cursor means nothing.
231
+ lastEventId = undefined;
232
+ seen.clear();
233
+ yield { type: 'reset', reason: 'COLLECTION_RECREATED' };
234
+ continue;
235
+ }
236
+ if (!error.retry.safe)
237
+ throw error;
238
+ await backOff(error.retry.afterMs);
239
+ continue;
240
+ }
241
+ finally {
242
+ clearTimeout(idle);
243
+ connection.abort();
244
+ stop.signal.removeEventListener('abort', closeConnection);
245
+ }
246
+ // The stream ended or went silent. Reconnect at once after a healthy connection, else back off.
247
+ if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
248
+ healthy = true;
249
+ if (healthy)
250
+ failures = 0;
251
+ else
252
+ failures += 1;
253
+ // A reset always waits a moment: a server resetting in a loop is not reconnected to in a loop.
254
+ const wait = Math.max(reset ? RESET_PAUSE_MS : 0, healthy ? 0 : reconnectDelay(failures, undefined));
255
+ if (wait > 0)
256
+ await pause(wait, stop.signal);
257
+ }
258
+ }
259
+ finally {
260
+ stop.abort();
261
+ options.signal?.removeEventListener('abort', onAbort);
262
+ }
263
+ };
264
+ }
265
+ //# sourceMappingURL=subscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../../src/data/subscribe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `data.subscribe`: committed record changes of declared collections over\n * UNS `records:subscribe` (server-sent events), as one async iterable that\n * outlives single connections.\n *\n * - Reconnects with `Last-Event-ID` when the stream ends, breaks, or stays\n * silent for `idleTimeoutMs` (UNS sends a heartbeat every 15 s). A stream\n * that dies young without a change backs off, so a server dropping every\n * connection is not hammered.\n * - Failures marked `retry.safe` (429 over the subscription limit, 503,\n * `APP_NOT_READY` from ensure …) are retried after `retry.afterMs` or a\n * backoff of up to 30 s, without end; any other failure ends the\n * iteration with that DataError.\n * - `reset` is passed on: `CURSOR_EXPIRED` (the position was cleaned up after\n * 24 h, cannot be decoded after a key rotation, or the tenant database\n * moved) or `SCOPE_CHANGED` (access changed, e.g. a collection is no longer\n * readable). The consumer reloads what it shows; the next connection\n * starts from now, without a resume position. A collection recreated\n * under a new ID (see bootstrap self-healing) yields `COLLECTION_RECREATED`.\n * - UNS delivers changes in write-transaction order, not commit order, so\n * revisions of one record may arrive out of order. A non-delete change at\n * or below the revision already passed on for its record is dropped; a\n * `delete` always passes, a late one too, and forgets the record's\n * revision, since a record written again under the same ID may start\n * over at revision 1.\n * - Changes in collections with employee data rules (`restricted`) come as\n * collection-level notices unless `restricted: \"record\"` is asked for.\n * - Aborting `signal` or leaving the loop ends the iteration without error.\n *\n * Events carry identities only, never record data; read the record to show it.\n */\nimport { isStaleCollectionError, type EnsureState } from './bootstrap.js';\nimport type { CallOptions, DataOpenStream } from './call.js';\nimport { assertEventId, UNS_V2_SUBSCRIBE_MAX_COLLECTIONS, UNS_V2_SUBSCRIBE_MAX_RECORD_IDS } from '../uns-v2/client.js';\nimport type { CollectionDefinition } from './define.js';\nimport { DataError } from './error.js';\n\n/** Three missed 15 s heartbeats. */\nexport const SUBSCRIBE_IDLE_TIMEOUT_MS = 45_000;\nconst RECONNECT_BASE_MS = 500;\nconst RECONNECT_MAX_MS = 30_000;\n/** After a reset, the next connection waits at least this long. */\nconst RESET_PAUSE_MS = 1_000;\n/** A connection alive this long counts as healthy even without changes. */\nconst HEALTHY_CONNECTION_MS = 30_000;\n/** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */\nconst SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;\n\nexport interface SubscribeOptions {\n /** Declarations or aliases to watch (at most 64); defaults to every declared collection. */\n collections?: readonly (CollectionDefinition | string)[];\n /** Only changes of these record IDs (1–100, duplicates ignored), when exactly one collection is watched. */\n recordIds?: readonly string[];\n /**\n * How changes in collections with employee data rules arrive.\n * `collection` (default): as a collection-level notice without record ID\n * or revision, safe to forward to any employee. `record`: the full event,\n * for server-side consumers that do not forward it to browsers, or that\n * redact per recipient with `redactRestricted`.\n */\n restricted?: 'collection' | 'record';\n /** Resume after this event (e.g. a browser's `Last-Event-ID`). */\n lastEventId?: string;\n signal?: AbortSignal;\n /** Reconnect when nothing, not even a heartbeat, arrives for this long. */\n idleTimeoutMs?: number;\n}\n\ninterface ChangeBase {\n type: 'change';\n eventId: string;\n /** Alias of the declared collection. */\n collection: string;\n collectionId: string;\n occurredAt: number;\n /**\n * The collection has employee data rules: whether an employee may see this\n * record depends on who they are. Such changes arrive as `CollectionChange`\n * unless the subscription asked for `restricted: \"record\"`.\n */\n restricted: boolean;\n}\n\n/**\n * One record committed (`record`) or deleted (`delete`) at `revision`. A\n * `record` change is never at or below a revision already passed on for the\n * same record since its last delete; a `delete` may be.\n */\nexport interface RecordChange extends ChangeBase {\n change: 'record' | 'delete';\n recordId: string;\n revision: number;\n}\n\n/** Something in the collection changed; which record is not disclosed. Reload through the data rules. */\nexport interface CollectionChange extends ChangeBase {\n change: 'collection';\n}\n\nexport type ChangeEvent = RecordChange | CollectionChange;\n/** `reason`: `CURSOR_EXPIRED`, `SCOPE_CHANGED` (from UNS) or `COLLECTION_RECREATED`. */\nexport type SubscriptionEvent = ChangeEvent | { type: 'reset'; reason: string; message?: string };\n\n/**\n * The event as an employee's browser may see it: a change in a collection\n * with employee data rules becomes a collection-level notice without record\n * ID or revision, so it does not reveal records the employee may not read.\n * Other events pass unchanged.\n */\nexport function redactRestricted<E extends SubscriptionEvent>(event: E): E | CollectionChange {\n if (event.type !== 'change' || !event.restricted || event.change === 'collection') return event;\n const { eventId, collection, collectionId, occurredAt, restricted } = event;\n return { type: 'change', eventId, collection, collectionId, occurredAt, restricted, change: 'collection' };\n}\n\nexport interface SubscribeRuntime {\n openStream: DataOpenStream;\n ensure(): Promise<EnsureState>;\n rebuild(before: EnsureState, cause: DataError): Promise<boolean>;\n lookup(target: CollectionDefinition | string): CollectionDefinition;\n declared: readonly CollectionDefinition[];\n subject(): string | undefined;\n}\n\nfunction reconnectDelay(failures: number, afterMs: number | undefined): number {\n if (afterMs !== undefined) return Math.min(afterMs, RECONNECT_MAX_MS);\n const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.max(0, failures - 1));\n return ceiling / 2 + Math.random() * (ceiling / 2);\n}\n\n/**\n * The highest revision passed on per record, so changes delivered out of\n * order are dropped (UNS orders by write transaction, not by commit).\n */\nclass SeenRevisions {\n private readonly revisions = new Map<string, number>();\n\n constructor(private readonly max: number) {}\n\n /** Whether the change is new; remembers its revision, or forgets the record on delete. */\n admit(collectionId: string, recordId: string, revision: number, change: 'record' | 'delete'): boolean {\n const key = `${collectionId}\\u0000${recordId}`;\n const seen = this.revisions.get(key);\n // A delete always passes, a late one too: the record may be written again under the same ID, from revision 1.\n if (change === 'delete') {\n this.revisions.delete(key);\n return true;\n }\n if (seen !== undefined && revision <= seen) return false;\n this.revisions.delete(key);\n this.revisions.set(key, revision);\n if (this.revisions.size > this.max) this.revisions.delete(this.revisions.keys().next().value!);\n return true;\n }\n\n clear() {\n this.revisions.clear();\n }\n}\n\nfunction pause(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const done = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', done);\n resolve();\n };\n const timer = setTimeout(done, ms);\n signal.addEventListener('abort', done, { once: true });\n });\n}\n\nexport function createSubscribe(runtime: SubscribeRuntime) {\n return async function* subscribe(options: SubscribeOptions = {}): AsyncGenerator<SubscriptionEvent, void, undefined> {\n const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];\n if (definitions.length < 1 || definitions.length > UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {\n throw new TypeError(`data.subscribe watches 1–${UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);\n }\n const recordIds = options.recordIds === undefined ? undefined : [...new Set(options.recordIds)];\n if (recordIds && (recordIds.length < 1 || recordIds.length > UNS_V2_SUBSCRIBE_MAX_RECORD_IDS || definitions.length !== 1)) {\n throw new TypeError(`data.subscribe takes 1–${UNS_V2_SUBSCRIBE_MAX_RECORD_IDS} recordIds, of exactly one collection.`);\n }\n if (options.lastEventId !== undefined) assertEventId(options.lastEventId);\n const redact = (options.restricted ?? 'collection') === 'collection';\n const idleMs = options.idleTimeoutMs ?? SUBSCRIBE_IDLE_TIMEOUT_MS;\n // The subject of the request that subscribed; an App-level subscription carries none.\n const call: CallOptions = { dataSubject: runtime.subject() };\n const stop = new AbortController();\n const onAbort = () => stop.abort();\n options.signal?.addEventListener('abort', onAbort, { once: true });\n if (options.signal?.aborted) stop.abort();\n let lastEventId = options.lastEventId;\n let failures = 0;\n const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);\n /** Waits, longer each time, before the next attempt. */\n const backOff = async (afterMs?: number) => {\n failures += 1;\n await pause(reconnectDelay(failures, afterMs), stop.signal);\n };\n try {\n while (!stop.signal.aborted) {\n let state: EnsureState;\n try {\n state = await runtime.ensure();\n } catch (error) {\n // A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.\n if (!(error instanceof DataError) || !error.retry.safe || stop.signal.aborted) throw error;\n await backOff(error.retry.afterMs);\n continue;\n }\n const aliases = new Map<string, string>();\n for (const definition of definitions) {\n const id = state.collectionIds.get(definition.alias.toLowerCase());\n if (!id) {\n throw new DataError({\n message: `Collection \"${definition.alias}\" is unavailable: ensure did not return it (see the server log).`,\n status: 503,\n code: 'COLLECTION_UNAVAILABLE',\n });\n }\n aliases.set(id, definition.alias);\n }\n\n const connection = new AbortController();\n const closeConnection = () => connection.abort();\n stop.signal.addEventListener('abort', closeConnection, { once: true });\n let idle: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n clearTimeout(idle);\n idle = setTimeout(closeConnection, idleMs);\n };\n const openedAt = Date.now();\n let healthy = false;\n let reset = false;\n try {\n arm();\n const stream = runtime.openStream(\n { collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) },\n { ...call, lastEventId, signal: connection.signal },\n );\n for await (const event of stream) {\n arm();\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) healthy = true;\n if (event.type === 'heartbeat') {\n // The connect heartbeat names the current position: resuming from it loses nothing.\n if (event.eventId !== undefined) lastEventId = event.eventId;\n continue;\n }\n if (event.type === 'reset') {\n lastEventId = undefined;\n reset = true;\n // The consumer rereads everything; revisions seen before mean nothing for what follows.\n seen.clear();\n yield event.message === undefined ? { type: 'reset', reason: event.reason } : { type: 'reset', reason: event.reason, message: event.message };\n continue;\n }\n healthy = true;\n // A dropped change is still a resume position.\n lastEventId = event.eventId;\n if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change)) continue;\n const change: RecordChange = {\n type: 'change',\n eventId: event.eventId,\n collection: aliases.get(event.collectionId) ?? event.collectionId,\n collectionId: event.collectionId,\n occurredAt: event.occurredAt,\n restricted: event.restricted,\n change: event.change,\n recordId: event.recordId,\n revision: event.revision,\n };\n yield redact ? redactRestricted(change) : change;\n }\n } catch (error) {\n if (stop.signal.aborted) return;\n if (!(error instanceof DataError)) throw error;\n if (isStaleCollectionError(error)) {\n let changed: boolean;\n try {\n changed = await runtime.rebuild(state, error);\n } catch (cause) {\n if (!(cause instanceof DataError) || !cause.retry.safe) throw cause;\n await backOff(cause.retry.afterMs);\n continue;\n }\n if (!changed) throw error;\n // Recreated collections have new IDs; their old cursor means nothing.\n lastEventId = undefined;\n seen.clear();\n yield { type: 'reset', reason: 'COLLECTION_RECREATED' };\n continue;\n }\n if (!error.retry.safe) throw error;\n await backOff(error.retry.afterMs);\n continue;\n } finally {\n clearTimeout(idle);\n connection.abort();\n stop.signal.removeEventListener('abort', closeConnection);\n }\n // The stream ended or went silent. Reconnect at once after a healthy connection, else back off.\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) healthy = true;\n if (healthy) failures = 0;\n else failures += 1;\n // A reset always waits a moment: a server resetting in a loop is not reconnected to in a loop.\n const wait = Math.max(reset ? RESET_PAUSE_MS : 0, healthy ? 0 : reconnectDelay(failures, undefined));\n if (wait > 0) await pause(wait, stop.signal);\n }\n } finally {\n stop.abort();\n options.signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"broker.js","sourceRoot":"","sources":["../../../src/mq/broker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * MQTT broker 地址归一化工具。\r\n *\r\n * 平台 `/openapi/v1/info` 返回的 `mqttBroker` 格式随环境变化,不保证统一:\r\n * - enterprise:env 为空时后端兜底返回 `tcp://<host>:1883`(面向边缘/服务端客户端的连接串);\r\n * - SaaS(Tier0 Cloud):env 原样透传,常见为 `host:port` 或裸 `host`。\r\n *\r\n * 浏览器 wss 客户端不应直接把它当 hostname 拼 URL(会得到 `wss://tcp://...`\r\n * 这类非法地址),先经本模块解析/推导,再交给 `Tier0MQClient`。\r\n */\r\n\r\n/** 解析后的 broker 端点。hostname 保证为裸主机(无 scheme、无端口、无路径)。 */\r\nexport interface MqttBrokerEndpoint {\r\n /** 裸主机名(已剥离 scheme/path/query,不含端口)。 */\r\n hostname: string;\r\n /**\r\n * 原始端口。注意:对 `tcp://host:1883` 这是 **tcp 端口**,浏览器 wss 场景\r\n * 不能直接沿用(wss 通常是 8084),由 `toWebSocketUrl` 的 `wssPort` 决定。\r\n */\r\n port?: number;\r\n /** 原始 scheme(tcp/mqtt/mqtts/ws/wss/...),裸 host 输入时为 undefined。 */\r\n scheme?: string;\r\n}\r\n\r\nconst SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//;\r\n\r\n/**\r\n * 解析各种形态的 broker 字符串为结构化端点。\r\n *\r\n * 兼容输入:`tcp://host:1883`、`mqtts://host:8883`、`wss://host:8084/mqtt`、\r\n * `host:1883`、裸 `host`。无法解析(空串、非法 URL)返回 undefined。\r\n */\r\nexport function parseMqttBroker(broker?: string | null): MqttBrokerEndpoint | undefined {\r\n const raw = broker?.trim();\r\n if (!raw) return undefined;\r\n\r\n if (SCHEME_RE.test(raw)) {\r\n try {\r\n const u = new URL(raw);\r\n if (!u.hostname) return undefined;\r\n const scheme = SCHEME_RE.exec(raw)?.[1]?.toLowerCase();\r\n return {\r\n hostname: u.hostname,\r\n port: u.port ? Number.parseInt(u.port, 10) : undefined,\r\n scheme,\r\n };\r\n } catch {\r\n return undefined;\r\n }\r\n }\r\n\r\n // 无 scheme:`host:port` 或裸 `host`;含路径/空白的是垃圾输入,拒绝\r\n if (/[\\s/]/.test(raw)) return undefined;\r\n const i = raw.lastIndexOf(':');\r\n if (i > 0 && /^\\d+$/.test(raw.slice(i + 1))) {\r\n const hostname = raw.slice(0, i);\r\n if (!hostname) return undefined;\r\n return { hostname, port: Number.parseInt(raw.slice(i + 1), 10) };\r\n }\r\n return { hostname: raw };\r\n}\r\n\r\n/** 浏览器 wss 默认端口(与平台 OS_MQTT_WEBSOCKET_TSL_PORT 默认值一致)。 */\r\nexport const DEFAULT_WSS_PORT = 8084;\r\n/** MQTT over WebSocket 路径。 */\r\nexport const DEFAULT_WS_PATH = '/mqtt';\r\n\r\nexport interface ToWebSocketUrlOptions {\r\n /** wss 端口,默认 {@link DEFAULT_WSS_PORT}(8084)。 */\r\n wssPort?: number;\r\n /**\r\n * 显式指定 ws(false)/wss(true)。缺省自适应:浏览器 https 页面用 wss,\r\n * 或 WebSocket 端口为 8084 时用 wss,其余用 ws。\r\n */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * 当前运行环境是否处于 https 安全上下文。\r\n *\r\n * 用 `globalThis.location` 而不是 `window.location`:Web Worker\r\n * (dedicated/shared)里 `window` 不存在,但 `location.protocol` 同样是 `https:`,\r\n * 应同等视为安全上下文(否则会被降级为 ws 而被浏览器按 mixed content 拦截)。\r\n */\r\nexport function isBrowserHttps(): boolean {\r\n const loc = (globalThis as { location?: { protocol?: string } }).location;\r\n return typeof loc?.protocol === 'string' && loc.protocol === 'https:';\r\n}\r\n\r\n/**\r\n * 把 `mqttBroker`(或 {@link MqttBrokerEndpoint})推导为可直接传给\r\n * `Tier0MQClient` 的 WebSocket URL。\r\n *\r\n * 规则:\r\n * - 输入已是 `ws(s)://` URL:原样归一(补 `/mqtt` 路径,端口、path、query 均保留);\r\n * - 输入 `tcp://host:1883` / `host:port` / 裸 `host`:按 `secure` 推导 scheme,\r\n * 端口一律用 `wssPort`(8084),**不沿用** tcp 端口;\r\n * - 输入为空/无法解析:返回 undefined(调用方应回退或报错,勿硬拼 URL)。\r\n *\r\n * @example\r\n * toWebSocketUrl('tcp://example.tier0.dev:1883', { secure: true });\r\n * // → 'wss://example.tier0.dev:8084/mqtt'\r\n */\r\nexport function toWebSocketUrl(\r\n broker: string | MqttBrokerEndpoint | undefined | null,\r\n opts: ToWebSocketUrlOptions = {},\r\n): string | undefined {\r\n if (broker == null || broker === '') return undefined;\r\n\r\n // 已是 ws(s) URL:解析后归一直通(保留其 scheme/端口/path/query,仅补 /mqtt 路径)\r\n if (typeof broker === 'string' && /^wss?:\\/\\//i.test(broker.trim())) {\r\n try {\r\n const u = new URL(broker.trim());\r\n if (!u.hostname) return undefined;\r\n if (!u.pathname || u.pathname === '/') {\r\n u.pathname = DEFAULT_WS_PATH;\r\n }\r\n return u.toString();\r\n } catch {\r\n return undefined;\r\n }\r\n }\r\n\r\n const endpoint = typeof broker === 'string' ? parseMqttBroker(broker) : broker;\r\n if (!endpoint?.hostname) return undefined;\r\n\r\n // ws(s) 端点:保留其 scheme 与端口(\"已是 WebSocket 地址\"的直通语义)\r\n if (endpoint.scheme === 'ws' || endpoint.scheme === 'wss') {\r\n const port = endpoint.port ? `:${endpoint.port}` : '';\r\n return `${endpoint.scheme}://${endpoint.hostname}${port}${DEFAULT_WS_PATH}`;\r\n }\r\n\r\n const port = opts.wssPort ?? DEFAULT_WSS_PORT;\r\n const secure = opts.secure ?? (isBrowserHttps() || port === DEFAULT_WSS_PORT);\r\n return `${secure ? 'wss' : 'ws'}://${endpoint.hostname}:${port}${DEFAULT_WS_PATH}`;\r\n}\r\n"]}
1
+ {"version":3,"file":"broker.js","sourceRoot":"","sources":["../../../src/mq/broker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAeH,MAAM,SAAS,GAAG,iCAAiC,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAsB;IACpD,MAAM,GAAG,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAE3B,IAAI,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,CAAC,CAAC,CAAC,QAAQ;gBAAE,OAAO,SAAS,CAAC;YAClC,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;YACvD,OAAO;gBACL,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;gBACtD,MAAM;aACP,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,iDAAiD;IACjD,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,QAAQ;YAAE,OAAO,SAAS,CAAC;QAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IACnE,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;AAC3B,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AACrC,8BAA8B;AAC9B,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AAYvC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,GAAG,GAAI,UAAmD,CAAC,QAAQ,CAAC;IAC1E,OAAO,OAAO,GAAG,EAAE,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAsD,EACtD,OAA8B,EAAE;IAEhC,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAEtD,6DAA6D;IAC7D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACpE,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YACjC,IAAI,CAAC,CAAC,CAAC,QAAQ;gBAAE,OAAO,SAAS,CAAC;YAClC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,GAAG,EAAE,CAAC;gBACtC,CAAC,CAAC,QAAQ,GAAG,eAAe,CAAC;YAC/B,CAAC;YACD,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/E,IAAI,CAAC,QAAQ,EAAE,QAAQ;QAAE,OAAO,SAAS,CAAC;IAE1C,kDAAkD;IAClD,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;QAC1D,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO,GAAG,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,QAAQ,GAAG,IAAI,GAAG,eAAe,EAAE,CAAC;IAC9E,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,cAAc,EAAE,IAAI,IAAI,KAAK,gBAAgB,CAAC,CAAC;IAC9E,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,QAAQ,CAAC,QAAQ,IAAI,IAAI,GAAG,eAAe,EAAE,CAAC;AACrF,CAAC","sourcesContent":["/**\n * MQTT broker 地址归一化工具。\n *\n * 平台 `/openapi/v1/info` 返回的 `mqttBroker` 格式随环境变化,不保证统一:\n * - enterprise:env 为空时后端兜底返回 `tcp://<host>:1883`(面向边缘/服务端客户端的连接串);\n * - SaaS(Tier0 Cloud):env 原样透传,常见为 `host:port` 或裸 `host`。\n *\n * 浏览器 wss 客户端不应直接把它当 hostname 拼 URL(会得到 `wss://tcp://...`\n * 这类非法地址),先经本模块解析/推导,再交给 `Tier0MQClient`。\n */\n\n/** 解析后的 broker 端点。hostname 保证为裸主机(无 scheme、无端口、无路径)。 */\nexport interface MqttBrokerEndpoint {\n /** 裸主机名(已剥离 scheme/path/query,不含端口)。 */\n hostname: string;\n /**\n * 原始端口。注意:对 `tcp://host:1883` 这是 **tcp 端口**,浏览器 wss 场景\n * 不能直接沿用(wss 通常是 8084),由 `toWebSocketUrl` 的 `wssPort` 决定。\n */\n port?: number;\n /** 原始 scheme(tcp/mqtt/mqtts/ws/wss/...),裸 host 输入时为 undefined。 */\n scheme?: string;\n}\n\nconst SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//;\n\n/**\n * 解析各种形态的 broker 字符串为结构化端点。\n *\n * 兼容输入:`tcp://host:1883`、`mqtts://host:8883`、`wss://host:8084/mqtt`、\n * `host:1883`、裸 `host`。无法解析(空串、非法 URL)返回 undefined。\n */\nexport function parseMqttBroker(broker?: string | null): MqttBrokerEndpoint | undefined {\n const raw = broker?.trim();\n if (!raw) return undefined;\n\n if (SCHEME_RE.test(raw)) {\n try {\n const u = new URL(raw);\n if (!u.hostname) return undefined;\n const scheme = SCHEME_RE.exec(raw)?.[1]?.toLowerCase();\n return {\n hostname: u.hostname,\n port: u.port ? Number.parseInt(u.port, 10) : undefined,\n scheme,\n };\n } catch {\n return undefined;\n }\n }\n\n // 无 scheme:`host:port` 或裸 `host`;含路径/空白的是垃圾输入,拒绝\n if (/[\\s/]/.test(raw)) return undefined;\n const i = raw.lastIndexOf(':');\n if (i > 0 && /^\\d+$/.test(raw.slice(i + 1))) {\n const hostname = raw.slice(0, i);\n if (!hostname) return undefined;\n return { hostname, port: Number.parseInt(raw.slice(i + 1), 10) };\n }\n return { hostname: raw };\n}\n\n/** 浏览器 wss 默认端口(与平台 OS_MQTT_WEBSOCKET_TSL_PORT 默认值一致)。 */\nexport const DEFAULT_WSS_PORT = 8084;\n/** MQTT over WebSocket 路径。 */\nexport const DEFAULT_WS_PATH = '/mqtt';\n\nexport interface ToWebSocketUrlOptions {\n /** wss 端口,默认 {@link DEFAULT_WSS_PORT}(8084)。 */\n wssPort?: number;\n /**\n * 显式指定 ws(false)/wss(true)。缺省自适应:浏览器 https 页面用 wss,\n * 或 WebSocket 端口为 8084 时用 wss,其余用 ws。\n */\n secure?: boolean;\n}\n\n/**\n * 当前运行环境是否处于 https 安全上下文。\n *\n * 用 `globalThis.location` 而不是 `window.location`:Web Worker\n * (dedicated/shared)里 `window` 不存在,但 `location.protocol` 同样是 `https:`,\n * 应同等视为安全上下文(否则会被降级为 ws 而被浏览器按 mixed content 拦截)。\n */\nexport function isBrowserHttps(): boolean {\n const loc = (globalThis as { location?: { protocol?: string } }).location;\n return typeof loc?.protocol === 'string' && loc.protocol === 'https:';\n}\n\n/**\n * 把 `mqttBroker`(或 {@link MqttBrokerEndpoint})推导为可直接传给\n * `Tier0MQClient` 的 WebSocket URL。\n *\n * 规则:\n * - 输入已是 `ws(s)://` URL:原样归一(补 `/mqtt` 路径,端口、path、query 均保留);\n * - 输入 `tcp://host:1883` / `host:port` / 裸 `host`:按 `secure` 推导 scheme,\n * 端口一律用 `wssPort`(8084),**不沿用** tcp 端口;\n * - 输入为空/无法解析:返回 undefined(调用方应回退或报错,勿硬拼 URL)。\n *\n * @example\n * toWebSocketUrl('tcp://example.tier0.dev:1883', { secure: true });\n * // → 'wss://example.tier0.dev:8084/mqtt'\n */\nexport function toWebSocketUrl(\n broker: string | MqttBrokerEndpoint | undefined | null,\n opts: ToWebSocketUrlOptions = {},\n): string | undefined {\n if (broker == null || broker === '') return undefined;\n\n // 已是 ws(s) URL:解析后归一直通(保留其 scheme/端口/path/query,仅补 /mqtt 路径)\n if (typeof broker === 'string' && /^wss?:\\/\\//i.test(broker.trim())) {\n try {\n const u = new URL(broker.trim());\n if (!u.hostname) return undefined;\n if (!u.pathname || u.pathname === '/') {\n u.pathname = DEFAULT_WS_PATH;\n }\n return u.toString();\n } catch {\n return undefined;\n }\n }\n\n const endpoint = typeof broker === 'string' ? parseMqttBroker(broker) : broker;\n if (!endpoint?.hostname) return undefined;\n\n // ws(s) 端点:保留其 scheme 与端口(\"已是 WebSocket 地址\"的直通语义)\n if (endpoint.scheme === 'ws' || endpoint.scheme === 'wss') {\n const port = endpoint.port ? `:${endpoint.port}` : '';\n return `${endpoint.scheme}://${endpoint.hostname}${port}${DEFAULT_WS_PATH}`;\n }\n\n const port = opts.wssPort ?? DEFAULT_WSS_PORT;\n const secure = opts.secure ?? (isBrowserHttps() || port === DEFAULT_WSS_PORT);\n return `${secure ? 'wss' : 'ws'}://${endpoint.hostname}:${port}${DEFAULT_WS_PATH}`;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/mq/client.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAWhG,SAAS,0BAA0B,CAAC,MAAc;IAChD,uEAAuE;IACvE,yEAAyE;IACzE,MAAM,KAAK,GAAG,gCAAgC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,wEAAwE;IACxE,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,WAAW,GAAG,WAAW,GAAG,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QAC9D,IAAI,WAAW,GAAG,oBAAoB;YAAE,OAAO,SAAS,CAAC;IAC3D,CAAC;IACD,OAAO,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC;AAED,SAAS,oBAAoB,CAAC,MAAM,GAAG,CAAC;IACtC,MAAM,KAAK,GAAG,sCAAsC,CAAC;IACrD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AASD,MAAM,OAAO,aAAa;IAChB,MAAM,GAAsB,IAAI,CAAC;IACjC,MAAM,CAAa;IACnB,gBAAgB,CAAU;IAC1B,gBAAgB,CAAU;IAC1B,SAAS,GAA2D,EAAE,CAAC;IACvE,aAAa,GAAmB,EAAE,CAAC;IACnC,iBAAiB,GAAyB,IAAI,CAAC;IAC/C,UAAU,GAAG,KAAK,CAAC;IAE3B,YAAY,MAAmB;QAC7B,IAAI,CAAC,gBAAgB,GAAG,MAAM,EAAE,QAAQ,KAAK,SAAS,CAAC;QACvD,IAAI,CAAC,gBAAgB,GAAG,MAAM,EAAE,QAAQ,KAAK,SAAS,CAAC;QACvD,MAAM,OAAO,GAAG,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAE7C,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,OAAO,IAAI,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,QAAQ,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC;QACjE,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,SAAS,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;QAEtE,mDAAmD;QACnD,MAAM,WAAW,GAAG,MAAM,EAAE,QAAQ,IAAI,MAAM,EAAE,QAAQ;YACtD,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC;QAEzC,MAAM,YAAY,GAAG,WAAW;YAC9B,CAAC,CAAC,GAAG,WAAW,IAAI,oBAAoB,CAAC,CAAC,CAAC,EAAE;YAC7C,CAAC,CAAC,SAAS,CAAC;QACd,MAAM,YAAY,GAAG,WAAW;YAC9B,CAAC,CAAC,GAAG,WAAW,OAAO;YACvB,CAAC,CAAC,SAAS,CAAC;QAEd,IAAI,CAAC,MAAM,GAAG;YACZ,IAAI;YACJ,IAAI;YACJ,QAAQ,EAAE,MAAM,EAAE,QAAQ,IAAI,YAAY;YAC1C,QAAQ,EAAE,MAAM,EAAE,QAAQ,IAAI,YAAY;YAC1C,QAAQ;YACR,SAAS,EAAE,MAAM,EAAE,SAAS,IAAI,EAAE;YAClC,eAAe,EAAE,MAAM,EAAE,eAAe,IAAI,IAAI;YAChD,cAAc,EAAE,MAAM,EAAE,cAAc,IAAI,KAAK;YAC/C,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,IAAI;YAC5B,GAAG,MAAM;SACV,CAAC;IACJ,CAAC;IAED,IAAY,OAAO;QACjB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CACb,2FAA2F,CAC5F,CAAC;QACJ,CAAC;QACD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACvD,IAAI,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YACvC,MAAM,GAAG,GAAG,cAAc,CAAC,cAAc,CAAC,CAAC;YAC3C,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;YAC5D,OAAO,GAAG,CAAC;QACb,CAAC;QACD,+BAA+B;QAC/B,MAAM,QAAQ,GAAG,eAAe,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,QAAQ,EAAE,QAAQ,IAAI,cAAc,CAAC;QACtD,+CAA+C;QAC/C,2DAA2D;QAC3D,MAAM,aAAa,GACjB,CAAC,QAAQ,EAAE,MAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK;YACxE,CAAC,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI;YACxB,CAAC,CAAC,IAAI,CAAC;QACX,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,IAAI,aAAa,KAAK,gBAAgB,CAAC,CAAC;QACrF,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,QAAQ,IAAI,aAAa,OAAO,CAAC;IAC3E,CAAC;IAED,eAAe;IACP,KAAK,CAAC,eAAe;QAC3B,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,IAAI,CAAC,iBAAiB;YAAE,OAAO,IAAI,CAAC,iBAAiB,CAAC;QAE1D,KAAK,IAAI,CAAC,OAAO,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,qGAAqG,CAAC,CAAC;QACzH,CAAC;QAED,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC,iBAAiB,CAAC;IAChC,CAAC;IAEO,SAAS;QACf,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,OAAO,GAAmB;gBAC9B,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC9B,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC9B,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC9B,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;gBAChC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe;gBAC5C,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc;gBAC1C,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;aACzB,CAAC;YAEF,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAElD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;gBAC7B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;gBACvB,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;gBAC9B,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACrB,gBAAgB;gBAChB,IAAI,CAAC,cAAc,EAAE,CAAC;gBACtB,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC9B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBACxB,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;gBAC9B,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE;gBAChC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC1B,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBAC3B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YAC1B,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CACZ,SAAS,EACT,CAAC,KAAa,EAAE,OAAe,EAAE,OAA4B,EAAE,EAAE;gBAC/D,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACtC,iDAAiD;gBACjD,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;oBACzC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;wBACtC,IAAI,CAAC;4BACH,qDAAqD;4BACrD,MAAM,QAAQ,GACZ,GAAG,CAAC,OACL,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;4BACrB,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;gCACvB,GAAG,CAAC,kBAAkB,GAAG,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;gCAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,0BAA0B,IAAI,CAAC,CAAC;gCAC1D,IAAI,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,kBAAkB,IAAI,KAAK,EAAE,CAAC;oCACjD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;oCACzC,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE;wCAC/B,KAAK,EAAE,GAAG,CAAC,KAAK;wCAChB,MAAM,EAAE,cAAc,GAAG,CAAC,kBAAkB,+CAA+C,KAAK,OAAO;qCACxG,CAAC,CAAC;gCACL,CAAC;4BACH,CAAC;iCAAM,CAAC;gCACN,GAAG,CAAC,kBAAkB,GAAG,CAAC,CAAC;4BAC7B,CAAC;wBACH,CAAC;wBAAC,OAAO,CAAC,EAAE,CAAC;4BACX,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAU,CAAC,CAAC;wBACjC,CAAC;oBACH,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,oBAAoB;IACpB,KAAK,CAAC,OAAO,CAAC,MAAmB;QAC/B,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;YAC9C,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;YAC5C,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;gBAAE,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAChE,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;gBAAE,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAChE,wEAAwE;YACxE,6EAA6E;YAC7E,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,gBAAgB,EAAE,CAAC;gBAC1E,MAAM,WAAW,GAAG,0BAA0B,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAChE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,GAAG,GAAG,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC/F,CAAC;gBACD,IAAI,CAAC,IAAI,CAAC,gBAAgB;oBAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;YACrG,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,eAAe,EAAE,CAAC;IAChC,CAAC;IAED,oDAAoD;IACpD,yBAAyB;IACzB,SAAS,CAAC,KAAa,EAAE,OAAqB;QAC5C,6BAA6B;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CACpC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAClD,CAAC;QACF,IAAI,MAAM,EAAE,CAAC;YACX,OAAO;QACT,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAE5C,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,CAAc,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC5D,IAAI,GAAG,EAAE,CAAC;oBACR,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;oBACxB,UAAU;oBACV,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CACrD,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,+CAA+C;YAC/C,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,2DAA2D;IAC3D,WAAW,CAAC,KAAa,EAAE,OAAsB;QAC/C,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CACrD,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;QAC3E,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEzB,mDAAmD;QACnD,MAAM,eAAe,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;QAC1E,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE;gBACrC,IAAI,GAAG,EAAE,CAAC;oBACR,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,gCAAgC;IAChC,KAAK,CAAC,OAAO,CACX,KAAa,EACb,OAAwB,EACxB,OAA4C;QAE5C,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAE7B,MAAM,OAAO,GACX,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAClE,IAAI,CAAC,MAAO,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE;YACnC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAc;YACrC,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,KAAK;SACjC,CAAC,CAAC;IACL,CAAC;IAED,UAAU;QACR,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEzB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE;YACzB,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAC9B,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;IAC1B,CAAC;IAED,EAAE,CAA+B,KAAQ,EAAE,OAAwB;QACjE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvC,CAAC;IAED,GAAG,CAA+B,KAAQ,EAAE,OAAwB;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YACf,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAEO,IAAI,CACV,KAAQ,EACR,GAAG,IAAiC;QAEpC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,wBAAwB;IAChB,cAAc;QACpB,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEzB,4BAA4B;QAC5B,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAC7B,IAAI,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAChD,CAAC;QACF,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;YAC7B,IAAI,CAAC,MAAO,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,CAAc,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC7D,IAAI,GAAG,EAAE,CAAC;oBACR,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,0CAA0C;IAClC,UAAU,CAAC,UAAkB,EAAE,QAAgB;QACrD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YAEvB,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACf,qBAAqB;gBACrB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACf,WAAW;gBACX,SAAS;YACX,CAAC;YACD,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACd,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,CAAC;IAC7C,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,IAAI,gBAAgB;QAClB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACrE,CAAC;CACF","sourcesContent":["import mqtt from 'mqtt';\r\nimport type { MqttClient, IClientOptions } from 'mqtt';\r\nimport { getEnvVar } from '../runtime-env.js';\r\nimport { DEFAULT_WSS_PORT, isBrowserHttps, parseMqttBroker, toWebSocketUrl } from './broker.js';\r\nimport type { MQTTConfig, MQTTEventMap } from './types.js';\r\n\r\n/**\r\n * 消息回调。公开类型保持 `void` 返回,以兼容 async handler、`push()` 简写等\r\n * 既有写法;运行时额外识别返回值:handler 返回严格的 `false` 视为下游停滞\r\n * (背压),SDK 会统计连续背压次数,配合 MQTTConfig.maxBackpressuredDeliveries\r\n * 可在阈值内自动移除订阅。\r\n */\r\nexport type TopicHandler = (topic: string, payload: string) => void;\r\n\r\nfunction parseWorkspaceIDFromApiKey(apiKey: string): string | undefined {\r\n // Match the backend's sk-<type>-ws<base36>_<secret> contract, not only\r\n // service keys. Other workspace-encoded keys use the same MQTT identity.\r\n const match = /^sk-[^-]+-ws([0-9a-zA-Z]+)_.+$/.exec(apiKey.trim());\r\n if (!match) return undefined;\r\n\r\n // Keep the int64 workspace ID exact; parseInt can round IDs above 2^53.\r\n let workspaceID = 0n;\r\n for (const digit of match[1]) {\r\n workspaceID = workspaceID * 36n + BigInt(parseInt(digit, 36));\r\n if (workspaceID > 9223372036854775807n) return undefined;\r\n }\r\n return workspaceID > 0n ? workspaceID.toString() : undefined;\r\n}\r\n\r\nfunction generateRandomString(length = 8): string {\r\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\r\n let result = '';\r\n for (let i = 0; i < length; i++) {\r\n result += chars.charAt(Math.floor(Math.random() * chars.length));\r\n }\r\n return result;\r\n}\r\n\r\ninterface Subscription {\r\n topic: string;\r\n handler: TopicHandler;\r\n /** handler 连续返回 false(背压)的次数,正常返回时清零。 */\r\n backpressuredCount?: number;\r\n}\r\n\r\nexport class Tier0MQClient {\r\n private client: MqttClient | null = null;\r\n private config: MQTTConfig;\r\n private explicitClientId: boolean;\r\n private explicitUsername: boolean;\r\n private listeners: { [K in keyof MQTTEventMap]?: Array<MQTTEventMap[K]> } = {};\r\n private subscriptions: Subscription[] = [];\r\n private connectingPromise: Promise<void> | null = null;\r\n private _connected = false;\r\n\r\n constructor(config?: MQTTConfig) {\r\n this.explicitClientId = config?.clientId !== undefined;\r\n this.explicitUsername = config?.username !== undefined;\r\n const envHost = getEnvVar('TIER0_MQTT_HOST');\r\n const envPort = getEnvVar('TIER0_MQTT_PORT');\r\n\r\n const host = config?.host || envHost || '';\r\n const port = config?.port || parseInt(envPort || '8084') || 8084;\r\n const password = config?.password || getEnvVar('TIER0_API_KEY') || '';\r\n\r\n // 从 apiKey 解析 workspaceID,自动生成 clientId 和 username\r\n const workspaceID = config?.clientId && config?.username\r\n ? undefined\r\n : parseWorkspaceIDFromApiKey(password);\r\n\r\n const autoClientId = workspaceID\r\n ? `${workspaceID}&${generateRandomString(8)}`\r\n : undefined;\r\n const autoUsername = workspaceID\r\n ? `${workspaceID}&open`\r\n : undefined;\r\n\r\n this.config = {\r\n host,\r\n port,\r\n clientId: config?.clientId || autoClientId,\r\n username: config?.username || autoUsername,\r\n password,\r\n keepAlive: config?.keepAlive ?? 60,\r\n reconnectPeriod: config?.reconnectPeriod ?? 5000,\r\n connectTimeout: config?.connectTimeout ?? 30000,\r\n clean: config?.clean ?? true,\r\n ...config,\r\n };\r\n }\r\n\r\n private get mqttUrl(): string {\r\n const { host, port, secure } = this.config;\r\n if (!host) {\r\n throw new Error(\r\n 'MQTT host is required. Provide it via MQTTConfig or TIER0_MQTT_HOST environment variable.'\r\n );\r\n }\r\n const normalizedHost = host.trim().replace(/\\/+$/, '');\r\n if (/^wss?:\\/\\//i.test(normalizedHost)) {\r\n const url = toWebSocketUrl(normalizedHost);\r\n if (!url) throw new TypeError('Invalid MQTT WebSocket URL');\r\n return url;\r\n }\r\n // 裸 host(可含端口):按运行环境自适应 ws/wss\r\n const endpoint = parseMqttBroker(normalizedHost);\r\n const hostname = endpoint?.hostname ?? normalizedHost;\r\n // 裸 host:port 和 ws(s) URL 的端口按 WebSocket 端口处理;\r\n // tcp/mqtt/mqtts 的端口是 TCP 端口(如 1883),wss 下不能沿用,回退到配置的 port\r\n const effectivePort =\r\n !endpoint?.scheme || endpoint.scheme === 'ws' || endpoint.scheme === 'wss'\r\n ? endpoint?.port ?? port\r\n : port;\r\n const useSecure = secure ?? (isBrowserHttps() || effectivePort === DEFAULT_WSS_PORT);\r\n return `${useSecure ? 'wss' : 'ws'}://${hostname}:${effectivePort}/mqtt`;\r\n }\r\n\r\n // 内部确保已连接(懒连接)\r\n private async ensureConnected(): Promise<void> {\r\n if (this._connected) return;\r\n if (this.connectingPromise) return this.connectingPromise;\r\n\r\n void this.mqttUrl;\r\n if (!this.config.password || !this.config.username || !this.config.clientId) {\r\n throw new Error('Tier0 SDK v2: provide a workspace-encoded API key or explicit MQTT username, clientId and password.');\r\n }\r\n\r\n this.connectingPromise = this.doConnect();\r\n return this.connectingPromise;\r\n }\r\n\r\n private doConnect(): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const options: IClientOptions = {\r\n clientId: this.config.clientId,\r\n username: this.config.username,\r\n password: this.config.password,\r\n keepalive: this.config.keepAlive,\r\n reconnectPeriod: this.config.reconnectPeriod,\r\n connectTimeout: this.config.connectTimeout,\r\n clean: this.config.clean,\r\n };\r\n\r\n this.client = mqtt.connect(this.mqttUrl, options);\r\n\r\n this.client.on('connect', () => {\r\n this._connected = true;\r\n this.connectingPromise = null;\r\n this.emit('connect');\r\n // 断连重连后自动恢复所有订阅\r\n this.resubscribeAll();\r\n resolve();\r\n });\r\n\r\n this.client.on('error', (err) => {\r\n this.emit('error', err);\r\n this.connectingPromise = null;\r\n reject(err);\r\n });\r\n\r\n this.client.on('disconnect', () => {\r\n this._connected = false;\r\n this.emit('disconnect');\r\n });\r\n\r\n this.client.on('close', () => {\r\n this._connected = false;\r\n });\r\n\r\n this.client.on(\r\n 'message',\r\n (topic: string, payload: Buffer, _packet: mqtt.IPublishPacket) => {\r\n const payloadStr = payload.toString();\r\n // 复制一份快照:handler 触发背压自动退订时会重建 this.subscriptions\r\n this.subscriptions.slice().forEach((sub) => {\r\n if (this.topicMatch(sub.topic, topic)) {\r\n try {\r\n // 公开类型为 void 返回(兼容 async/简写 handler),运行时仅识别严格的 false\r\n const accepted = (\r\n sub.handler as (t: string, p: string) => unknown\r\n )(topic, payloadStr);\r\n if (accepted === false) {\r\n sub.backpressuredCount = (sub.backpressuredCount ?? 0) + 1;\r\n const limit = this.config.maxBackpressuredDeliveries ?? 0;\r\n if (limit > 0 && sub.backpressuredCount >= limit) {\r\n this.unsubscribe(sub.topic, sub.handler);\r\n this.emit('subscriptionDropped', {\r\n topic: sub.topic,\r\n reason: `handler 连续 ${sub.backpressuredCount} 次返回背压(false),已按 maxBackpressuredDeliveries=${limit} 自动退订`,\r\n });\r\n }\r\n } else {\r\n sub.backpressuredCount = 0;\r\n }\r\n } catch (e) {\r\n this.emit('error', e as Error);\r\n }\r\n }\r\n });\r\n }\r\n );\r\n });\r\n }\r\n\r\n // 显式连接(需要等待连接完成时使用)\r\n async connect(config?: MQTTConfig): Promise<void> {\r\n if (config) {\r\n const previousPassword = this.config.password;\r\n this.config = { ...this.config, ...config };\r\n if (config.clientId !== undefined) this.explicitClientId = true;\r\n if (config.username !== undefined) this.explicitUsername = true;\r\n // A connect-time key may belong to a different workspace. Preserve only\r\n // caller-supplied identity fields, not values derived from the previous key.\r\n if (config.password !== undefined && config.password !== previousPassword) {\r\n const workspaceID = parseWorkspaceIDFromApiKey(config.password);\r\n if (!this.explicitClientId) {\r\n this.config.clientId = workspaceID ? workspaceID + '&' + generateRandomString(8) : undefined;\r\n }\r\n if (!this.explicitUsername) this.config.username = workspaceID ? workspaceID + '&open' : undefined;\r\n }\r\n }\r\n return this.ensureConnected();\r\n }\r\n\r\n // 订阅 topic,内部 qos 固定为 1,handler 接收 (topic, payload)\r\n // 无需先调用 connect(),自动处理连接\r\n subscribe(topic: string, handler: TopicHandler): void {\r\n // 避免重复注册同一 topic 的同一 handler\r\n const exists = this.subscriptions.some(\r\n (s) => s.topic === topic && s.handler === handler\r\n );\r\n if (exists) {\r\n return;\r\n }\r\n\r\n this.subscriptions.push({ topic, handler });\r\n\r\n if (this._connected && this.client) {\r\n this.client.subscribe(topic, { qos: 1 as 0 | 1 | 2 }, (err) => {\r\n if (err) {\r\n this.emit('error', err);\r\n // 订阅失败时回滚\r\n this.subscriptions = this.subscriptions.filter(\r\n (s) => !(s.topic === topic && s.handler === handler)\r\n );\r\n }\r\n });\r\n } else {\r\n // 未连接时触发懒连接,连接成功后 resubscribeAll 会自动 subscribe\r\n this.ensureConnected().catch((err) => this.emit('error', err));\r\n }\r\n }\r\n\r\n // 取消订阅:如果提供了 handler 则仅移除该 handler,否则移除该 topic 下所有 handler\r\n unsubscribe(topic: string, handler?: TopicHandler): void {\r\n if (handler) {\r\n this.subscriptions = this.subscriptions.filter(\r\n (s) => !(s.topic === topic && s.handler === handler)\r\n );\r\n } else {\r\n this.subscriptions = this.subscriptions.filter((s) => s.topic !== topic);\r\n }\r\n\r\n if (!this.client) return;\r\n\r\n // 如果该 topic 下已无任何 handler,则向 broker 发送 unsubscribe\r\n const stillHasHandler = this.subscriptions.some((s) => s.topic === topic);\r\n if (!stillHasHandler) {\r\n this.client.unsubscribe(topic, (err) => {\r\n if (err) {\r\n this.emit('error', err);\r\n }\r\n });\r\n }\r\n }\r\n\r\n // 发布消息,无需先调用 connect(),内部自动处理连接\r\n async publish(\r\n topic: string,\r\n payload: string | object,\r\n options?: { qos?: number; retain?: boolean }\r\n ): Promise<void> {\r\n await this.ensureConnected();\r\n\r\n const message =\r\n typeof payload === 'string' ? payload : JSON.stringify(payload);\r\n this.client!.publish(topic, message, {\r\n qos: (options?.qos ?? 0) as 0 | 1 | 2,\r\n retain: options?.retain ?? false,\r\n });\r\n }\r\n\r\n disconnect(): void {\r\n if (!this.client) return;\r\n\r\n this.client.end(true, () => {\r\n this.emit('disconnect');\r\n });\r\n this.client = null;\r\n this._connected = false;\r\n this.connectingPromise = null;\r\n this.subscriptions = [];\r\n }\r\n\r\n on<K extends keyof MQTTEventMap>(event: K, handler: MQTTEventMap[K]): void {\r\n if (!this.listeners[event]) {\r\n this.listeners[event] = [];\r\n }\r\n this.listeners[event]!.push(handler);\r\n }\r\n\r\n off<K extends keyof MQTTEventMap>(event: K, handler: MQTTEventMap[K]): void {\r\n const handlers = this.listeners[event];\r\n if (!handlers) return;\r\n const idx = handlers.indexOf(handler);\r\n if (idx !== -1) {\r\n handlers.splice(idx, 1);\r\n }\r\n }\r\n\r\n private emit<K extends keyof MQTTEventMap>(\r\n event: K,\r\n ...args: Parameters<MQTTEventMap[K]>\r\n ): void {\r\n const handlers = this.listeners[event];\r\n if (!handlers) return;\r\n handlers.forEach((h) => (h as any)(...args));\r\n }\r\n\r\n // 断连后自动重新订阅所有已注册的 topic\r\n private resubscribeAll(): void {\r\n if (!this.client) return;\r\n\r\n // 按 topic 去重,每个 topic 只订阅一次\r\n const uniqueTopics = Array.from(\r\n new Set(this.subscriptions.map((s) => s.topic))\r\n );\r\n uniqueTopics.forEach((topic) => {\r\n this.client!.subscribe(topic, { qos: 1 as 0 | 1 | 2 }, (err) => {\r\n if (err) {\r\n this.emit('error', err);\r\n }\r\n });\r\n });\r\n }\r\n\r\n // 判断接收到的 topic 是否匹配订阅的 topic(支持通配符 # 和 +)\r\n private topicMatch(subscribed: string, received: string): boolean {\r\n const subParts = subscribed.split('/');\r\n const recParts = received.split('/');\r\n\r\n for (let i = 0; i < subParts.length; i++) {\r\n const sp = subParts[i];\r\n const rp = recParts[i];\r\n\r\n if (sp === '#') {\r\n // # 必须是最后一个层级,匹配后面所有\r\n return true;\r\n }\r\n if (sp === '+') {\r\n // + 匹配单个层级\r\n continue;\r\n }\r\n if (sp !== rp) {\r\n return false;\r\n }\r\n }\r\n\r\n return subParts.length === recParts.length;\r\n }\r\n\r\n get isConnected(): boolean {\r\n return this._connected;\r\n }\r\n\r\n get subscribedTopics(): string[] {\r\n return Array.from(new Set(this.subscriptions.map((s) => s.topic)));\r\n }\r\n}\r\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/mq/client.ts"],"names":[],"mappings":"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","sourcesContent":["import mqtt from 'mqtt';\nimport type { MqttClient, IClientOptions } from 'mqtt';\nimport { getEnvVar } from '../runtime-env.js';\nimport { DEFAULT_WSS_PORT, isBrowserHttps, parseMqttBroker, toWebSocketUrl } from './broker.js';\nimport type { MQTTConfig, MQTTEventMap } from './types.js';\n\n/**\n * 消息回调。公开类型保持 `void` 返回,以兼容 async handler、`push()` 简写等\n * 既有写法;运行时额外识别返回值:handler 返回严格的 `false` 视为下游停滞\n * (背压),SDK 会统计连续背压次数,配合 MQTTConfig.maxBackpressuredDeliveries\n * 可在阈值内自动移除订阅。\n */\nexport type TopicHandler = (topic: string, payload: string) => void;\n\nfunction parseWorkspaceIDFromApiKey(apiKey: string): string | undefined {\n // Match the backend's sk-<type>-ws<base36>_<secret> contract, not only\n // service keys. Other workspace-encoded keys use the same MQTT identity.\n const match = /^sk-[^-]+-ws([0-9a-zA-Z]+)_.+$/.exec(apiKey.trim());\n if (!match) return undefined;\n\n // Keep the int64 workspace ID exact; parseInt can round IDs above 2^53.\n let workspaceID = 0n;\n for (const digit of match[1]) {\n workspaceID = workspaceID * 36n + BigInt(parseInt(digit, 36));\n if (workspaceID > 9223372036854775807n) return undefined;\n }\n return workspaceID > 0n ? workspaceID.toString() : undefined;\n}\n\nfunction generateRandomString(length = 8): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let result = '';\n for (let i = 0; i < length; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n\ninterface Subscription {\n topic: string;\n handler: TopicHandler;\n /** handler 连续返回 false(背压)的次数,正常返回时清零。 */\n backpressuredCount?: number;\n}\n\nexport class Tier0MQClient {\n private client: MqttClient | null = null;\n private config: MQTTConfig;\n private explicitClientId: boolean;\n private explicitUsername: boolean;\n private listeners: { [K in keyof MQTTEventMap]?: Array<MQTTEventMap[K]> } = {};\n private subscriptions: Subscription[] = [];\n private connectingPromise: Promise<void> | null = null;\n private _connected = false;\n\n constructor(config?: MQTTConfig) {\n this.explicitClientId = config?.clientId !== undefined;\n this.explicitUsername = config?.username !== undefined;\n const envHost = getEnvVar('TIER0_MQTT_HOST');\n const envPort = getEnvVar('TIER0_MQTT_PORT');\n\n const host = config?.host || envHost || '';\n const port = config?.port || parseInt(envPort || '8084') || 8084;\n const password = config?.password || getEnvVar('TIER0_API_KEY') || '';\n\n // 从 apiKey 解析 workspaceID,自动生成 clientId 和 username\n const workspaceID = config?.clientId && config?.username\n ? undefined\n : parseWorkspaceIDFromApiKey(password);\n\n const autoClientId = workspaceID\n ? `${workspaceID}&${generateRandomString(8)}`\n : undefined;\n const autoUsername = workspaceID\n ? `${workspaceID}&open`\n : undefined;\n\n this.config = {\n host,\n port,\n clientId: config?.clientId || autoClientId,\n username: config?.username || autoUsername,\n password,\n keepAlive: config?.keepAlive ?? 60,\n reconnectPeriod: config?.reconnectPeriod ?? 5000,\n connectTimeout: config?.connectTimeout ?? 30000,\n clean: config?.clean ?? true,\n ...config,\n };\n }\n\n private get mqttUrl(): string {\n const { host, port, secure } = this.config;\n if (!host) {\n throw new Error(\n 'MQTT host is required. Provide it via MQTTConfig or TIER0_MQTT_HOST environment variable.'\n );\n }\n const normalizedHost = host.trim().replace(/\\/+$/, '');\n if (/^wss?:\\/\\//i.test(normalizedHost)) {\n const url = toWebSocketUrl(normalizedHost);\n if (!url) throw new TypeError('Invalid MQTT WebSocket URL');\n return url;\n }\n // 裸 host(可含端口):按运行环境自适应 ws/wss\n const endpoint = parseMqttBroker(normalizedHost);\n const hostname = endpoint?.hostname ?? normalizedHost;\n // 裸 host:port 和 ws(s) URL 的端口按 WebSocket 端口处理;\n // tcp/mqtt/mqtts 的端口是 TCP 端口(如 1883),wss 下不能沿用,回退到配置的 port\n const effectivePort =\n !endpoint?.scheme || endpoint.scheme === 'ws' || endpoint.scheme === 'wss'\n ? endpoint?.port ?? port\n : port;\n const useSecure = secure ?? (isBrowserHttps() || effectivePort === DEFAULT_WSS_PORT);\n return `${useSecure ? 'wss' : 'ws'}://${hostname}:${effectivePort}/mqtt`;\n }\n\n // 内部确保已连接(懒连接)\n private async ensureConnected(): Promise<void> {\n if (this._connected) return;\n if (this.connectingPromise) return this.connectingPromise;\n\n void this.mqttUrl;\n if (!this.config.password || !this.config.username || !this.config.clientId) {\n throw new Error('Tier0 SDK v2: provide a workspace-encoded API key or explicit MQTT username, clientId and password.');\n }\n\n this.connectingPromise = this.doConnect();\n return this.connectingPromise;\n }\n\n private doConnect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const options: IClientOptions = {\n clientId: this.config.clientId,\n username: this.config.username,\n password: this.config.password,\n keepalive: this.config.keepAlive,\n reconnectPeriod: this.config.reconnectPeriod,\n connectTimeout: this.config.connectTimeout,\n clean: this.config.clean,\n };\n\n this.client = mqtt.connect(this.mqttUrl, options);\n\n this.client.on('connect', () => {\n this._connected = true;\n this.connectingPromise = null;\n this.emit('connect');\n // 断连重连后自动恢复所有订阅\n this.resubscribeAll();\n resolve();\n });\n\n this.client.on('error', (err) => {\n this.emit('error', err);\n this.connectingPromise = null;\n reject(err);\n });\n\n this.client.on('disconnect', () => {\n this._connected = false;\n this.emit('disconnect');\n });\n\n this.client.on('close', () => {\n this._connected = false;\n });\n\n this.client.on(\n 'message',\n (topic: string, payload: Buffer, _packet: mqtt.IPublishPacket) => {\n const payloadStr = payload.toString();\n // 复制一份快照:handler 触发背压自动退订时会重建 this.subscriptions\n this.subscriptions.slice().forEach((sub) => {\n if (this.topicMatch(sub.topic, topic)) {\n try {\n // 公开类型为 void 返回(兼容 async/简写 handler),运行时仅识别严格的 false\n const accepted = (\n sub.handler as (t: string, p: string) => unknown\n )(topic, payloadStr);\n if (accepted === false) {\n sub.backpressuredCount = (sub.backpressuredCount ?? 0) + 1;\n const limit = this.config.maxBackpressuredDeliveries ?? 0;\n if (limit > 0 && sub.backpressuredCount >= limit) {\n this.unsubscribe(sub.topic, sub.handler);\n this.emit('subscriptionDropped', {\n topic: sub.topic,\n reason: `handler 连续 ${sub.backpressuredCount} 次返回背压(false),已按 maxBackpressuredDeliveries=${limit} 自动退订`,\n });\n }\n } else {\n sub.backpressuredCount = 0;\n }\n } catch (e) {\n this.emit('error', e as Error);\n }\n }\n });\n }\n );\n });\n }\n\n // 显式连接(需要等待连接完成时使用)\n async connect(config?: MQTTConfig): Promise<void> {\n if (config) {\n const previousPassword = this.config.password;\n this.config = { ...this.config, ...config };\n if (config.clientId !== undefined) this.explicitClientId = true;\n if (config.username !== undefined) this.explicitUsername = true;\n // A connect-time key may belong to a different workspace. Preserve only\n // caller-supplied identity fields, not values derived from the previous key.\n if (config.password !== undefined && config.password !== previousPassword) {\n const workspaceID = parseWorkspaceIDFromApiKey(config.password);\n if (!this.explicitClientId) {\n this.config.clientId = workspaceID ? workspaceID + '&' + generateRandomString(8) : undefined;\n }\n if (!this.explicitUsername) this.config.username = workspaceID ? workspaceID + '&open' : undefined;\n }\n }\n return this.ensureConnected();\n }\n\n // 订阅 topic,内部 qos 固定为 1,handler 接收 (topic, payload)\n // 无需先调用 connect(),自动处理连接\n subscribe(topic: string, handler: TopicHandler): void {\n // 避免重复注册同一 topic 的同一 handler\n const exists = this.subscriptions.some(\n (s) => s.topic === topic && s.handler === handler\n );\n if (exists) {\n return;\n }\n\n this.subscriptions.push({ topic, handler });\n\n if (this._connected && this.client) {\n this.client.subscribe(topic, { qos: 1 as 0 | 1 | 2 }, (err) => {\n if (err) {\n this.emit('error', err);\n // 订阅失败时回滚\n this.subscriptions = this.subscriptions.filter(\n (s) => !(s.topic === topic && s.handler === handler)\n );\n }\n });\n } else {\n // 未连接时触发懒连接,连接成功后 resubscribeAll 会自动 subscribe\n this.ensureConnected().catch((err) => this.emit('error', err));\n }\n }\n\n // 取消订阅:如果提供了 handler 则仅移除该 handler,否则移除该 topic 下所有 handler\n unsubscribe(topic: string, handler?: TopicHandler): void {\n if (handler) {\n this.subscriptions = this.subscriptions.filter(\n (s) => !(s.topic === topic && s.handler === handler)\n );\n } else {\n this.subscriptions = this.subscriptions.filter((s) => s.topic !== topic);\n }\n\n if (!this.client) return;\n\n // 如果该 topic 下已无任何 handler,则向 broker 发送 unsubscribe\n const stillHasHandler = this.subscriptions.some((s) => s.topic === topic);\n if (!stillHasHandler) {\n this.client.unsubscribe(topic, (err) => {\n if (err) {\n this.emit('error', err);\n }\n });\n }\n }\n\n // 发布消息,无需先调用 connect(),内部自动处理连接\n async publish(\n topic: string,\n payload: string | object,\n options?: { qos?: number; retain?: boolean }\n ): Promise<void> {\n await this.ensureConnected();\n\n const message =\n typeof payload === 'string' ? payload : JSON.stringify(payload);\n this.client!.publish(topic, message, {\n qos: (options?.qos ?? 0) as 0 | 1 | 2,\n retain: options?.retain ?? false,\n });\n }\n\n disconnect(): void {\n if (!this.client) return;\n\n this.client.end(true, () => {\n this.emit('disconnect');\n });\n this.client = null;\n this._connected = false;\n this.connectingPromise = null;\n this.subscriptions = [];\n }\n\n on<K extends keyof MQTTEventMap>(event: K, handler: MQTTEventMap[K]): void {\n if (!this.listeners[event]) {\n this.listeners[event] = [];\n }\n this.listeners[event]!.push(handler);\n }\n\n off<K extends keyof MQTTEventMap>(event: K, handler: MQTTEventMap[K]): void {\n const handlers = this.listeners[event];\n if (!handlers) return;\n const idx = handlers.indexOf(handler);\n if (idx !== -1) {\n handlers.splice(idx, 1);\n }\n }\n\n private emit<K extends keyof MQTTEventMap>(\n event: K,\n ...args: Parameters<MQTTEventMap[K]>\n ): void {\n const handlers = this.listeners[event];\n if (!handlers) return;\n handlers.forEach((h) => (h as any)(...args));\n }\n\n // 断连后自动重新订阅所有已注册的 topic\n private resubscribeAll(): void {\n if (!this.client) return;\n\n // 按 topic 去重,每个 topic 只订阅一次\n const uniqueTopics = Array.from(\n new Set(this.subscriptions.map((s) => s.topic))\n );\n uniqueTopics.forEach((topic) => {\n this.client!.subscribe(topic, { qos: 1 as 0 | 1 | 2 }, (err) => {\n if (err) {\n this.emit('error', err);\n }\n });\n });\n }\n\n // 判断接收到的 topic 是否匹配订阅的 topic(支持通配符 # 和 +)\n private topicMatch(subscribed: string, received: string): boolean {\n const subParts = subscribed.split('/');\n const recParts = received.split('/');\n\n for (let i = 0; i < subParts.length; i++) {\n const sp = subParts[i];\n const rp = recParts[i];\n\n if (sp === '#') {\n // # 必须是最后一个层级,匹配后面所有\n return true;\n }\n if (sp === '+') {\n // + 匹配单个层级\n continue;\n }\n if (sp !== rp) {\n return false;\n }\n }\n\n return subParts.length === recParts.length;\n }\n\n get isConnected(): boolean {\n return this._connected;\n }\n\n get subscribedTopics(): string[] {\n return Array.from(new Set(this.subscriptions.map((s) => s.topic)));\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mq/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC","sourcesContent":["export { Tier0MQClient } from './client.js';\r\nexport type { TopicHandler } from './client.js';\r\nexport {\r\n parseMqttBroker,\r\n toWebSocketUrl,\r\n DEFAULT_WSS_PORT,\r\n DEFAULT_WS_PATH,\r\n} from './broker.js';\r\nexport type { MqttBrokerEndpoint, ToWebSocketUrlOptions } from './broker.js';\r\nexport type { MQTTConfig, MQTTMessage, MQTTEventMap } from './types.js';\r\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mq/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC","sourcesContent":["export { Tier0MQClient } from './client.js';\nexport type { TopicHandler } from './client.js';\nexport {\n parseMqttBroker,\n toWebSocketUrl,\n DEFAULT_WSS_PORT,\n DEFAULT_WS_PATH,\n} from './broker.js';\nexport type { MqttBrokerEndpoint, ToWebSocketUrlOptions } from './broker.js';\nexport type { MQTTConfig, MQTTMessage, MQTTEventMap } from './types.js';\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/mq/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface MQTTConfig {\r\n /**\r\n * broker 地址。支持三种形态:\r\n * - 完整 WebSocket URL,如 `wss://host:8084/mqtt`(推荐浏览器场景显式使用);\r\n * - 裸 `host`(可含端口,如 `host:8084`);\r\n * - 平台 `/openapi/v1/info` 返回的原始 `mqttBroker`(如 `tcp://host:1883`,\r\n * 建议先用 {@link toWebSocketUrl} 归一后再传入,见 `@tier0/sdk-v2/mq` 的 broker 工具)。\r\n */\r\n host?: string;\r\n /** WebSocket port fallback when host has no embedded WebSocket port. */\r\n port?: number;\r\n /**\r\n * 当 host 不带 ws(s) scheme 时,显式指定用 wss(true) 还是 ws(false)。\r\n * 缺省自适应:浏览器 https 页面或端口 8084 用 wss,其余用 ws。\r\n */\r\n secure?: boolean;\r\n clientId?: string;\r\n username?: string;\r\n /** Original API key; connect-time changes refresh automatically derived identity fields. */\r\n password?: string;\r\n keepAlive?: number;\r\n reconnectPeriod?: number;\r\n connectTimeout?: number;\r\n clean?: boolean;\r\n /**\r\n * 背压保护阈值:handler 连续返回 false(表示下游停滞/消费者已断开)达到\r\n * 该次数后,SDK 自动移除该订阅并向 broker 退订,避免消息在无消费者的\r\n * 应用内队列中持续堆积。0 或不设置表示关闭该保护(保持原有行为)。\r\n */\r\n maxBackpressuredDeliveries?: number;\r\n}\r\n\r\nexport type MQTTMessage = {\r\n topic: string;\r\n payload: string | Buffer;\r\n qos: number;\r\n retain: boolean;\r\n};\r\n\r\nexport type MQTTEventMap = {\r\n connect: () => void;\r\n disconnect: () => void;\r\n error: (error: Error) => void;\r\n message: (message: MQTTMessage) => void;\r\n /** 订阅因持续背压被 SDK 自动移除时触发。 */\r\n subscriptionDropped: (info: { topic: string; reason: string }) => void;\r\n};\r\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/mq/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface MQTTConfig {\n /**\n * broker 地址。支持三种形态:\n * - 完整 WebSocket URL,如 `wss://host:8084/mqtt`(推荐浏览器场景显式使用);\n * - 裸 `host`(可含端口,如 `host:8084`);\n * - 平台 `/openapi/v1/info` 返回的原始 `mqttBroker`(如 `tcp://host:1883`,\n * 建议先用 {@link toWebSocketUrl} 归一后再传入,见 `@tier0/sdk-v2/mq` 的 broker 工具)。\n */\n host?: string;\n /** WebSocket port fallback when host has no embedded WebSocket port. */\n port?: number;\n /**\n * 当 host 不带 ws(s) scheme 时,显式指定用 wss(true) 还是 ws(false)。\n * 缺省自适应:浏览器 https 页面或端口 8084 用 wss,其余用 ws。\n */\n secure?: boolean;\n clientId?: string;\n username?: string;\n /** Original API key; connect-time changes refresh automatically derived identity fields. */\n password?: string;\n keepAlive?: number;\n reconnectPeriod?: number;\n connectTimeout?: number;\n clean?: boolean;\n /**\n * 背压保护阈值:handler 连续返回 false(表示下游停滞/消费者已断开)达到\n * 该次数后,SDK 自动移除该订阅并向 broker 退订,避免消息在无消费者的\n * 应用内队列中持续堆积。0 或不设置表示关闭该保护(保持原有行为)。\n */\n maxBackpressuredDeliveries?: number;\n}\n\nexport type MQTTMessage = {\n topic: string;\n payload: string | Buffer;\n qos: number;\n retain: boolean;\n};\n\nexport type MQTTEventMap = {\n connect: () => void;\n disconnect: () => void;\n error: (error: Error) => void;\n message: (message: MQTTMessage) => void;\n /** 订阅因持续背压被 SDK 自动移除时触发。 */\n subscriptionDropped: (info: { topic: string; reason: string }) => void;\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-context.js","sourceRoot":"","sources":["../../src/runtime-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,8FAA8F,CAC/F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,sBAAsB,KAAK,uDAAuD;YAChF,0GAA0G,CAC7G,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { getEnvVar } from './runtime-env.js';\r\n\r\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\r\n\r\n/**\r\n * Return the agent-platform app id injected by the Tier0 application runtime.\r\n *\r\n * Throws rather than passing a suspect value through: a wrong app id has no\r\n * symptom at call time — the notification is still accepted — it only makes the\r\n * server-side app lookup miss forever, leaving the message with no App name,\r\n * no icon and no Open button.\r\n */\r\nexport function getCurrentAppId(): string {\r\n const appId = getEnvVar('APP_ID')?.trim();\r\n if (!appId) {\r\n throw new Error(\r\n 'Tier0 SDK: current app ID is required. Run inside a Tier0 application runtime or set APP_ID.'\r\n );\r\n }\r\n if (!UUID_RE.test(appId)) {\r\n throw new Error(\r\n `Tier0 SDK: APP_ID=\"${appId}\" is not an agent-platform app id (expected a UUID). ` +\r\n 'The MonoApp scaffold sets APP_ID to the deployment session id; the platform must inject the real app id.'\r\n );\r\n }\r\n return appId;\r\n}\r\n"]}
1
+ {"version":3,"file":"runtime-context.js","sourceRoot":"","sources":["../../src/runtime-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;IAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,8FAA8F,CAC/F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,sBAAsB,KAAK,uDAAuD;YAChF,0GAA0G,CAC7G,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { getEnvVar } from './runtime-env.js';\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Return the agent-platform app id injected by the Tier0 application runtime.\n *\n * Throws rather than passing a suspect value through: a wrong app id has no\n * symptom at call time — the notification is still accepted — it only makes the\n * server-side app lookup miss forever, leaving the message with no App name,\n * no icon and no Open button.\n */\nexport function getCurrentAppId(): string {\n const appId = getEnvVar('APP_ID')?.trim();\n if (!appId) {\n throw new Error(\n 'Tier0 SDK: current app ID is required. Run inside a Tier0 application runtime or set APP_ID.'\n );\n }\n if (!UUID_RE.test(appId)) {\n throw new Error(\n `Tier0 SDK: APP_ID=\"${appId}\" is not an agent-platform app id (expected a UUID). ` +\n 'The MonoApp scaffold sets APP_ID to the deployment session id; the platform must inject the real app id.'\n );\n }\n return appId;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-env.js","sourceRoot":"","sources":["../../src/runtime-env.ts"],"names":[],"mappings":"AAAA,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YAClD,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["function getProcessEnvVar(key: string): string | undefined {\r\n try {\r\n if (typeof process !== 'undefined' && process.env) {\r\n return process.env[key];\r\n }\r\n } catch {\r\n // ignore\r\n }\r\n return undefined;\r\n}\r\n\r\nexport function getEnvVar(key: string): string | undefined {\r\n return getProcessEnvVar(key);\r\n}\r\n"]}
1
+ {"version":3,"file":"runtime-env.js","sourceRoot":"","sources":["../../src/runtime-env.ts"],"names":[],"mappings":"AAAA,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YAClD,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["function getProcessEnvVar(key: string): string | undefined {\n try {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key];\n }\n } catch {\n // ignore\n }\n return undefined;\n}\n\nexport function getEnvVar(key: string): string | undefined {\n return getProcessEnvVar(key);\n}\n"]}