@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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SUBSCRIBE_IDLE_TIMEOUT_MS = void 0;
|
|
4
|
-
exports.redactRestricted = redactRestricted;
|
|
5
4
|
exports.createSubscribe = createSubscribe;
|
|
6
5
|
/**
|
|
7
6
|
* `data.subscribe`: committed record changes of declared collections over
|
|
@@ -28,9 +27,11 @@ exports.createSubscribe = createSubscribe;
|
|
|
28
27
|
* `delete` always passes, a late one too, and forgets the record's
|
|
29
28
|
* revision, since a record written again under the same ID may start
|
|
30
29
|
* over at revision 1.
|
|
31
|
-
* - Changes in collections with employee data rules (`restricted`) come as
|
|
32
|
-
* collection-level notices unless `restricted: "record"` is asked for.
|
|
33
30
|
* - Aborting `signal` or leaving the loop ends the iteration without error.
|
|
31
|
+
* - The stream runs as the App: it sees every change of the collections
|
|
32
|
+
* granted to the App. A server opens one per process and fans it out to
|
|
33
|
+
* browsers with `createChangeRelay`, filtering per browser by what the App
|
|
34
|
+
* lets that visitor see.
|
|
34
35
|
*
|
|
35
36
|
* Events carry identities only, never record data; read the record to show it.
|
|
36
37
|
*/
|
|
@@ -48,18 +49,6 @@ const RESET_PAUSE_MS = 1_000;
|
|
|
48
49
|
const HEALTHY_CONNECTION_MS = 30_000;
|
|
49
50
|
/** Records whose last revision is remembered; beyond this the oldest is forgotten (a stale change may then pass, which only costs a reread). */
|
|
50
51
|
const SUBSCRIBE_SEEN_RECORDS_MAX = 10_000;
|
|
51
|
-
/**
|
|
52
|
-
* The event as an employee's browser may see it: a change in a collection
|
|
53
|
-
* with employee data rules becomes a collection-level notice without record
|
|
54
|
-
* ID or revision, so it does not reveal records the employee may not read.
|
|
55
|
-
* Other events pass unchanged.
|
|
56
|
-
*/
|
|
57
|
-
function redactRestricted(event) {
|
|
58
|
-
if (event.type !== 'change' || !event.restricted || event.change === 'collection')
|
|
59
|
-
return event;
|
|
60
|
-
const { eventId, collection, collectionId, occurredAt, restricted } = event;
|
|
61
|
-
return { type: 'change', eventId, collection, collectionId, occurredAt, restricted, change: 'collection' };
|
|
62
|
-
}
|
|
63
52
|
function reconnectDelay(failures, afterMs) {
|
|
64
53
|
if (afterMs !== undefined)
|
|
65
54
|
return Math.min(afterMs, RECONNECT_MAX_MS);
|
|
@@ -111,7 +100,7 @@ function pause(ms, signal) {
|
|
|
111
100
|
});
|
|
112
101
|
}
|
|
113
102
|
function createSubscribe(runtime) {
|
|
114
|
-
return async function* subscribe(options = {}) {
|
|
103
|
+
return async function* subscribe(options = {}, observer = {}) {
|
|
115
104
|
const definitions = [...new Set((options.collections ?? runtime.declared).map(runtime.lookup))];
|
|
116
105
|
if (definitions.length < 1 || definitions.length > client_js_1.UNS_V2_SUBSCRIBE_MAX_COLLECTIONS) {
|
|
117
106
|
throw new TypeError(`data.subscribe watches 1–${client_js_1.UNS_V2_SUBSCRIBE_MAX_COLLECTIONS} collections; got ${definitions.length}.`);
|
|
@@ -122,10 +111,7 @@ function createSubscribe(runtime) {
|
|
|
122
111
|
}
|
|
123
112
|
if (options.lastEventId !== undefined)
|
|
124
113
|
(0, client_js_1.assertEventId)(options.lastEventId);
|
|
125
|
-
const redact = (options.restricted ?? 'collection') === 'collection';
|
|
126
114
|
const idleMs = options.idleTimeoutMs ?? exports.SUBSCRIBE_IDLE_TIMEOUT_MS;
|
|
127
|
-
// The subject of the request that subscribed; an App-level subscription carries none.
|
|
128
|
-
const call = { dataSubject: runtime.subject() };
|
|
129
115
|
const stop = new AbortController();
|
|
130
116
|
const onAbort = () => stop.abort();
|
|
131
117
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
@@ -135,7 +121,9 @@ function createSubscribe(runtime) {
|
|
|
135
121
|
let failures = 0;
|
|
136
122
|
const seen = new SeenRevisions(SUBSCRIBE_SEEN_RECORDS_MAX);
|
|
137
123
|
/** Waits, longer each time, before the next attempt. */
|
|
138
|
-
const backOff = async (
|
|
124
|
+
const backOff = async (error) => {
|
|
125
|
+
observer.retrying?.(error);
|
|
126
|
+
const afterMs = error.retry.afterMs;
|
|
139
127
|
failures += 1;
|
|
140
128
|
await pause(reconnectDelay(failures, afterMs), stop.signal);
|
|
141
129
|
};
|
|
@@ -149,7 +137,7 @@ function createSubscribe(runtime) {
|
|
|
149
137
|
// A freshly imported App answers APP_NOT_READY for a while: keep waiting for it.
|
|
150
138
|
if (!(error instanceof error_js_1.DataError) || !error.retry.safe || stop.signal.aborted)
|
|
151
139
|
throw error;
|
|
152
|
-
await backOff(error
|
|
140
|
+
await backOff(error);
|
|
153
141
|
continue;
|
|
154
142
|
}
|
|
155
143
|
const aliases = new Map();
|
|
@@ -168,17 +156,24 @@ function createSubscribe(runtime) {
|
|
|
168
156
|
const openedAt = Date.now();
|
|
169
157
|
let healthy = false;
|
|
170
158
|
let reset = false;
|
|
159
|
+
let streaming = false;
|
|
171
160
|
try {
|
|
172
161
|
arm();
|
|
173
|
-
const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, {
|
|
162
|
+
const stream = runtime.openStream({ collectionIds: [...aliases.keys()], ...(recordIds ? { recordIds } : {}) }, { lastEventId, signal: connection.signal });
|
|
174
163
|
for await (const event of stream) {
|
|
175
164
|
arm();
|
|
176
165
|
if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
|
|
177
166
|
healthy = true;
|
|
167
|
+
if (!streaming) {
|
|
168
|
+
streaming = true;
|
|
169
|
+
observer.connected?.();
|
|
170
|
+
}
|
|
178
171
|
if (event.type === 'heartbeat') {
|
|
179
172
|
// The connect heartbeat names the current position: resuming from it loses nothing.
|
|
180
|
-
if (event.eventId !== undefined)
|
|
173
|
+
if (event.eventId !== undefined) {
|
|
181
174
|
lastEventId = event.eventId;
|
|
175
|
+
observer.position?.(event.eventId);
|
|
176
|
+
}
|
|
182
177
|
continue;
|
|
183
178
|
}
|
|
184
179
|
if (event.type === 'reset') {
|
|
@@ -192,20 +187,19 @@ function createSubscribe(runtime) {
|
|
|
192
187
|
healthy = true;
|
|
193
188
|
// A dropped change is still a resume position.
|
|
194
189
|
lastEventId = event.eventId;
|
|
190
|
+
observer.position?.(event.eventId);
|
|
195
191
|
if (!seen.admit(event.collectionId, event.recordId, event.revision, event.change))
|
|
196
192
|
continue;
|
|
197
|
-
|
|
193
|
+
yield {
|
|
198
194
|
type: 'change',
|
|
199
195
|
eventId: event.eventId,
|
|
200
196
|
collection: aliases.get(event.collectionId) ?? event.collectionId,
|
|
201
197
|
collectionId: event.collectionId,
|
|
202
198
|
occurredAt: event.occurredAt,
|
|
203
|
-
restricted: event.restricted,
|
|
204
199
|
change: event.change,
|
|
205
200
|
recordId: event.recordId,
|
|
206
201
|
revision: event.revision,
|
|
207
202
|
};
|
|
208
|
-
yield redact ? redactRestricted(change) : change;
|
|
209
203
|
}
|
|
210
204
|
}
|
|
211
205
|
catch (error) {
|
|
@@ -221,7 +215,7 @@ function createSubscribe(runtime) {
|
|
|
221
215
|
catch (cause) {
|
|
222
216
|
if (!(cause instanceof error_js_1.DataError) || !cause.retry.safe)
|
|
223
217
|
throw cause;
|
|
224
|
-
await backOff(cause
|
|
218
|
+
await backOff(cause);
|
|
225
219
|
continue;
|
|
226
220
|
}
|
|
227
221
|
if (!changed)
|
|
@@ -234,7 +228,7 @@ function createSubscribe(runtime) {
|
|
|
234
228
|
}
|
|
235
229
|
if (!error.retry.safe)
|
|
236
230
|
throw error;
|
|
237
|
-
await backOff(error
|
|
231
|
+
await backOff(error);
|
|
238
232
|
continue;
|
|
239
233
|
}
|
|
240
234
|
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,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventStreamError = void 0;
|
|
4
|
+
exports.openEventStream = openEventStream;
|
|
5
|
+
/**
|
|
6
|
+
* A server-sent event stream over `fetch`, for browsers. `EventSource` gives up
|
|
7
|
+
* for good on any HTTP error, so a 503 from a sleeping App or a restarting node
|
|
8
|
+
* agent (design 21.1) would end it. This one reads the error and acts on it:
|
|
9
|
+
*
|
|
10
|
+
* - `sbx_sleeping`, `sbx_starting`: stop, and reconnect on the runtime's
|
|
11
|
+
* `resumed`. Without the runtime `sbx_sleeping` ends the stream and
|
|
12
|
+
* `sbx_starting` waits `retry_after_ms`.
|
|
13
|
+
* - an authorization failure: reported to the runtime, then as above. A
|
|
14
|
+
* successful reauthorization also fires `resumed`.
|
|
15
|
+
* - `node_restarting`, `upstream_error` and any other retryable failure, and a
|
|
16
|
+
* broken connection: reconnect by itself after `retry_after_ms` or a backoff,
|
|
17
|
+
* resuming from `Last-Event-ID`.
|
|
18
|
+
* - a non-retryable failure: ends the stream.
|
|
19
|
+
*
|
|
20
|
+
* While the runtime says `sleeping` nothing is sent: those requests would not
|
|
21
|
+
* wake the App anyway.
|
|
22
|
+
*
|
|
23
|
+
* While the page is hidden (`document.visibilityState`) the connection is
|
|
24
|
+
* closed, so a background tab does not hold a long connection to the App;
|
|
25
|
+
* when the page is visible again it reconnects at once from the last event
|
|
26
|
+
* id (`Last-Event-ID`). The server replays what was missed or answers with
|
|
27
|
+
* its own reset; `onResume` is not called for it. `pauseWhenHidden: false`
|
|
28
|
+
* keeps the connection open in the background.
|
|
29
|
+
*/
|
|
30
|
+
const page_runtime_js_1 = require("./page-runtime.js");
|
|
31
|
+
const sse_js_1 = require("./uns-v2/sse.js");
|
|
32
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
33
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
34
|
+
/** A connection alive this long resets the backoff. */
|
|
35
|
+
const HEALTHY_CONNECTION_MS = 30_000;
|
|
36
|
+
/**
|
|
37
|
+
* A paused stream looks again this often while the runtime still says
|
|
38
|
+
* `running`: a routing race can answer `sbx_sleeping` for a running App, and
|
|
39
|
+
* then no `resumed` ever comes. Such a request cannot wake anything.
|
|
40
|
+
*/
|
|
41
|
+
const PAUSE_RECHECK_MS = 30_000;
|
|
42
|
+
class EventStreamError extends Error {
|
|
43
|
+
status;
|
|
44
|
+
code;
|
|
45
|
+
constructor(status, code) {
|
|
46
|
+
super(`Event stream refused with HTTP ${status}${code ? ` (${code})` : ''}.`);
|
|
47
|
+
this.status = status;
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.name = 'EventStreamError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.EventStreamError = EventStreamError;
|
|
53
|
+
function classify(status, failure) {
|
|
54
|
+
const code = failure?.code;
|
|
55
|
+
const runtime = (0, page_runtime_js_1.getPageRuntime)() !== undefined;
|
|
56
|
+
if ((0, page_runtime_js_1.isAuthErrorCode)(code)) {
|
|
57
|
+
(0, page_runtime_js_1.reportAuthError)(code);
|
|
58
|
+
return runtime ? { next: 'pause' } : { next: 'end', error: new EventStreamError(status, code) };
|
|
59
|
+
}
|
|
60
|
+
if (code === 'sbx_sleeping' || code === 'sbx_starting' || code === 'sbx_start_failed') {
|
|
61
|
+
if (runtime)
|
|
62
|
+
return { next: 'pause' };
|
|
63
|
+
if (code !== 'sbx_starting')
|
|
64
|
+
return { next: 'end', error: new EventStreamError(status, code) };
|
|
65
|
+
}
|
|
66
|
+
if (failure?.retryable)
|
|
67
|
+
return { next: 'reconnect', afterMs: failure.retryAfterMs };
|
|
68
|
+
// Not ours to classify: an edge proxy's 5xx passes, a route's 4xx is final.
|
|
69
|
+
if (!failure && status >= 500)
|
|
70
|
+
return { next: 'reconnect' };
|
|
71
|
+
return { next: 'end', error: new EventStreamError(status, code) };
|
|
72
|
+
}
|
|
73
|
+
function openEventStream(options) {
|
|
74
|
+
const { url, onEvent, onResume, onClose, idleTimeoutMs } = options;
|
|
75
|
+
let lastEventId = options.lastEventId;
|
|
76
|
+
let closed = false;
|
|
77
|
+
let paused = (0, page_runtime_js_1.isAppSleeping)();
|
|
78
|
+
let resumedFromPause = false;
|
|
79
|
+
let failures = 0;
|
|
80
|
+
let resumes = 0;
|
|
81
|
+
let connection;
|
|
82
|
+
let wake;
|
|
83
|
+
const page = (options.pauseWhenHidden ?? true) && typeof document !== 'undefined' ? document : undefined;
|
|
84
|
+
let hidden = page?.visibilityState === 'hidden';
|
|
85
|
+
const onVisibility = () => {
|
|
86
|
+
hidden = page.visibilityState === 'hidden';
|
|
87
|
+
if (hidden)
|
|
88
|
+
connection?.abort();
|
|
89
|
+
wake?.();
|
|
90
|
+
};
|
|
91
|
+
page?.addEventListener('visibilitychange', onVisibility);
|
|
92
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
93
|
+
const timer = ms === undefined ? undefined : setTimeout(() => done(), ms);
|
|
94
|
+
const done = () => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
wake = undefined;
|
|
97
|
+
resolve();
|
|
98
|
+
};
|
|
99
|
+
wake = done;
|
|
100
|
+
});
|
|
101
|
+
const offSleeping = (0, page_runtime_js_1.onPageRuntime)('sleeping', () => {
|
|
102
|
+
paused = true;
|
|
103
|
+
connection?.abort();
|
|
104
|
+
wake?.();
|
|
105
|
+
});
|
|
106
|
+
const offResumed = (0, page_runtime_js_1.onPageRuntime)('resumed', () => {
|
|
107
|
+
if (paused)
|
|
108
|
+
resumedFromPause = true;
|
|
109
|
+
paused = false;
|
|
110
|
+
resumes += 1;
|
|
111
|
+
wake?.();
|
|
112
|
+
});
|
|
113
|
+
function backoff() {
|
|
114
|
+
failures += 1;
|
|
115
|
+
const ceiling = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** (failures - 1));
|
|
116
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
117
|
+
}
|
|
118
|
+
async function consume(body, controller) {
|
|
119
|
+
const openedAt = Date.now();
|
|
120
|
+
let idle;
|
|
121
|
+
const arm = () => {
|
|
122
|
+
if (idleTimeoutMs === undefined)
|
|
123
|
+
return;
|
|
124
|
+
clearTimeout(idle);
|
|
125
|
+
idle = setTimeout(() => controller.abort(), idleTimeoutMs);
|
|
126
|
+
};
|
|
127
|
+
arm();
|
|
128
|
+
try {
|
|
129
|
+
for await (const event of (0, sse_js_1.parseServerSentEvents)(body)) {
|
|
130
|
+
arm();
|
|
131
|
+
if (Date.now() - openedAt >= HEALTHY_CONNECTION_MS)
|
|
132
|
+
failures = 0;
|
|
133
|
+
if ('comment' in event)
|
|
134
|
+
continue;
|
|
135
|
+
if (event.id !== undefined)
|
|
136
|
+
lastEventId = event.id;
|
|
137
|
+
if ('event' in event)
|
|
138
|
+
onEvent(event);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
clearTimeout(idle);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function connect() {
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
connection = controller;
|
|
148
|
+
const headers = { ...options.headers, Accept: 'text/event-stream' };
|
|
149
|
+
if (lastEventId !== undefined)
|
|
150
|
+
headers['Last-Event-ID'] = lastEventId;
|
|
151
|
+
try {
|
|
152
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, { headers, cache: 'no-store', signal: controller.signal });
|
|
153
|
+
const type = response.headers.get('content-type') ?? '';
|
|
154
|
+
if (response.ok && /^text\/event-stream\b/i.test(type) && response.body) {
|
|
155
|
+
if (resumedFromPause) {
|
|
156
|
+
resumedFromPause = false;
|
|
157
|
+
onResume?.();
|
|
158
|
+
}
|
|
159
|
+
await consume(response.body, controller);
|
|
160
|
+
return { next: 'reconnect' };
|
|
161
|
+
}
|
|
162
|
+
const failure = await (0, page_runtime_js_1.readPlatformError)(response);
|
|
163
|
+
await response.body?.cancel().catch(() => undefined);
|
|
164
|
+
return classify(response.status, failure);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { next: 'reconnect' };
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
connection = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function run() {
|
|
174
|
+
while (!closed) {
|
|
175
|
+
if (hidden) {
|
|
176
|
+
await sleep();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (paused) {
|
|
180
|
+
await sleep((0, page_runtime_js_1.getPageRuntime)()?.getState() === 'running' ? PAUSE_RECHECK_MS : undefined);
|
|
181
|
+
if ((0, page_runtime_js_1.getPageRuntime)()?.getState() === 'running' && !closed) {
|
|
182
|
+
resumedFromPause = true;
|
|
183
|
+
paused = false;
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const seenResumes = resumes;
|
|
188
|
+
const outcome = await connect();
|
|
189
|
+
if (closed)
|
|
190
|
+
return;
|
|
191
|
+
if (outcome.next === 'end') {
|
|
192
|
+
close();
|
|
193
|
+
onClose?.(outcome.error);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (outcome.next === 'pause') {
|
|
197
|
+
// A `resumed` that arrived while the refusal was on its way already happened.
|
|
198
|
+
if (resumes === seenResumes)
|
|
199
|
+
paused = true;
|
|
200
|
+
else
|
|
201
|
+
resumedFromPause = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Closed for a hidden page: wait for it to be visible, not for a backoff.
|
|
205
|
+
if (hidden)
|
|
206
|
+
continue;
|
|
207
|
+
if (!paused)
|
|
208
|
+
await sleep(outcome.afterMs ?? backoff());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function close() {
|
|
212
|
+
closed = true;
|
|
213
|
+
offSleeping();
|
|
214
|
+
offResumed();
|
|
215
|
+
page?.removeEventListener('visibilitychange', onVisibility);
|
|
216
|
+
connection?.abort();
|
|
217
|
+
wake?.();
|
|
218
|
+
}
|
|
219
|
+
void run();
|
|
220
|
+
return { close };
|
|
221
|
+
}
|
|
222
|
+
//# 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/cjs/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[];
|