@tier0/sdk-v2 0.9.10 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -21
- package/README.md +160 -24
- package/dist/cjs/client.d.ts +79 -3
- package/dist/cjs/client.js +4 -6
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/cloud-server.d.ts +33 -26
- package/dist/cjs/cloud-server.js +102 -100
- package/dist/cjs/cloud-server.js.map +1 -1
- package/dist/cjs/cloud-transport.d.ts +15 -20
- package/dist/cjs/cloud-transport.js +87 -68
- package/dist/cjs/cloud-transport.js.map +1 -1
- package/dist/cjs/data/bootstrap.js +3 -4
- package/dist/cjs/data/bootstrap.js.map +1 -1
- package/dist/cjs/data/call.d.ts +1 -3
- package/dist/cjs/data/call.js +8 -12
- package/dist/cjs/data/call.js.map +1 -1
- package/dist/cjs/data/client.d.ts +8 -7
- package/dist/cjs/data/client.js +24 -11
- package/dist/cjs/data/client.js.map +1 -1
- package/dist/cjs/data/collection.d.ts +3 -5
- package/dist/cjs/data/collection.js +3 -4
- package/dist/cjs/data/collection.js.map +1 -1
- package/dist/cjs/data/context.d.ts +5 -11
- package/dist/cjs/data/context.js +21 -36
- package/dist/cjs/data/context.js.map +1 -1
- package/dist/cjs/data/error.js +1 -1
- package/dist/cjs/data/error.js.map +1 -1
- package/dist/cjs/data/index.d.ts +5 -3
- package/dist/cjs/data/index.js +5 -5
- package/dist/cjs/data/index.js.map +1 -1
- package/dist/cjs/data/relay.d.ts +51 -0
- package/dist/cjs/data/relay.js +371 -0
- package/dist/cjs/data/relay.js.map +1 -0
- package/dist/cjs/data/subscribe.d.ts +22 -38
- package/dist/cjs/data/subscribe.js +22 -28
- package/dist/cjs/data/subscribe.js.map +1 -1
- package/dist/cjs/event-stream.d.ts +28 -0
- package/dist/cjs/event-stream.js +222 -0
- package/dist/cjs/event-stream.js.map +1 -0
- package/dist/cjs/mq/client.d.ts +32 -1
- package/dist/cjs/mq/client.js +404 -69
- package/dist/cjs/mq/client.js.map +1 -1
- package/dist/cjs/mq/index.d.ts +1 -0
- package/dist/cjs/mq/index.js +3 -1
- package/dist/cjs/mq/index.js.map +1 -1
- package/dist/cjs/mq/topic.d.ts +29 -0
- package/dist/cjs/mq/topic.js +48 -0
- package/dist/cjs/mq/topic.js.map +1 -0
- package/dist/cjs/mq/types.d.ts +21 -1
- package/dist/cjs/mq/types.js.map +1 -1
- package/dist/cjs/openapi/api.generated.d.ts +20 -0
- package/dist/cjs/openapi/api.generated.js +11 -0
- package/dist/cjs/openapi/api.generated.js.map +1 -1
- package/dist/cjs/openapi/client.d.ts +46 -0
- package/dist/cjs/openapi/client.js +158 -10
- package/dist/cjs/openapi/client.js.map +1 -1
- package/dist/cjs/openapi/codecs.generated.d.ts +74 -0
- package/dist/cjs/openapi/codecs.generated.js +1 -1
- package/dist/cjs/openapi/codecs.generated.js.map +1 -1
- package/dist/cjs/openapi/types.generated.d.ts +205 -1
- package/dist/cjs/openapi/types.generated.js.map +1 -1
- package/dist/cjs/page-runtime.d.ts +48 -0
- package/dist/cjs/page-runtime.js +74 -0
- package/dist/cjs/page-runtime.js.map +1 -0
- package/dist/cjs/platform-proxy.d.ts +17 -0
- package/dist/cjs/platform-proxy.js +52 -0
- package/dist/cjs/platform-proxy.js.map +1 -0
- package/dist/cjs/runtime-context.d.ts +2 -1
- package/dist/cjs/runtime-context.js +6 -2
- package/dist/cjs/runtime-context.js.map +1 -1
- package/dist/cjs/runtime-env.js.map +1 -1
- package/dist/cjs/source.d.ts +19 -1
- package/dist/cjs/source.js +9 -3
- package/dist/cjs/source.js.map +1 -1
- package/dist/cjs/stream.js +3 -2
- package/dist/cjs/stream.js.map +1 -1
- package/dist/cjs/streams/contract.generated.d.ts +7 -1
- package/dist/cjs/streams/contract.generated.js.map +1 -1
- package/dist/cjs/streams/messages.js +1 -10
- package/dist/cjs/streams/messages.js.map +1 -1
- package/dist/cjs/streams/requests.d.ts +3 -0
- package/dist/cjs/streams/requests.js +5 -2
- package/dist/cjs/streams/requests.js.map +1 -1
- package/dist/cjs/streams/transport.d.ts +1 -13
- package/dist/cjs/streams/transport.js +2 -18
- package/dist/cjs/streams/transport.js.map +1 -1
- package/dist/cjs/uns-v2/client.d.ts +1 -6
- package/dist/cjs/uns-v2/client.js +30 -6
- package/dist/cjs/uns-v2/client.js.map +1 -1
- package/dist/cjs/uns-v2/data-client.d.ts +5 -2
- package/dist/cjs/uns-v2/data-client.js +7 -4
- package/dist/cjs/uns-v2/data-client.js.map +1 -1
- package/dist/cjs/uns-v2/generated.d.ts +6783 -4655
- package/dist/cjs/uns-v2/generated.js +221 -65
- package/dist/cjs/uns-v2/generated.js.map +1 -1
- package/dist/cjs/uns-v2/sse.d.ts +7 -1
- package/dist/cjs/uns-v2/sse.js +12 -2
- package/dist/cjs/uns-v2/sse.js.map +1 -1
- package/dist/esm/client.d.ts +79 -3
- package/dist/esm/client.js +5 -7
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cloud-server.d.ts +33 -26
- package/dist/esm/cloud-server.js +99 -97
- package/dist/esm/cloud-server.js.map +1 -1
- package/dist/esm/cloud-transport.d.ts +15 -20
- package/dist/esm/cloud-transport.js +86 -67
- package/dist/esm/cloud-transport.js.map +1 -1
- package/dist/esm/data/bootstrap.js +3 -4
- package/dist/esm/data/bootstrap.js.map +1 -1
- package/dist/esm/data/call.d.ts +1 -3
- package/dist/esm/data/call.js +9 -13
- package/dist/esm/data/call.js.map +1 -1
- package/dist/esm/data/client.d.ts +8 -7
- package/dist/esm/data/client.js +22 -9
- package/dist/esm/data/client.js.map +1 -1
- package/dist/esm/data/collection.d.ts +3 -5
- package/dist/esm/data/collection.js +3 -4
- package/dist/esm/data/collection.js.map +1 -1
- package/dist/esm/data/context.d.ts +5 -11
- package/dist/esm/data/context.js +19 -33
- package/dist/esm/data/context.js.map +1 -1
- package/dist/esm/data/error.js +1 -1
- package/dist/esm/data/error.js.map +1 -1
- package/dist/esm/data/index.d.ts +5 -3
- package/dist/esm/data/index.js +3 -2
- package/dist/esm/data/index.js.map +1 -1
- package/dist/esm/data/relay.d.ts +51 -0
- package/dist/esm/data/relay.js +368 -0
- package/dist/esm/data/relay.js.map +1 -0
- package/dist/esm/data/subscribe.d.ts +22 -38
- package/dist/esm/data/subscribe.js +22 -27
- package/dist/esm/data/subscribe.js.map +1 -1
- package/dist/esm/event-stream.d.ts +28 -0
- package/dist/esm/event-stream.js +217 -0
- package/dist/esm/event-stream.js.map +1 -0
- package/dist/esm/mq/client.d.ts +32 -1
- package/dist/esm/mq/client.js +404 -69
- package/dist/esm/mq/client.js.map +1 -1
- package/dist/esm/mq/index.d.ts +1 -0
- package/dist/esm/mq/index.js +1 -0
- package/dist/esm/mq/index.js.map +1 -1
- package/dist/esm/mq/topic.d.ts +29 -0
- package/dist/esm/mq/topic.js +44 -0
- package/dist/esm/mq/topic.js.map +1 -0
- package/dist/esm/mq/types.d.ts +21 -1
- package/dist/esm/mq/types.js.map +1 -1
- package/dist/esm/openapi/api.generated.d.ts +20 -0
- package/dist/esm/openapi/api.generated.js +11 -0
- package/dist/esm/openapi/api.generated.js.map +1 -1
- package/dist/esm/openapi/client.d.ts +46 -0
- package/dist/esm/openapi/client.js +157 -10
- package/dist/esm/openapi/client.js.map +1 -1
- package/dist/esm/openapi/codecs.generated.d.ts +74 -0
- package/dist/esm/openapi/codecs.generated.js +1 -1
- package/dist/esm/openapi/codecs.generated.js.map +1 -1
- package/dist/esm/openapi/types.generated.d.ts +205 -1
- package/dist/esm/openapi/types.generated.js.map +1 -1
- package/dist/esm/page-runtime.d.ts +48 -0
- package/dist/esm/page-runtime.js +65 -0
- package/dist/esm/page-runtime.js.map +1 -0
- package/dist/esm/platform-proxy.d.ts +17 -0
- package/dist/esm/platform-proxy.js +47 -0
- package/dist/esm/platform-proxy.js.map +1 -0
- package/dist/esm/runtime-context.d.ts +2 -1
- package/dist/esm/runtime-context.js +6 -2
- package/dist/esm/runtime-context.js.map +1 -1
- package/dist/esm/runtime-env.js.map +1 -1
- package/dist/esm/source.d.ts +19 -1
- package/dist/esm/source.js +10 -4
- package/dist/esm/source.js.map +1 -1
- package/dist/esm/stream.js +4 -3
- package/dist/esm/stream.js.map +1 -1
- package/dist/esm/streams/contract.generated.d.ts +7 -1
- package/dist/esm/streams/contract.generated.js.map +1 -1
- package/dist/esm/streams/messages.js +1 -10
- package/dist/esm/streams/messages.js.map +1 -1
- package/dist/esm/streams/requests.d.ts +3 -0
- package/dist/esm/streams/requests.js +5 -2
- package/dist/esm/streams/requests.js.map +1 -1
- package/dist/esm/streams/transport.d.ts +1 -13
- package/dist/esm/streams/transport.js +1 -17
- package/dist/esm/streams/transport.js.map +1 -1
- package/dist/esm/uns-v2/client.d.ts +1 -6
- package/dist/esm/uns-v2/client.js +30 -6
- package/dist/esm/uns-v2/client.js.map +1 -1
- package/dist/esm/uns-v2/data-client.d.ts +5 -2
- package/dist/esm/uns-v2/data-client.js +8 -5
- package/dist/esm/uns-v2/data-client.js.map +1 -1
- package/dist/esm/uns-v2/generated.d.ts +6783 -4655
- package/dist/esm/uns-v2/generated.js +221 -65
- package/dist/esm/uns-v2/generated.js.map +1 -1
- package/dist/esm/uns-v2/sse.d.ts +7 -1
- package/dist/esm/uns-v2/sse.js +12 -2
- package/dist/esm/uns-v2/sse.js.map +1 -1
- package/package.json +25 -12
- package/skills/tier0-sdk-v2/SKILL.md +11 -5
- package/skills/tier0-sdk-v2/references/integration.md +14 -24
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/SKILL.md +11 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/consistency.md +6 -10
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/errors.md +20 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-data/references/read-write.md +46 -13
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/SKILL.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-members/references/app-members.md +5 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/SKILL.md +25 -0
- package/skills/tier0-sdk-v2/tier0-sdk-v2-mq/references/quickstart.md +1 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-notifications/references/send.md +6 -6
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/SKILL.md +4 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/changes.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/create.md +26 -36
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/delete.md +9 -23
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/protocols.md +12 -29
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/read.md +4 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-source/references/update.md +5 -5
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/SKILL.md +3 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/create.md +23 -9
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/deploy.md +14 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/http-server.md +4 -4
- package/skills/tier0-sdk-v2/tier0-sdk-v2-stream/references/read.md +4 -3
- package/skills/tier0-sdk-v2/tier0-sdk-v2-streams/SKILL.md +12 -12
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/SKILL.md +2 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/create.md +2 -2
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/delete.md +22 -1
- package/skills/tier0-sdk-v2/tier0-sdk-v2-uns/references/read.md +5 -6
- package/dist/cjs/app-identity-contract.generated.d.ts +0 -22
- package/dist/cjs/app-identity-contract.generated.js +0 -51
- package/dist/cjs/app-identity-contract.generated.js.map +0 -1
- package/dist/cjs/runtime.d.ts +0 -13
- package/dist/cjs/runtime.js +0 -51
- package/dist/cjs/runtime.js.map +0 -1
- package/dist/esm/app-identity-contract.generated.d.ts +0 -22
- package/dist/esm/app-identity-contract.generated.js +0 -48
- package/dist/esm/app-identity-contract.generated.js.map +0 -1
- package/dist/esm/runtime.d.ts +0 -13
- package/dist/esm/runtime.js +0 -46
- package/dist/esm/runtime.js.map +0 -1
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* `delete` always passes, a late one too, and forgets the record's
|
|
24
24
|
* revision, since a record written again under the same ID may start
|
|
25
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
26
|
* - Aborting `signal` or leaving the loop ends the iteration without error.
|
|
27
|
+
* - The stream runs as the App: it sees every change of the collections
|
|
28
|
+
* granted to the App. A server opens one per process and fans it out to
|
|
29
|
+
* browsers with `createChangeRelay`, filtering per browser by what the App
|
|
30
|
+
* lets that visitor see.
|
|
29
31
|
*
|
|
30
32
|
* Events carry identities only, never record data; read the record to show it.
|
|
31
33
|
*/
|
|
@@ -43,18 +45,6 @@ const RESET_PAUSE_MS = 1_000;
|
|
|
43
45
|
const HEALTHY_CONNECTION_MS = 30_000;
|
|
44
46
|
/** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */
|
|
45
47
|
const SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;
|
|
46
|
-
/**
|
|
47
|
-
* The event as an employee's browser may see it: a change in a collection
|
|
48
|
-
* with employee data rules becomes a collection-level notice without record
|
|
49
|
-
* ID or revision, so it does not reveal records the employee may not read.
|
|
50
|
-
* Other events pass unchanged.
|
|
51
|
-
*/
|
|
52
|
-
export function redactRestricted(event) {
|
|
53
|
-
if (event.type !== 'change' || !event.restricted || event.change === 'collection')
|
|
54
|
-
return event;
|
|
55
|
-
const { eventId, collection, collectionId, occurredAt, restricted } = event;
|
|
56
|
-
return { type: 'change', eventId, collection, collectionId, occurredAt, restricted, change: 'collection' };
|
|
57
|
-
}
|
|
58
48
|
function reconnectDelay(failures, afterMs) {
|
|
59
49
|
if (afterMs !== undefined)
|
|
60
50
|
return Math.min(afterMs, RECONNECT_MAX_MS);
|
|
@@ -106,7 +96,7 @@ function pause(ms, signal) {
|
|
|
106
96
|
});
|
|
107
97
|
}
|
|
108
98
|
export function createSubscribe(runtime) {
|
|
109
|
-
return async function* subscribe(options = {}) {
|
|
99
|
+
return async function* subscribe(options = {}, observer = {}) {
|
|
110
100
|
const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];
|
|
111
101
|
if (definitions.length < 1 || definitions.length > UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {
|
|
112
102
|
throw new TypeError(`data.subscribe watches 1–${UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);
|
|
@@ -117,10 +107,7 @@ export function createSubscribe(runtime) {
|
|
|
117
107
|
}
|
|
118
108
|
if (options.lastEventId !== undefined)
|
|
119
109
|
assertEventId(options.lastEventId);
|
|
120
|
-
const redact = (options.restricted ?? 'collection') === 'collection';
|
|
121
110
|
const idleMs = options.idleTimeoutMs ?? SUBSCRIBE_IDLE_TIMEOUT_MS;
|
|
122
|
-
// The subject of the request that subscribed; an App-level subscription carries none.
|
|
123
|
-
const call = { dataSubject: runtime.subject() };
|
|
124
111
|
const stop = new AbortController();
|
|
125
112
|
const onAbort = () => stop.abort();
|
|
126
113
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
@@ -130,7 +117,9 @@ export function createSubscribe(runtime) {
|
|
|
130
117
|
let failures = 0;
|
|
131
118
|
const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);
|
|
132
119
|
/** Waits, longer each time, before the next attempt. */
|
|
133
|
-
const backOff = async (
|
|
120
|
+
const backOff = async (error) => {
|
|
121
|
+
observer.retrying?.(error);
|
|
122
|
+
const afterMs = error.retry.afterMs;
|
|
134
123
|
failures += 1;
|
|
135
124
|
await pause(reconnectDelay(failures, afterMs), stop.signal);
|
|
136
125
|
};
|
|
@@ -144,7 +133,7 @@ export function createSubscribe(runtime) {
|
|
|
144
133
|
// A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.
|
|
145
134
|
if (!(error instanceof DataError) || !error.retry.safe || stop.signal.aborted)
|
|
146
135
|
throw error;
|
|
147
|
-
await backOff(error
|
|
136
|
+
await backOff(error);
|
|
148
137
|
continue;
|
|
149
138
|
}
|
|
150
139
|
const aliases = new Map();
|
|
@@ -163,17 +152,24 @@ export function createSubscribe(runtime) {
|
|
|
163
152
|
const openedAt = Date.now();
|
|
164
153
|
let healthy = false;
|
|
165
154
|
let reset = false;
|
|
155
|
+
let streaming = false;
|
|
166
156
|
try {
|
|
167
157
|
arm();
|
|
168
|
-
const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, {
|
|
158
|
+
const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, { lastEventId, signal: connection.signal });
|
|
169
159
|
for await (const event of stream) {
|
|
170
160
|
arm();
|
|
171
161
|
if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
|
|
172
162
|
healthy = true;
|
|
163
|
+
if (!streaming) {
|
|
164
|
+
streaming = true;
|
|
165
|
+
observer.connected?.();
|
|
166
|
+
}
|
|
173
167
|
if (event.type === 'heartbeat') {
|
|
174
168
|
// The connect heartbeat names the current position: resuming from it loses nothing.
|
|
175
|
-
if (event.eventId !== undefined)
|
|
169
|
+
if (event.eventId !== undefined) {
|
|
176
170
|
lastEventId = event.eventId;
|
|
171
|
+
observer.position?.(event.eventId);
|
|
172
|
+
}
|
|
177
173
|
continue;
|
|
178
174
|
}
|
|
179
175
|
if (event.type === 'reset') {
|
|
@@ -187,20 +183,19 @@ export function createSubscribe(runtime) {
|
|
|
187
183
|
healthy = true;
|
|
188
184
|
// A dropped change is still a resume position.
|
|
189
185
|
lastEventId = event.eventId;
|
|
186
|
+
observer.position?.(event.eventId);
|
|
190
187
|
if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change))
|
|
191
188
|
continue;
|
|
192
|
-
|
|
189
|
+
yield {
|
|
193
190
|
type: 'change',
|
|
194
191
|
eventId: event.eventId,
|
|
195
192
|
collection: aliases.get(event.collectionId) ?? event.collectionId,
|
|
196
193
|
collectionId: event.collectionId,
|
|
197
194
|
occurredAt: event.occurredAt,
|
|
198
|
-
restricted: event.restricted,
|
|
199
195
|
change: event.change,
|
|
200
196
|
recordId: event.recordId,
|
|
201
197
|
revision: event.revision,
|
|
202
198
|
};
|
|
203
|
-
yield redact ? redactRestricted(change) : change;
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
catch (error) {
|
|
@@ -216,7 +211,7 @@ export function createSubscribe(runtime) {
|
|
|
216
211
|
catch (cause) {
|
|
217
212
|
if (!(cause instanceof DataError) || !cause.retry.safe)
|
|
218
213
|
throw cause;
|
|
219
|
-
await backOff(cause
|
|
214
|
+
await backOff(cause);
|
|
220
215
|
continue;
|
|
221
216
|
}
|
|
222
217
|
if (!changed)
|
|
@@ -229,7 +224,7 @@ export function createSubscribe(runtime) {
|
|
|
229
224
|
}
|
|
230
225
|
if (!error.retry.safe)
|
|
231
226
|
throw error;
|
|
232
|
-
await backOff(error
|
|
227
|
+
await backOff(error);
|
|
233
228
|
continue;
|
|
234
229
|
}
|
|
235
230
|
finally {
|
|
@@ -1 +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 { collectionLabel, type CollectionTarget } 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, aliases, or existing-ID references (at most 64); defaults to every declared collection. */\n collections?: readonly (CollectionTarget | 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 a declared collection, or the ID of an existing collection reference. */\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(targets: readonly CollectionTarget[]): Promise<EnsureState>;\n rebuild(before: EnsureState, cause: DataError, targets: readonly CollectionTarget[]): Promise<boolean>;\n resolve(target: CollectionTarget): Promise<string>;\n lookup(target: CollectionTarget | string): CollectionTarget;\n declared: readonly CollectionTarget[];\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(definitions);\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 = await runtime.resolve(definition);\n aliases.set(id, collectionLabel(definition));\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, definitions);\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
|
+
{"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 * - Aborting `signal` or leaving the loop ends the iteration without error.\n * - The stream runs as the App: it sees every change of the collections\n * granted to the App. A server opens one per process and fans it out to\n * browsers with `createChangeRelay`, filtering per browser by what the App\n * lets that visitor see.\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 { DataOpenStream } from './call.js';\nimport { assertEventId, UNS_V2_SUBSCRIBE_MAX_COLLECTIONS, UNS_V2_SUBSCRIBE_MAX_RECORD_IDS } from '../uns-v2/client.js';\nimport { collectionLabel, type CollectionTarget } 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, aliases, or existing-ID references (at most 64); defaults to every declared collection. */\n collections?: readonly (CollectionTarget | string)[];\n /** Only changes of these record IDs (1–100, duplicates ignored), when exactly one collection is watched. */\n recordIds?: readonly string[];\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\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 {\n type: 'change';\n eventId: string;\n /** Alias of a declared collection, or the ID of an existing collection reference. */\n collection: string;\n collectionId: string;\n occurredAt: number;\n change: 'record' | 'delete';\n recordId: string;\n revision: number;\n}\n\n/** `reason`: `CURSOR_EXPIRED`, `SCOPE_CHANGED` (from UNS) or `COLLECTION_RECREATED`. */\nexport type SubscriptionEvent = RecordChange | { type: 'reset'; reason: string; message?: string };\n\n/**\n * What `data.subscribe` tells a server-side consumer about the upstream\n * (`createChangeRelay` uses it; not part of the public API).\n */\nexport interface SubscribeObserver {\n /** A connection is streaming: its first frame (the connect heartbeat) arrived. */\n connected?(): void;\n /** The resume position moved: a heartbeat position or a change, dropped or not. */\n position?(eventId: string): void;\n /** An attempt failed and will be retried after a wait (e.g. 429 over the subscription limit, `APP_NOT_READY`). */\n retrying?(error: DataError): void;\n}\n\nexport interface SubscribeRuntime {\n openStream: DataOpenStream;\n ensure(targets: readonly CollectionTarget[]): Promise<EnsureState>;\n rebuild(before: EnsureState, cause: DataError, targets: readonly CollectionTarget[]): Promise<boolean>;\n resolve(target: CollectionTarget): Promise<string>;\n lookup(target: CollectionTarget | string): CollectionTarget;\n declared: readonly CollectionTarget[];\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 = {}, observer: SubscribeObserver = {}): 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 idleMs = options.idleTimeoutMs ?? SUBSCRIBE_IDLE_TIMEOUT_MS;\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 (error: DataError) => {\n observer.retrying?.(error);\n const afterMs = error.retry.afterMs;\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(definitions);\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);\n continue;\n }\n const aliases = new Map<string, string>();\n for (const definition of definitions) {\n const id = await runtime.resolve(definition);\n aliases.set(id, collectionLabel(definition));\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 let streaming = false;\n try {\n arm();\n const stream = runtime.openStream(\n { collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) },\n { 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 (!streaming) {\n streaming = true;\n observer.connected?.();\n }\n if (event.type === 'heartbeat') {\n // The connect heartbeat names the current position: resuming from it loses nothing.\n if (event.eventId !== undefined) {\n lastEventId = event.eventId;\n observer.position?.(event.eventId);\n }\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 observer.position?.(event.eventId);\n if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change)) continue;\n yield {\n type: 'change',\n eventId: event.eventId,\n collection: aliases.get(event.collectionId) ?? event.collectionId,\n collectionId: event.collectionId,\n occurredAt: event.occurredAt,\n change: event.change,\n recordId: event.recordId,\n revision: event.revision,\n };\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, definitions);\n } catch (cause) {\n if (!(cause instanceof DataError) || !cause.retry.safe) throw cause;\n await backOff(cause);\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);\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"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ServerSentEvent } from './uns-v2/sse.js';
|
|
2
|
+
export interface EventStreamOptions {
|
|
3
|
+
url: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
lastEventId?: string;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
onEvent(event: ServerSentEvent): void;
|
|
8
|
+
/**
|
|
9
|
+
* The stream is back after a pause for the App's sleep or an authorization
|
|
10
|
+
* failure. Events may have been missed, resume position or not.
|
|
11
|
+
*/
|
|
12
|
+
onResume?(): void;
|
|
13
|
+
/** The stream ended for good; it does not reconnect. Not called after `close()`. */
|
|
14
|
+
onClose?(error: EventStreamError): void;
|
|
15
|
+
/** Reconnect when nothing, not even a heartbeat, arrives for this long. Off by default. */
|
|
16
|
+
idleTimeoutMs?: number;
|
|
17
|
+
/** Close the connection while the page is hidden and resume from the last event id when it is visible. Default true. */
|
|
18
|
+
pauseWhenHidden?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface EventStream {
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class EventStreamError extends Error {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly code: string | undefined;
|
|
26
|
+
constructor(status: number, code: string | undefined);
|
|
27
|
+
}
|
|
28
|
+
export declare function openEventStream(options: EventStreamOptions): EventStream;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A server-sent event stream over `fetch`, for browsers. `EventSource` gives up
|
|
3
|
+
* for good on any HTTP error, so a 503 from a sleeping App or a restarting node
|
|
4
|
+
* agent (design 21.1) would end it. This one reads the error and acts on it:
|
|
5
|
+
*
|
|
6
|
+
* - `sbx_sleeping`, `sbx_starting`: stop, and reconnect on the runtime's
|
|
7
|
+
* `resumed`. Without the runtime `sbx_sleeping` ends the stream and
|
|
8
|
+
* `sbx_starting` waits `retry_after_ms`.
|
|
9
|
+
* - an authorization failure: reported to the runtime, then as above. A
|
|
10
|
+
* successful reauthorization also fires `resumed`.
|
|
11
|
+
* - `node_restarting`, `upstream_error` and any other retryable failure, and a
|
|
12
|
+
* broken connection: reconnect by itself after `retry_after_ms` or a backoff,
|
|
13
|
+
* resuming from `Last-Event-ID`.
|
|
14
|
+
* - a non-retryable failure: ends the stream.
|
|
15
|
+
*
|
|
16
|
+
* While the runtime says `sleeping` nothing is sent: those requests would not
|
|
17
|
+
* wake the App anyway.
|
|
18
|
+
*
|
|
19
|
+
* While the page is hidden (`document.visibilityState`) the connection is
|
|
20
|
+
* closed, so a background tab does not hold a long connection to the App;
|
|
21
|
+
* when the page is visible again it reconnects at once from the last event
|
|
22
|
+
* id (`Last-Event-ID`). The server replays what was missed or answers with
|
|
23
|
+
* its own reset; `onResume` is not called for it. `pauseWhenHidden: false`
|
|
24
|
+
* keeps the connection open in the background.
|
|
25
|
+
*/
|
|
26
|
+
import { getPageRuntime, onPageRuntime, isAuthErrorCode, isAppSleeping, readPlatformError, reportAuthError } from './page-runtime.js';
|
|
27
|
+
import { parseServerSentEvents } from './uns-v2/sse.js';
|
|
28
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
29
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
30
|
+
/** A connection alive this long resets the backoff. */
|
|
31
|
+
const HEALTHY_CONNECTION_MS = 30_000;
|
|
32
|
+
/**
|
|
33
|
+
* A paused stream looks again this often while the runtime still says
|
|
34
|
+
* `running`: a routing race can answer `sbx_sleeping` for a running App, and
|
|
35
|
+
* then no `resumed` ever comes. Such a request cannot wake anything.
|
|
36
|
+
*/
|
|
37
|
+
const PAUSE_RECHECK_MS = 30_000;
|
|
38
|
+
export class EventStreamError extends Error {
|
|
39
|
+
status;
|
|
40
|
+
code;
|
|
41
|
+
constructor(status, code) {
|
|
42
|
+
super(`Event stream refused with HTTP ${status}${code ? ` (${code})` : ''}.`);
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.name = 'EventStreamError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function classify(status, failure) {
|
|
49
|
+
const code = failure?.code;
|
|
50
|
+
const runtime = getPageRuntime() !== undefined;
|
|
51
|
+
if (isAuthErrorCode(code)) {
|
|
52
|
+
reportAuthError(code);
|
|
53
|
+
return runtime ? { next: 'pause' } : { next: 'end', error: new EventStreamError(status, code) };
|
|
54
|
+
}
|
|
55
|
+
if (code === 'sbx_sleeping' || code === 'sbx_starting' || code === 'sbx_start_failed') {
|
|
56
|
+
if (runtime)
|
|
57
|
+
return { next: 'pause' };
|
|
58
|
+
if (code !== 'sbx_starting')
|
|
59
|
+
return { next: 'end', error: new EventStreamError(status, code) };
|
|
60
|
+
}
|
|
61
|
+
if (failure?.retryable)
|
|
62
|
+
return { next: 'reconnect', afterMs: failure.retryAfterMs };
|
|
63
|
+
// Not ours to classify: an edge proxy's 5xx passes, a route's 4xx is final.
|
|
64
|
+
if (!failure && status >= 500)
|
|
65
|
+
return { next: 'reconnect' };
|
|
66
|
+
return { next: 'end', error: new EventStreamError(status, code) };
|
|
67
|
+
}
|
|
68
|
+
export function openEventStream(options) {
|
|
69
|
+
const { url, onEvent, onResume, onClose, idleTimeoutMs } = options;
|
|
70
|
+
let lastEventId = options.lastEventId;
|
|
71
|
+
let closed = false;
|
|
72
|
+
let paused = isAppSleeping();
|
|
73
|
+
let resumedFromPause = false;
|
|
74
|
+
let failures = 0;
|
|
75
|
+
let resumes = 0;
|
|
76
|
+
let connection;
|
|
77
|
+
let wake;
|
|
78
|
+
const page = (options.pauseWhenHidden ?? true) && typeof document !== 'undefined' ? document : undefined;
|
|
79
|
+
let hidden = page?.visibilityState === 'hidden';
|
|
80
|
+
const onVisibility = () => {
|
|
81
|
+
hidden = page.visibilityState === 'hidden';
|
|
82
|
+
if (hidden)
|
|
83
|
+
connection?.abort();
|
|
84
|
+
wake?.();
|
|
85
|
+
};
|
|
86
|
+
page?.addEventListener('visibilitychange', onVisibility);
|
|
87
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
88
|
+
const timer = ms === undefined ? undefined : setTimeout(() => done(), ms);
|
|
89
|
+
const done = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
wake = undefined;
|
|
92
|
+
resolve();
|
|
93
|
+
};
|
|
94
|
+
wake = done;
|
|
95
|
+
});
|
|
96
|
+
const offSleeping = onPageRuntime('sleeping', () => {
|
|
97
|
+
paused = true;
|
|
98
|
+
connection?.abort();
|
|
99
|
+
wake?.();
|
|
100
|
+
});
|
|
101
|
+
const offResumed = onPageRuntime('resumed', () => {
|
|
102
|
+
if (paused)
|
|
103
|
+
resumedFromPause = true;
|
|
104
|
+
paused = false;
|
|
105
|
+
resumes += 1;
|
|
106
|
+
wake?.();
|
|
107
|
+
});
|
|
108
|
+
function backoff() {
|
|
109
|
+
failures += 1;
|
|
110
|
+
const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** (failures - 1));
|
|
111
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
112
|
+
}
|
|
113
|
+
async function consume(body, controller) {
|
|
114
|
+
const openedAt = Date.now();
|
|
115
|
+
let idle;
|
|
116
|
+
const arm = () => {
|
|
117
|
+
if (idleTimeoutMs === undefined)
|
|
118
|
+
return;
|
|
119
|
+
clearTimeout(idle);
|
|
120
|
+
idle = setTimeout(() => controller.abort(), idleTimeoutMs);
|
|
121
|
+
};
|
|
122
|
+
arm();
|
|
123
|
+
try {
|
|
124
|
+
for await (const event of parseServerSentEvents(body)) {
|
|
125
|
+
arm();
|
|
126
|
+
if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
|
|
127
|
+
failures = 0;
|
|
128
|
+
if ('comment' in event)
|
|
129
|
+
continue;
|
|
130
|
+
if (event.id !== undefined)
|
|
131
|
+
lastEventId = event.id;
|
|
132
|
+
if ('event' in event)
|
|
133
|
+
onEvent(event);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(idle);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function connect() {
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
connection = controller;
|
|
143
|
+
const headers = { ...options.headers, Accept: 'text/event-stream' };
|
|
144
|
+
if (lastEventId !== undefined)
|
|
145
|
+
headers['Last-Event-ID'] = lastEventId;
|
|
146
|
+
try {
|
|
147
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, { headers, cache: 'no-store', signal: controller.signal });
|
|
148
|
+
const type = response.headers.get('content-type') ?? '';
|
|
149
|
+
if (response.ok && /^text\/event-stream\b/i.test(type) && response.body) {
|
|
150
|
+
if (resumedFromPause) {
|
|
151
|
+
resumedFromPause = false;
|
|
152
|
+
onResume?.();
|
|
153
|
+
}
|
|
154
|
+
await consume(response.body, controller);
|
|
155
|
+
return { next: 'reconnect' };
|
|
156
|
+
}
|
|
157
|
+
const failure = await readPlatformError(response);
|
|
158
|
+
await response.body?.cancel().catch(() => undefined);
|
|
159
|
+
return classify(response.status, failure);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { next: 'reconnect' };
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
connection = undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function run() {
|
|
169
|
+
while (!closed) {
|
|
170
|
+
if (hidden) {
|
|
171
|
+
await sleep();
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (paused) {
|
|
175
|
+
await sleep(getPageRuntime()?.getState() === 'running' ? PAUSE_RECHECK_MS : undefined);
|
|
176
|
+
if (getPageRuntime()?.getState() === 'running' && !closed) {
|
|
177
|
+
resumedFromPause = true;
|
|
178
|
+
paused = false;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const seenResumes = resumes;
|
|
183
|
+
const outcome = await connect();
|
|
184
|
+
if (closed)
|
|
185
|
+
return;
|
|
186
|
+
if (outcome.next === 'end') {
|
|
187
|
+
close();
|
|
188
|
+
onClose?.(outcome.error);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (outcome.next === 'pause') {
|
|
192
|
+
// A `resumed` that arrived while the refusal was on its way already happened.
|
|
193
|
+
if (resumes === seenResumes)
|
|
194
|
+
paused = true;
|
|
195
|
+
else
|
|
196
|
+
resumedFromPause = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// Closed for a hidden page: wait for it to be visible, not for a backoff.
|
|
200
|
+
if (hidden)
|
|
201
|
+
continue;
|
|
202
|
+
if (!paused)
|
|
203
|
+
await sleep(outcome.afterMs ?? backoff());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function close() {
|
|
207
|
+
closed = true;
|
|
208
|
+
offSleeping();
|
|
209
|
+
offResumed();
|
|
210
|
+
page?.removeEventListener('visibilitychange', onVisibility);
|
|
211
|
+
connection?.abort();
|
|
212
|
+
wake?.();
|
|
213
|
+
}
|
|
214
|
+
void run();
|
|
215
|
+
return { close };
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=event-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-stream.js","sourceRoot":"","sources":["../../src/event-stream.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A server-sent event stream over `fetch`, for browsers. `EventSource` gives up\n * for good on any HTTP error, so a 503 from a sleeping App or a restarting node\n * agent (design 21.1) would end it. This one reads the error and acts on it:\n *\n * - `sbx_sleeping`, `sbx_starting`: stop, and reconnect on the runtime's\n * `resumed`. Without the runtime `sbx_sleeping` ends the stream and\n * `sbx_starting` waits `retry_after_ms`.\n * - an authorization failure: reported to the runtime, then as above. A\n * successful reauthorization also fires `resumed`.\n * - `node_restarting`, `upstream_error` and any other retryable failure, and a\n * broken connection: reconnect by itself after `retry_after_ms` or a backoff,\n * resuming from `Last-Event-ID`.\n * - a non-retryable failure: ends the stream.\n *\n * While the runtime says `sleeping` nothing is sent: those requests would not\n * wake the App anyway.\n *\n * While the page is hidden (`document.visibilityState`) the connection is\n * closed, so a background tab does not hold a long connection to the App;\n * when the page is visible again it reconnects at once from the last event\n * id (`Last-Event-ID`). The server replays what was missed or answers with\n * its own reset; `onResume` is not called for it. `pauseWhenHidden: false`\n * keeps the connection open in the background.\n */\nimport { getPageRuntime, onPageRuntime, isAuthErrorCode, isAppSleeping, readPlatformError, reportAuthError, type PlatformError } from './page-runtime.js';\nimport { parseServerSentEvents, type ServerSentEvent } from './uns-v2/sse.js';\n\nconst RECONNECT_BASE_MS = 1_000;\nconst RECONNECT_MAX_MS = 30_000;\n/** A connection alive this long resets the backoff. */\nconst HEALTHY_CONNECTION_MS = 30_000;\n/**\n * A paused stream looks again this often while the runtime still says\n * `running`: a routing race can answer `sbx_sleeping` for a running App, and\n * then no `resumed` ever comes. Such a request cannot wake anything.\n */\nconst PAUSE_RECHECK_MS = 30_000;\n\nexport interface EventStreamOptions {\n url: string;\n headers?: Record<string, string>;\n lastEventId?: string;\n fetch?: typeof globalThis.fetch;\n onEvent(event: ServerSentEvent): void;\n /**\n * The stream is back after a pause for the App's sleep or an authorization\n * failure. Events may have been missed, resume position or not.\n */\n onResume?(): void;\n /** The stream ended for good; it does not reconnect. Not called after `close()`. */\n onClose?(error: EventStreamError): void;\n /** Reconnect when nothing, not even a heartbeat, arrives for this long. Off by default. */\n idleTimeoutMs?: number;\n /** Close the connection while the page is hidden and resume from the last event id when it is visible. Default true. */\n pauseWhenHidden?: boolean;\n}\n\nexport interface EventStream {\n close(): void;\n}\n\nexport class EventStreamError extends Error {\n constructor(readonly status: number, readonly code: string | undefined) {\n super(`Event stream refused with HTTP ${status}${code ? ` (${code})` : ''}.`);\n this.name = 'EventStreamError';\n }\n}\n\ntype Outcome =\n | { next: 'reconnect'; afterMs?: number }\n | { next: 'pause' }\n | { next: 'end'; error: EventStreamError };\n\nfunction classify(status: number, failure: PlatformError | undefined): Outcome {\n const code = failure?.code;\n const runtime = getPageRuntime() !== undefined;\n if (isAuthErrorCode(code)) {\n reportAuthError(code);\n return runtime ? { next: 'pause' } : { next: 'end', error: new EventStreamError(status, code) };\n }\n if (code === 'sbx_sleeping' || code === 'sbx_starting' || code === 'sbx_start_failed') {\n if (runtime) return { next: 'pause' };\n if (code !== 'sbx_starting') return { next: 'end', error: new EventStreamError(status, code) };\n }\n if (failure?.retryable) return { next: 'reconnect', afterMs: failure.retryAfterMs };\n // Not ours to classify: an edge proxy's 5xx passes, a route's 4xx is final.\n if (!failure && status >= 500) return { next: 'reconnect' };\n return { next: 'end', error: new EventStreamError(status, code) };\n}\n\nexport function openEventStream(options: EventStreamOptions): EventStream {\n const { url, onEvent, onResume, onClose, idleTimeoutMs } = options;\n let lastEventId = options.lastEventId;\n let closed = false;\n let paused = isAppSleeping();\n let resumedFromPause = false;\n let failures = 0;\n let resumes = 0;\n let connection: AbortController | undefined;\n let wake: (() => void) | undefined;\n const page = (options.pauseWhenHidden ?? true) && typeof document !== 'undefined' ? document : undefined;\n let hidden = page?.visibilityState === 'hidden';\n const onVisibility = () => {\n hidden = page!.visibilityState === 'hidden';\n if (hidden) connection?.abort();\n wake?.();\n };\n page?.addEventListener('visibilitychange', onVisibility);\n\n const sleep = (ms?: number) => new Promise<void>((resolve) => {\n const timer = ms === undefined ? undefined : setTimeout(() => done(), ms);\n const done = () => {\n clearTimeout(timer);\n wake = undefined;\n resolve();\n };\n wake = done;\n });\n\n const offSleeping = onPageRuntime('sleeping', () => {\n paused = true;\n connection?.abort();\n wake?.();\n });\n const offResumed = onPageRuntime('resumed', () => {\n if (paused) resumedFromPause = true;\n paused = false;\n resumes += 1;\n wake?.();\n });\n\n function backoff(): number {\n failures += 1;\n const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** (failures - 1));\n return ceiling / 2 + Math.random() * (ceiling / 2);\n }\n\n async function consume(body: ReadableStream<Uint8Array>, controller: AbortController): Promise<void> {\n const openedAt = Date.now();\n let idle: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n if (idleTimeoutMs === undefined) return;\n clearTimeout(idle);\n idle = setTimeout(() => controller.abort(), idleTimeoutMs);\n };\n arm();\n try {\n for await (const event of parseServerSentEvents(body)) {\n arm();\n if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS) failures = 0;\n if ('comment' in event) continue;\n if (event.id !== undefined) lastEventId = event.id;\n if ('event' in event) onEvent(event);\n }\n } finally {\n clearTimeout(idle);\n }\n }\n\n async function connect(): Promise<Outcome> {\n const controller = new AbortController();\n connection = controller;\n const headers: Record<string, string> = { ...options.headers, Accept: 'text/event-stream' };\n if (lastEventId !== undefined) headers['Last-Event-ID'] = lastEventId;\n try {\n const response = await (options.fetch ?? globalThis.fetch)(url, { headers, cache: 'no-store', signal: controller.signal });\n const type = response.headers.get('content-type') ?? '';\n if (response.ok && /^text\\/event-stream\\b/i.test(type) && response.body) {\n if (resumedFromPause) {\n resumedFromPause = false;\n onResume?.();\n }\n await consume(response.body, controller);\n return { next: 'reconnect' };\n }\n const failure = await readPlatformError(response);\n await response.body?.cancel().catch(() => undefined);\n return classify(response.status, failure);\n } catch {\n return { next: 'reconnect' };\n } finally {\n connection = undefined;\n }\n }\n\n async function run(): Promise<void> {\n while (!closed) {\n if (hidden) {\n await sleep();\n continue;\n }\n if (paused) {\n await sleep(getPageRuntime()?.getState() === 'running' ? PAUSE_RECHECK_MS : undefined);\n if (getPageRuntime()?.getState() === 'running' && !closed) {\n resumedFromPause = true;\n paused = false;\n }\n continue;\n }\n const seenResumes = resumes;\n const outcome = await connect();\n if (closed) return;\n if (outcome.next === 'end') {\n close();\n onClose?.(outcome.error);\n return;\n }\n if (outcome.next === 'pause') {\n // A `resumed` that arrived while the refusal was on its way already happened.\n if (resumes === seenResumes) paused = true;\n else resumedFromPause = true;\n continue;\n }\n // Closed for a hidden page: wait for it to be visible, not for a backoff.\n if (hidden) continue;\n if (!paused) await sleep(outcome.afterMs ?? backoff());\n }\n }\n\n function close(): void {\n closed = true;\n offSleeping();\n offResumed();\n page?.removeEventListener('visibilitychange', onVisibility);\n connection?.abort();\n wake?.();\n }\n\n void run();\n return { close };\n}\n"]}
|
package/dist/esm/mq/client.d.ts
CHANGED
|
@@ -13,11 +13,40 @@ export declare class Tier0MQClient {
|
|
|
13
13
|
private explicitUsername;
|
|
14
14
|
private listeners;
|
|
15
15
|
private subscriptions;
|
|
16
|
+
private topicSubscriptions;
|
|
17
|
+
private pendingTopicPackets;
|
|
18
|
+
private sentTopicPackets;
|
|
16
19
|
private connectingPromise;
|
|
20
|
+
private rejectPendingConnect;
|
|
17
21
|
private _connected;
|
|
22
|
+
/** Bumped by disconnect(); callbacks of an older generation are ignored. */
|
|
23
|
+
private generation;
|
|
24
|
+
/** Random clientId segment; a persistent session is found again through it. */
|
|
25
|
+
private readonly clientSuffix;
|
|
26
|
+
private reconnectTimer;
|
|
27
|
+
private reconnectAttempts;
|
|
28
|
+
private connectWaiters;
|
|
18
29
|
constructor(config?: MQTTConfig);
|
|
19
|
-
private
|
|
30
|
+
private mqttUrlFor;
|
|
31
|
+
/**
|
|
32
|
+
* Where the credential comes from; undefined when a fixed password is used.
|
|
33
|
+
* `proxy`: platform proxy mode, the node proxy puts in the App's key and
|
|
34
|
+
* the SDK sends none.
|
|
35
|
+
*/
|
|
36
|
+
private tokenSource;
|
|
37
|
+
/** Local preflight before a transport exists: address and credentials must be usable. */
|
|
38
|
+
private validateConnectionConfig;
|
|
39
|
+
/**
|
|
40
|
+
* What the next connection presents. Through the node proxy the same
|
|
41
|
+
* identity reconnects, and the proxy puts in the App's current key.
|
|
42
|
+
*/
|
|
43
|
+
private connectionParams;
|
|
20
44
|
private ensureConnected;
|
|
45
|
+
private clearReconnect;
|
|
46
|
+
private reconnectDelay;
|
|
47
|
+
private scheduleReconnect;
|
|
48
|
+
/** The same client: mqtt.js keeps its session and offline queue. */
|
|
49
|
+
private reconnect;
|
|
21
50
|
private doConnect;
|
|
22
51
|
connect(config?: MQTTConfig): Promise<void>;
|
|
23
52
|
subscribe(topic: string, handler: TopicHandler): void;
|
|
@@ -31,6 +60,8 @@ export declare class Tier0MQClient {
|
|
|
31
60
|
off<K extends keyof MQTTEventMap>(event: K, handler: MQTTEventMap[K]): void;
|
|
32
61
|
private emit;
|
|
33
62
|
private resubscribeAll;
|
|
63
|
+
private clearTopicState;
|
|
64
|
+
private subscribeTopic;
|
|
34
65
|
private topicMatch;
|
|
35
66
|
get isConnected(): boolean;
|
|
36
67
|
get subscribedTopics(): string[];
|