remult 3.4.0-next.1 → 3.4.0-next.4
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/esm/internals.js +1 -0
- package/esm/src/live-query/LiveQueryClient.js +53 -25
- package/esm/src/live-query/SseSubscriptionClient.js +49 -20
- package/esm/src/live-query/SubscriptionChannel.js +34 -10
- package/esm/src/remult3/remult3.js +2 -2
- package/internals.d.ts +1 -0
- package/internals.js +3 -1
- package/package.json +1 -1
- package/src/live-query/LiveQueryClient.js +87 -37
- package/src/live-query/SseSubscriptionClient.js +75 -44
- package/src/live-query/SubscriptionChannel.d.ts +12 -0
- package/src/live-query/SubscriptionChannel.js +36 -11
- package/src/remult3/remult3.d.ts +1 -1
- package/src/remult3/remult3.js +2 -2
package/esm/internals.js
CHANGED
|
@@ -7,5 +7,6 @@ export { __updateEntityBasedOnWhere } from './src/filter/filter-interfaces.js';
|
|
|
7
7
|
export const actionInfo = remultStatic.actionInfo;
|
|
8
8
|
export { isOfType } from './src/isOfType.js';
|
|
9
9
|
export { flags } from './src/remult3/remult3.js';
|
|
10
|
+
export { SseSubscriptionClient } from './src/live-query/SseSubscriptionClient.js';
|
|
10
11
|
export { DataProviderPromiseWrapper } from './src/data-interfaces.js';
|
|
11
12
|
export { pagedQueryResult } from './src/remult3/pagedQueryResult.js';
|
|
@@ -46,7 +46,11 @@ export class LiveQueryClient {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
catch (err) {
|
|
49
|
-
|
|
49
|
+
// drop the half-registered channel so the next subscribe retries
|
|
50
|
+
// instead of joining a channel that was never subscribed
|
|
51
|
+
this.channels.delete(key);
|
|
52
|
+
q.listeners.forEach((l) => l.error(err));
|
|
53
|
+
this.closeIfNoListeners();
|
|
50
54
|
throw err;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -98,6 +102,8 @@ export class LiveQueryClient {
|
|
|
98
102
|
if (!q) {
|
|
99
103
|
this.queries.set(eventTypeKey, (q = new LiveQuerySubscriber(repo, { entityKey: repo.metadata.key, options }, this.getUserId())));
|
|
100
104
|
q.subscribeCode = () => {
|
|
105
|
+
q.snapshotReady = false;
|
|
106
|
+
q.droppedWhilePending = false;
|
|
101
107
|
q.unsubscribeChannel();
|
|
102
108
|
let unsubscribeToChannel = () => { };
|
|
103
109
|
q.unsubscribeChannel = () => {
|
|
@@ -119,7 +125,10 @@ export class LiveQueryClient {
|
|
|
119
125
|
this.runPromise(r.unsubscribe());
|
|
120
126
|
};
|
|
121
127
|
snapshotDone = true;
|
|
122
|
-
return this.runPromise(q.setAllItems(r.result))
|
|
128
|
+
return this.runPromise(q.setAllItems(r.result).then(() => {
|
|
129
|
+
if (q.droppedWhilePending)
|
|
130
|
+
return this.runKeepAlive();
|
|
131
|
+
}));
|
|
123
132
|
};
|
|
124
133
|
this.runPromise(this.subscribeChannel(q.queryChannel, {
|
|
125
134
|
next: (value) => this.runPromise(q.handle(value)),
|
|
@@ -135,7 +144,7 @@ export class LiveQueryClient {
|
|
|
135
144
|
}
|
|
136
145
|
unsubscribeToChannel = unsub;
|
|
137
146
|
if (snapshotDone)
|
|
138
|
-
return this.runPromise(this.runKeepAlive(
|
|
147
|
+
return this.runPromise(this.runKeepAlive());
|
|
139
148
|
})
|
|
140
149
|
.catch((err) => {
|
|
141
150
|
q.listeners.forEach((l) => l.error(err));
|
|
@@ -185,21 +194,24 @@ export class LiveQueryClient {
|
|
|
185
194
|
return false;
|
|
186
195
|
return Date.now() - conn.lastServerEvent > flags.sseStaleMs;
|
|
187
196
|
}
|
|
188
|
-
async runKeepAlive(
|
|
197
|
+
async runKeepAlive() {
|
|
189
198
|
if (this.keepAliveInFlight)
|
|
190
199
|
return this.keepAliveInFlight;
|
|
191
|
-
this.keepAliveInFlight = this.sendKeepAlive(
|
|
200
|
+
this.keepAliveInFlight = this.sendKeepAlive().finally(() => {
|
|
192
201
|
this.keepAliveInFlight = undefined;
|
|
193
202
|
});
|
|
194
203
|
return this.keepAliveInFlight;
|
|
195
204
|
}
|
|
196
|
-
|
|
205
|
+
/** Resolves true when a query was refetched. */
|
|
206
|
+
async sendKeepAlive() {
|
|
197
207
|
const ids = [];
|
|
198
208
|
for (const q of this.queries.values()) {
|
|
209
|
+
if (!q.snapshotReady)
|
|
210
|
+
continue;
|
|
199
211
|
ids.push(q.queryChannel);
|
|
200
212
|
}
|
|
201
213
|
if (ids.length === 0)
|
|
202
|
-
return;
|
|
214
|
+
return false;
|
|
203
215
|
let p = this.apiProvider();
|
|
204
216
|
const raw = await this.runPromise(remultStatic.actionInfo.runActionWithoutBlockingUI(() => buildRestDataProvider(p.httpClient).post(p.url + '/' + liveQueryKeepAliveRoute, { queryIds: ids })));
|
|
205
217
|
const unknownIds = Array.isArray(raw)
|
|
@@ -208,12 +220,13 @@ export class LiveQueryClient {
|
|
|
208
220
|
const versions = Array.isArray(raw)
|
|
209
221
|
? {}
|
|
210
222
|
: (raw?.versions ?? {});
|
|
211
|
-
const stale = opts?.checkVersions || this.isSseStale();
|
|
212
223
|
let reloaded = false;
|
|
213
224
|
for (const q of this.queries.values()) {
|
|
214
225
|
const serverVersion = versions[q.queryChannel];
|
|
215
226
|
const unknown = unknownIds.includes(q.queryChannel);
|
|
216
|
-
|
|
227
|
+
// server returns versions on every keep-alive, so a missed message is
|
|
228
|
+
// caught even while SSE pings look healthy
|
|
229
|
+
const mismatch = serverVersion !== undefined && serverVersion !== q.version;
|
|
217
230
|
if (unknown || mismatch) {
|
|
218
231
|
reloaded = true;
|
|
219
232
|
if (serverVersion !== undefined)
|
|
@@ -221,14 +234,7 @@ export class LiveQueryClient {
|
|
|
221
234
|
q.subscribeCode();
|
|
222
235
|
}
|
|
223
236
|
}
|
|
224
|
-
|
|
225
|
-
this.keepAliveBackoffMs = reloaded
|
|
226
|
-
? flags.liveQueryPollWhenStaleMs
|
|
227
|
-
: Math.min(this.keepAliveBackoffMs * 2, flags.liveQueryKeepAliveMs);
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
231
|
-
}
|
|
237
|
+
return reloaded;
|
|
232
238
|
}
|
|
233
239
|
async maybeKeepAlive() {
|
|
234
240
|
const stale = this.isSseStale();
|
|
@@ -242,7 +248,18 @@ export class LiveQueryClient {
|
|
|
242
248
|
this.lastKeepAliveAt = Date.now();
|
|
243
249
|
if (stale)
|
|
244
250
|
this.openedConnection?.resume?.(true);
|
|
245
|
-
|
|
251
|
+
let reloaded = false;
|
|
252
|
+
try {
|
|
253
|
+
reloaded = await this.runKeepAlive();
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
// runs on a failed post too, so an unreachable server is not polled
|
|
257
|
+
// every second; channel-only clients (no query ids) back off as well
|
|
258
|
+
if (this.isSseStale())
|
|
259
|
+
this.keepAliveBackoffMs = reloaded
|
|
260
|
+
? flags.liveQueryPollWhenStaleMs
|
|
261
|
+
: Math.min(this.keepAliveBackoffMs * 2, flags.liveQueryKeepAliveMs);
|
|
262
|
+
}
|
|
246
263
|
}
|
|
247
264
|
detachForeground = () => { };
|
|
248
265
|
attachForeground() {
|
|
@@ -255,7 +272,7 @@ export class LiveQueryClient {
|
|
|
255
272
|
this.foregroundKeepAliveTimer = undefined;
|
|
256
273
|
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
257
274
|
this.lastKeepAliveAt = 0;
|
|
258
|
-
this.runPromise(this.runKeepAlive(
|
|
275
|
+
this.runPromise(this.runKeepAlive());
|
|
259
276
|
}, 300);
|
|
260
277
|
});
|
|
261
278
|
this.detachForeground = () => {
|
|
@@ -267,19 +284,30 @@ export class LiveQueryClient {
|
|
|
267
284
|
this.detachForeground = () => { };
|
|
268
285
|
};
|
|
269
286
|
}
|
|
287
|
+
startKeepAlive() {
|
|
288
|
+
// openConnection may reject and be retried with queries still alive
|
|
289
|
+
if (this.interval !== undefined)
|
|
290
|
+
return;
|
|
291
|
+
this.lastKeepAliveAt = Date.now();
|
|
292
|
+
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
293
|
+
this.attachForeground();
|
|
294
|
+
this.interval = setInterval(() => {
|
|
295
|
+
// a failed post is transient: the backoff covers it, next tick retries
|
|
296
|
+
this.runPromise(this.maybeKeepAlive().catch(() => { }));
|
|
297
|
+
}, Math.min(flags.liveQueryPollWhenStaleMs, flags.liveQueryKeepAliveMs));
|
|
298
|
+
}
|
|
270
299
|
openIfNoOpened() {
|
|
271
300
|
if (!this.client) {
|
|
272
|
-
this.
|
|
273
|
-
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
274
|
-
this.attachForeground();
|
|
275
|
-
this.interval = setInterval(() => {
|
|
276
|
-
this.runPromise(this.maybeKeepAlive());
|
|
277
|
-
}, Math.min(flags.liveQueryPollWhenStaleMs, flags.liveQueryKeepAliveMs));
|
|
301
|
+
this.startKeepAlive();
|
|
278
302
|
return this.runPromise((this.client = this.apiProvider()
|
|
279
303
|
.subscriptionClient.openConnection(() => {
|
|
280
304
|
for (const q of this.queries.values()) {
|
|
281
305
|
q.subscribeCode();
|
|
282
306
|
}
|
|
307
|
+
for (const c of this.channels.values()) {
|
|
308
|
+
for (const l of c.listeners)
|
|
309
|
+
l.reconnect?.();
|
|
310
|
+
}
|
|
283
311
|
})
|
|
284
312
|
.then((c) => {
|
|
285
313
|
this.openedConnection = c;
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { buildRestDataProvider } from '../buildRestDataProvider.js';
|
|
2
2
|
import { remult } from '../remult-proxy.js';
|
|
3
3
|
import { remultStatic } from '../remult-static.js';
|
|
4
|
+
import { flags } from '../remult3/remult3.js';
|
|
4
5
|
import { streamUrl } from './SubscriptionChannel.js';
|
|
5
6
|
export class SseSubscriptionClient {
|
|
6
7
|
openConnection(onReconnect) {
|
|
7
8
|
let connectionId;
|
|
8
9
|
const channels = new Map();
|
|
10
|
+
const errorHandlers = new Map();
|
|
9
11
|
const provider = buildRestDataProvider(remult.apiClient.httpClient);
|
|
10
12
|
let source;
|
|
11
13
|
let retryCount = 0;
|
|
12
14
|
let closed = false;
|
|
13
15
|
let connected = false;
|
|
16
|
+
let everConnected = false;
|
|
14
17
|
let reconnectTimer;
|
|
18
|
+
let lastConnectAt = 0;
|
|
15
19
|
let connectionReady;
|
|
16
20
|
let connectionPromise = new Promise((res) => {
|
|
17
21
|
connectionReady = res;
|
|
@@ -36,15 +40,16 @@ export class SseSubscriptionClient {
|
|
|
36
40
|
function resume(force) {
|
|
37
41
|
if (closed)
|
|
38
42
|
return;
|
|
39
|
-
|
|
40
|
-
clearReconnectTimer();
|
|
43
|
+
const open = source && source.readyState !== EVENT_SOURCE_CLOSED;
|
|
41
44
|
if (force) {
|
|
45
|
+
// a source still inside its handshake window is not a zombie yet
|
|
46
|
+
if (open && Date.now() - lastConnectAt < flags.sseStaleMs)
|
|
47
|
+
return;
|
|
42
48
|
createConnection();
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
source.readyState !== EventSource.CLOSED;
|
|
51
|
+
retryCount = 0;
|
|
52
|
+
clearReconnectTimer();
|
|
48
53
|
if (!open)
|
|
49
54
|
createConnection();
|
|
50
55
|
}
|
|
@@ -56,23 +61,39 @@ export class SseSubscriptionClient {
|
|
|
56
61
|
clearReconnectTimer();
|
|
57
62
|
source?.close();
|
|
58
63
|
},
|
|
59
|
-
async subscribe(channel, handler) {
|
|
64
|
+
async subscribe(channel, handler, onError) {
|
|
60
65
|
let listeners = channels.get(channel);
|
|
66
|
+
let onErrors = errorHandlers.get(channel);
|
|
61
67
|
if (!listeners) {
|
|
62
68
|
channels.set(channel, (listeners = []));
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await
|
|
69
|
+
errorHandlers.set(channel, (onErrors = []));
|
|
70
|
+
try {
|
|
71
|
+
await connectionPromise;
|
|
72
|
+
if (!closed)
|
|
73
|
+
await subscribeToChannel(channel);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
// otherwise the next subscribe sees the entry and never retries
|
|
77
|
+
channels.delete(channel);
|
|
78
|
+
errorHandlers.delete(channel);
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
66
81
|
}
|
|
67
82
|
listeners.push(handler);
|
|
83
|
+
onErrors.push(onError);
|
|
68
84
|
return () => {
|
|
69
|
-
listeners.
|
|
85
|
+
const i = listeners.indexOf(handler);
|
|
86
|
+
if (i >= 0) {
|
|
87
|
+
listeners.splice(i, 1);
|
|
88
|
+
onErrors.splice(i, 1);
|
|
89
|
+
}
|
|
70
90
|
if (listeners.length == 0) {
|
|
71
91
|
remultStatic.actionInfo.runActionWithoutBlockingUI(() => provider.post(remult.apiClient.url + '/' + streamUrl + '/unsubscribe', {
|
|
72
92
|
channel: channel,
|
|
73
93
|
clientId: connectionId,
|
|
74
94
|
}));
|
|
75
95
|
channels.delete(channel);
|
|
96
|
+
errorHandlers.delete(channel);
|
|
76
97
|
}
|
|
77
98
|
};
|
|
78
99
|
},
|
|
@@ -86,6 +107,7 @@ export class SseSubscriptionClient {
|
|
|
86
107
|
clearReconnectTimer();
|
|
87
108
|
if (source)
|
|
88
109
|
source.close();
|
|
110
|
+
lastConnectAt = Date.now();
|
|
89
111
|
source = SseSubscriptionClient.createEventSource(remult.apiClient.url + '/' + streamUrl);
|
|
90
112
|
source.onmessage = (e) => {
|
|
91
113
|
noteServerEvent();
|
|
@@ -109,16 +131,23 @@ export class SseSubscriptionClient {
|
|
|
109
131
|
connectionId = e.data;
|
|
110
132
|
retryCount = 0;
|
|
111
133
|
noteServerEvent();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
// a connection the server forgot counts as a reconnect too: every
|
|
135
|
+
// channel must be re-subscribed and live queries refetched
|
|
136
|
+
const wasConnected = everConnected;
|
|
137
|
+
connected = everConnected = true;
|
|
138
|
+
if (wasConnected) {
|
|
139
|
+
for (const channel of [...channels.keys()]) {
|
|
140
|
+
try {
|
|
141
|
+
await subscribeToChannel(channel);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// one refused channel must not block the others
|
|
145
|
+
errorHandlers.get(channel)?.forEach((f) => f(err));
|
|
146
|
+
}
|
|
115
147
|
}
|
|
116
148
|
onReconnect();
|
|
117
149
|
}
|
|
118
|
-
|
|
119
|
-
connected = true;
|
|
120
|
-
connectionReady();
|
|
121
|
-
}
|
|
150
|
+
connectionReady();
|
|
122
151
|
});
|
|
123
152
|
}
|
|
124
153
|
createConnection();
|
|
@@ -130,15 +159,13 @@ export class SseSubscriptionClient {
|
|
|
130
159
|
clientId: connectionId,
|
|
131
160
|
});
|
|
132
161
|
});
|
|
133
|
-
if (result === ConnectionNotFoundError) {
|
|
162
|
+
if (result === ConnectionNotFoundError && connected) {
|
|
134
163
|
connected = false;
|
|
135
164
|
connectionPromise = new Promise((res) => {
|
|
136
165
|
connectionReady = res;
|
|
137
166
|
});
|
|
138
167
|
createConnection();
|
|
139
168
|
await connectionPromise;
|
|
140
|
-
if (!closed)
|
|
141
|
-
await subscribeToChannel(channel);
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
}
|
|
@@ -149,3 +176,5 @@ export class SseSubscriptionClient {
|
|
|
149
176
|
}
|
|
150
177
|
}
|
|
151
178
|
export const ConnectionNotFoundError = 'client connection not found';
|
|
179
|
+
// EventSource.CLOSED; the global is absent in Node
|
|
180
|
+
const EVENT_SOURCE_CLOSED = 2;
|
|
@@ -14,14 +14,19 @@ export class LiveQuerySubscriber {
|
|
|
14
14
|
unsubscribe = () => { };
|
|
15
15
|
unsubscribeChannel = () => { };
|
|
16
16
|
unsubscribeQuery = () => { };
|
|
17
|
+
snapshotReady = false;
|
|
18
|
+
droppedWhilePending = false;
|
|
17
19
|
async setAllItems(result) {
|
|
20
|
+
// server re-registers the query at version 0; reset before the await so a
|
|
21
|
+
// version message landing mid-parse is judged against the new baseline
|
|
22
|
+
this.version = 0;
|
|
18
23
|
const items = await getRepositoryInternals(this.repo)._fromJsonArray(result, this.query.options);
|
|
19
24
|
this.forListeners((listener) => {
|
|
20
25
|
listener(() => {
|
|
21
26
|
return items;
|
|
22
27
|
});
|
|
23
28
|
}, this.allItemsMessage(items));
|
|
24
|
-
this.
|
|
29
|
+
this.snapshotReady = true;
|
|
25
30
|
}
|
|
26
31
|
allItemsMessage(items) {
|
|
27
32
|
return [
|
|
@@ -55,16 +60,24 @@ export class LiveQuerySubscriber {
|
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
async handle(messages) {
|
|
63
|
+
// the pending snapshot supersedes anything in flight; the keep-alive
|
|
64
|
+
// after it lands catches whatever was dropped here
|
|
65
|
+
if (!this.snapshotReady) {
|
|
66
|
+
this.droppedWhilePending = true;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
58
69
|
const ver = messages.find((m) => m.type === 'version');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
70
|
+
// any gap, forward or backward, means a message was missed or two servers
|
|
71
|
+
// published the same version with different diffs: refetch instead of drop
|
|
72
|
+
if (ver && ver.from !== this.version) {
|
|
73
|
+
this.version = ver.to;
|
|
74
|
+
this.subscribeCode?.();
|
|
75
|
+
return;
|
|
67
76
|
}
|
|
77
|
+
// bump before the await below, so a second message delivered in the same
|
|
78
|
+
// tick is judged against this one and not mistaken for a gap
|
|
79
|
+
if (ver)
|
|
80
|
+
this.version = ver.to;
|
|
68
81
|
const data = messages.filter((m) => m.type !== 'version');
|
|
69
82
|
{
|
|
70
83
|
let x = data.filter(({ type }) => type == 'add' || type == 'replace');
|
|
@@ -102,7 +115,8 @@ export class LiveQuerySubscriber {
|
|
|
102
115
|
return items;
|
|
103
116
|
});
|
|
104
117
|
}, data);
|
|
105
|
-
|
|
118
|
+
if (!ver && data.length > 0)
|
|
119
|
+
this.version++;
|
|
106
120
|
}
|
|
107
121
|
defaultQueryState = [];
|
|
108
122
|
listeners = [];
|
|
@@ -200,6 +214,16 @@ export const liveQueryKeepAliveRoute = '_liveQueryKeepAlive';
|
|
|
200
214
|
* });
|
|
201
215
|
* ```
|
|
202
216
|
*
|
|
217
|
+
* #### Recovering after a dropped connection
|
|
218
|
+
* Messages published while the connection was down are not replayed.
|
|
219
|
+
* Use the `reconnect` listener to refetch state:
|
|
220
|
+
* ```ts
|
|
221
|
+
* chart.subscribe({
|
|
222
|
+
* next: (message) => (chartData = message.data),
|
|
223
|
+
* reconnect: () => loadChartData(),
|
|
224
|
+
* });
|
|
225
|
+
* ```
|
|
226
|
+
*
|
|
203
227
|
*/
|
|
204
228
|
export class SubscriptionChannel {
|
|
205
229
|
channelKey;
|
|
@@ -16,8 +16,8 @@ const wnn = [
|
|
|
16
16
|
];
|
|
17
17
|
export const flags = {
|
|
18
18
|
error500RetryCount: 4,
|
|
19
|
-
/** Client treats SSE as dead if no event for this long */
|
|
20
|
-
sseStaleMs:
|
|
19
|
+
/** Client treats SSE as dead if no event for this long. Server pings every 15s; two missed pings plus slack, so a stalled event loop does not trigger a reconnect storm */
|
|
20
|
+
sseStaleMs: 40_000,
|
|
21
21
|
/** HTTP keep-alive while SSE is healthy (touches lastUsed) */
|
|
22
22
|
liveQueryKeepAliveMs: 30_000,
|
|
23
23
|
/** Version poll while SSE is stale */
|
package/internals.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export declare const actionInfo: {
|
|
|
15
15
|
export { isOfType } from './src/isOfType.js';
|
|
16
16
|
export type { ClassType } from './classType.js';
|
|
17
17
|
export { flags } from './src/remult3/remult3.js';
|
|
18
|
+
export { SseSubscriptionClient } from './src/live-query/SseSubscriptionClient.js';
|
|
18
19
|
export { DataProviderPromiseWrapper } from './src/data-interfaces.js';
|
|
19
20
|
export { pagedQueryResult } from './src/remult3/pagedQueryResult.js';
|
package/internals.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pagedQueryResult = exports.DataProviderPromiseWrapper = exports.flags = exports.isOfType = exports.actionInfo = exports.__updateEntityBasedOnWhere = exports.getEntitySettings = exports.fieldOptionsEnricher = exports.getControllerRef = exports.controllerRefImpl = exports.decorateColumnSettings = exports.getRelationFieldInfo = exports.getRelationInfo = exports.sqlRelationsFilter = exports.sqlRelations = void 0;
|
|
3
|
+
exports.pagedQueryResult = exports.DataProviderPromiseWrapper = exports.SseSubscriptionClient = exports.flags = exports.isOfType = exports.actionInfo = exports.__updateEntityBasedOnWhere = exports.getEntitySettings = exports.fieldOptionsEnricher = exports.getControllerRef = exports.controllerRefImpl = exports.decorateColumnSettings = exports.getRelationFieldInfo = exports.getRelationInfo = exports.sqlRelationsFilter = exports.sqlRelations = void 0;
|
|
4
4
|
var remult_static_js_1 = require("./src/remult-static.js");
|
|
5
5
|
var sql_relations_js_1 = require("./src/data-providers/sql-relations.js");
|
|
6
6
|
Object.defineProperty(exports, "sqlRelations", { enumerable: true, get: function () { return sql_relations_js_1.sqlRelations; } });
|
|
@@ -22,6 +22,8 @@ var isOfType_js_1 = require("./src/isOfType.js");
|
|
|
22
22
|
Object.defineProperty(exports, "isOfType", { enumerable: true, get: function () { return isOfType_js_1.isOfType; } });
|
|
23
23
|
var remult3_js_1 = require("./src/remult3/remult3.js");
|
|
24
24
|
Object.defineProperty(exports, "flags", { enumerable: true, get: function () { return remult3_js_1.flags; } });
|
|
25
|
+
var SseSubscriptionClient_js_1 = require("./src/live-query/SseSubscriptionClient.js");
|
|
26
|
+
Object.defineProperty(exports, "SseSubscriptionClient", { enumerable: true, get: function () { return SseSubscriptionClient_js_1.SseSubscriptionClient; } });
|
|
25
27
|
var data_interfaces_js_1 = require("./src/data-interfaces.js");
|
|
26
28
|
Object.defineProperty(exports, "DataProviderPromiseWrapper", { enumerable: true, get: function () { return data_interfaces_js_1.DataProviderPromiseWrapper; } });
|
|
27
29
|
var pagedQueryResult_js_1 = require("./src/remult3/pagedQueryResult.js");
|
package/package.json
CHANGED
|
@@ -67,7 +67,11 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
67
67
|
return [3 /*break*/, 6];
|
|
68
68
|
case 5:
|
|
69
69
|
err_1 = _b.sent();
|
|
70
|
-
|
|
70
|
+
// drop the half-registered channel so the next subscribe retries
|
|
71
|
+
// instead of joining a channel that was never subscribed
|
|
72
|
+
this.channels.delete(key);
|
|
73
|
+
q_1.listeners.forEach(function (l) { return l.error(err_1); });
|
|
74
|
+
this.closeIfNoListeners();
|
|
71
75
|
throw err_1;
|
|
72
76
|
case 6:
|
|
73
77
|
q_1.listeners.push(onResult);
|
|
@@ -122,6 +126,8 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
122
126
|
if (!q) {
|
|
123
127
|
_this.queries.set(eventTypeKey, (q = new SubscriptionChannel_js_1.LiveQuerySubscriber(repo, { entityKey: repo.metadata.key, options: options }, _this.getUserId())));
|
|
124
128
|
q.subscribeCode = function () {
|
|
129
|
+
q.snapshotReady = false;
|
|
130
|
+
q.droppedWhilePending = false;
|
|
125
131
|
q.unsubscribeChannel();
|
|
126
132
|
var unsubscribeToChannel = function () { };
|
|
127
133
|
q.unsubscribeChannel = function () {
|
|
@@ -143,7 +149,10 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
143
149
|
_this.runPromise(r.unsubscribe());
|
|
144
150
|
};
|
|
145
151
|
snapshotDone = true;
|
|
146
|
-
return _this.runPromise(q.setAllItems(r.result))
|
|
152
|
+
return _this.runPromise(q.setAllItems(r.result).then(function () {
|
|
153
|
+
if (q.droppedWhilePending)
|
|
154
|
+
return _this.runKeepAlive();
|
|
155
|
+
}));
|
|
147
156
|
};
|
|
148
157
|
_this.runPromise(_this.subscribeChannel(q.queryChannel, {
|
|
149
158
|
next: function (value) { return _this.runPromise(q.handle(value)); },
|
|
@@ -159,7 +168,7 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
159
168
|
}
|
|
160
169
|
unsubscribeToChannel = unsub;
|
|
161
170
|
if (snapshotDone)
|
|
162
|
-
return _this.runPromise(_this.runKeepAlive(
|
|
171
|
+
return _this.runPromise(_this.runKeepAlive());
|
|
163
172
|
})
|
|
164
173
|
.catch(function (err) {
|
|
165
174
|
q.listeners.forEach(function (l) { return l.error(err); });
|
|
@@ -202,22 +211,23 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
202
211
|
return false;
|
|
203
212
|
return Date.now() - conn.lastServerEvent > remult3_js_1.flags.sseStaleMs;
|
|
204
213
|
};
|
|
205
|
-
LiveQueryClient.prototype.runKeepAlive = function (
|
|
214
|
+
LiveQueryClient.prototype.runKeepAlive = function () {
|
|
206
215
|
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
207
216
|
var _this = this;
|
|
208
217
|
return tslib_1.__generator(this, function (_a) {
|
|
209
218
|
if (this.keepAliveInFlight)
|
|
210
219
|
return [2 /*return*/, this.keepAliveInFlight];
|
|
211
|
-
this.keepAliveInFlight = this.sendKeepAlive(
|
|
220
|
+
this.keepAliveInFlight = this.sendKeepAlive().finally(function () {
|
|
212
221
|
_this.keepAliveInFlight = undefined;
|
|
213
222
|
});
|
|
214
223
|
return [2 /*return*/, this.keepAliveInFlight];
|
|
215
224
|
});
|
|
216
225
|
});
|
|
217
226
|
};
|
|
218
|
-
|
|
227
|
+
/** Resolves true when a query was refetched. */
|
|
228
|
+
LiveQueryClient.prototype.sendKeepAlive = function () {
|
|
219
229
|
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
220
|
-
var ids, _a, _b, q, p, raw, unknownIds, versions,
|
|
230
|
+
var ids, _a, _b, q, p, raw, unknownIds, versions, reloaded, _c, _d, q, serverVersion, unknown, mismatch;
|
|
221
231
|
var e_1, _e, e_2, _f;
|
|
222
232
|
var _g, _h;
|
|
223
233
|
return tslib_1.__generator(this, function (_j) {
|
|
@@ -227,6 +237,8 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
227
237
|
try {
|
|
228
238
|
for (_a = tslib_1.__values(this.queries.values()), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
229
239
|
q = _b.value;
|
|
240
|
+
if (!q.snapshotReady)
|
|
241
|
+
continue;
|
|
230
242
|
ids.push(q.queryChannel);
|
|
231
243
|
}
|
|
232
244
|
}
|
|
@@ -238,7 +250,7 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
238
250
|
finally { if (e_1) throw e_1.error; }
|
|
239
251
|
}
|
|
240
252
|
if (ids.length === 0)
|
|
241
|
-
return [2 /*return
|
|
253
|
+
return [2 /*return*/, false];
|
|
242
254
|
p = this.apiProvider();
|
|
243
255
|
return [4 /*yield*/, this.runPromise(remult_static_js_1.remultStatic.actionInfo.runActionWithoutBlockingUI(function () {
|
|
244
256
|
return (0, buildRestDataProvider_js_1.buildRestDataProvider)(p.httpClient).post(p.url + '/' + SubscriptionChannel_js_1.liveQueryKeepAliveRoute, { queryIds: ids });
|
|
@@ -251,14 +263,13 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
251
263
|
versions = Array.isArray(raw)
|
|
252
264
|
? {}
|
|
253
265
|
: ((_h = raw === null || raw === void 0 ? void 0 : raw.versions) !== null && _h !== void 0 ? _h : {});
|
|
254
|
-
stale = (opts === null || opts === void 0 ? void 0 : opts.checkVersions) || this.isSseStale();
|
|
255
266
|
reloaded = false;
|
|
256
267
|
try {
|
|
257
268
|
for (_c = tslib_1.__values(this.queries.values()), _d = _c.next(); !_d.done; _d = _c.next()) {
|
|
258
269
|
q = _d.value;
|
|
259
270
|
serverVersion = versions[q.queryChannel];
|
|
260
271
|
unknown = unknownIds.includes(q.queryChannel);
|
|
261
|
-
mismatch =
|
|
272
|
+
mismatch = serverVersion !== undefined && serverVersion !== q.version;
|
|
262
273
|
if (unknown || mismatch) {
|
|
263
274
|
reloaded = true;
|
|
264
275
|
if (serverVersion !== undefined)
|
|
@@ -274,22 +285,14 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
274
285
|
}
|
|
275
286
|
finally { if (e_2) throw e_2.error; }
|
|
276
287
|
}
|
|
277
|
-
|
|
278
|
-
this.keepAliveBackoffMs = reloaded
|
|
279
|
-
? remult3_js_1.flags.liveQueryPollWhenStaleMs
|
|
280
|
-
: Math.min(this.keepAliveBackoffMs * 2, remult3_js_1.flags.liveQueryKeepAliveMs);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
this.keepAliveBackoffMs = remult3_js_1.flags.liveQueryPollWhenStaleMs;
|
|
284
|
-
}
|
|
285
|
-
return [2 /*return*/];
|
|
288
|
+
return [2 /*return*/, reloaded];
|
|
286
289
|
}
|
|
287
290
|
});
|
|
288
291
|
});
|
|
289
292
|
};
|
|
290
293
|
LiveQueryClient.prototype.maybeKeepAlive = function () {
|
|
291
294
|
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
292
|
-
var stale, interval;
|
|
295
|
+
var stale, interval, reloaded;
|
|
293
296
|
var _a, _b;
|
|
294
297
|
return tslib_1.__generator(this, function (_c) {
|
|
295
298
|
switch (_c.label) {
|
|
@@ -305,10 +308,23 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
305
308
|
this.lastKeepAliveAt = Date.now();
|
|
306
309
|
if (stale)
|
|
307
310
|
(_b = (_a = this.openedConnection) === null || _a === void 0 ? void 0 : _a.resume) === null || _b === void 0 ? void 0 : _b.call(_a, true);
|
|
308
|
-
|
|
311
|
+
reloaded = false;
|
|
312
|
+
_c.label = 1;
|
|
309
313
|
case 1:
|
|
310
|
-
_c.
|
|
311
|
-
return [
|
|
314
|
+
_c.trys.push([1, , 3, 4]);
|
|
315
|
+
return [4 /*yield*/, this.runKeepAlive()];
|
|
316
|
+
case 2:
|
|
317
|
+
reloaded = _c.sent();
|
|
318
|
+
return [3 /*break*/, 4];
|
|
319
|
+
case 3:
|
|
320
|
+
// runs on a failed post too, so an unreachable server is not polled
|
|
321
|
+
// every second; channel-only clients (no query ids) back off as well
|
|
322
|
+
if (this.isSseStale())
|
|
323
|
+
this.keepAliveBackoffMs = reloaded
|
|
324
|
+
? remult3_js_1.flags.liveQueryPollWhenStaleMs
|
|
325
|
+
: Math.min(this.keepAliveBackoffMs * 2, remult3_js_1.flags.liveQueryKeepAliveMs);
|
|
326
|
+
return [7 /*endfinally*/];
|
|
327
|
+
case 4: return [2 /*return*/];
|
|
312
328
|
}
|
|
313
329
|
});
|
|
314
330
|
});
|
|
@@ -325,7 +341,7 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
325
341
|
_this.foregroundKeepAliveTimer = undefined;
|
|
326
342
|
_this.keepAliveBackoffMs = remult3_js_1.flags.liveQueryPollWhenStaleMs;
|
|
327
343
|
_this.lastKeepAliveAt = 0;
|
|
328
|
-
_this.runPromise(_this.runKeepAlive(
|
|
344
|
+
_this.runPromise(_this.runKeepAlive());
|
|
329
345
|
}, 300);
|
|
330
346
|
});
|
|
331
347
|
this.detachForeground = function () {
|
|
@@ -337,31 +353,65 @@ var LiveQueryClient = /** @class */ (function () {
|
|
|
337
353
|
_this.detachForeground = function () { };
|
|
338
354
|
};
|
|
339
355
|
};
|
|
356
|
+
LiveQueryClient.prototype.startKeepAlive = function () {
|
|
357
|
+
var _this = this;
|
|
358
|
+
// openConnection may reject and be retried with queries still alive
|
|
359
|
+
if (this.interval !== undefined)
|
|
360
|
+
return;
|
|
361
|
+
this.lastKeepAliveAt = Date.now();
|
|
362
|
+
this.keepAliveBackoffMs = remult3_js_1.flags.liveQueryPollWhenStaleMs;
|
|
363
|
+
this.attachForeground();
|
|
364
|
+
this.interval = setInterval(function () {
|
|
365
|
+
// a failed post is transient: the backoff covers it, next tick retries
|
|
366
|
+
_this.runPromise(_this.maybeKeepAlive().catch(function () { }));
|
|
367
|
+
}, Math.min(remult3_js_1.flags.liveQueryPollWhenStaleMs, remult3_js_1.flags.liveQueryKeepAliveMs));
|
|
368
|
+
};
|
|
340
369
|
LiveQueryClient.prototype.openIfNoOpened = function () {
|
|
341
370
|
var _this = this;
|
|
342
371
|
if (!this.client) {
|
|
343
|
-
this.
|
|
344
|
-
this.keepAliveBackoffMs = remult3_js_1.flags.liveQueryPollWhenStaleMs;
|
|
345
|
-
this.attachForeground();
|
|
346
|
-
this.interval = setInterval(function () {
|
|
347
|
-
_this.runPromise(_this.maybeKeepAlive());
|
|
348
|
-
}, Math.min(remult3_js_1.flags.liveQueryPollWhenStaleMs, remult3_js_1.flags.liveQueryKeepAliveMs));
|
|
372
|
+
this.startKeepAlive();
|
|
349
373
|
return this.runPromise((this.client = this.apiProvider()
|
|
350
374
|
.subscriptionClient.openConnection(function () {
|
|
351
|
-
var e_3, _a;
|
|
375
|
+
var e_3, _a, e_4, _b, e_5, _c;
|
|
376
|
+
var _d;
|
|
352
377
|
try {
|
|
353
|
-
for (var
|
|
354
|
-
var q =
|
|
378
|
+
for (var _e = tslib_1.__values(_this.queries.values()), _f = _e.next(); !_f.done; _f = _e.next()) {
|
|
379
|
+
var q = _f.value;
|
|
355
380
|
q.subscribeCode();
|
|
356
381
|
}
|
|
357
382
|
}
|
|
358
383
|
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
359
384
|
finally {
|
|
360
385
|
try {
|
|
361
|
-
if (
|
|
386
|
+
if (_f && !_f.done && (_a = _e.return)) _a.call(_e);
|
|
362
387
|
}
|
|
363
388
|
finally { if (e_3) throw e_3.error; }
|
|
364
389
|
}
|
|
390
|
+
try {
|
|
391
|
+
for (var _g = tslib_1.__values(_this.channels.values()), _h = _g.next(); !_h.done; _h = _g.next()) {
|
|
392
|
+
var c = _h.value;
|
|
393
|
+
try {
|
|
394
|
+
for (var _j = (e_5 = void 0, tslib_1.__values(c.listeners)), _k = _j.next(); !_k.done; _k = _j.next()) {
|
|
395
|
+
var l = _k.value;
|
|
396
|
+
(_d = l.reconnect) === null || _d === void 0 ? void 0 : _d.call(l);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
400
|
+
finally {
|
|
401
|
+
try {
|
|
402
|
+
if (_k && !_k.done && (_c = _j.return)) _c.call(_j);
|
|
403
|
+
}
|
|
404
|
+
finally { if (e_5) throw e_5.error; }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
409
|
+
finally {
|
|
410
|
+
try {
|
|
411
|
+
if (_h && !_h.done && (_b = _g.return)) _b.call(_g);
|
|
412
|
+
}
|
|
413
|
+
finally { if (e_4) throw e_4.error; }
|
|
414
|
+
}
|
|
365
415
|
})
|
|
366
416
|
.then(function (c) {
|
|
367
417
|
_this.openedConnection = c;
|
|
@@ -387,7 +437,7 @@ var MessageChannel = /** @class */ (function () {
|
|
|
387
437
|
MessageChannel.prototype.handle = function (message) {
|
|
388
438
|
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
389
439
|
var _a, _b, l;
|
|
390
|
-
var
|
|
440
|
+
var e_6, _c;
|
|
391
441
|
return tslib_1.__generator(this, function (_d) {
|
|
392
442
|
try {
|
|
393
443
|
for (_a = tslib_1.__values(this.listeners), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
@@ -395,12 +445,12 @@ var MessageChannel = /** @class */ (function () {
|
|
|
395
445
|
l.next(message);
|
|
396
446
|
}
|
|
397
447
|
}
|
|
398
|
-
catch (
|
|
448
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
399
449
|
finally {
|
|
400
450
|
try {
|
|
401
451
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
402
452
|
}
|
|
403
|
-
finally { if (
|
|
453
|
+
finally { if (e_6) throw e_6.error; }
|
|
404
454
|
}
|
|
405
455
|
return [2 /*return*/];
|
|
406
456
|
});
|
|
@@ -5,6 +5,7 @@ var tslib_1 = require("tslib");
|
|
|
5
5
|
var buildRestDataProvider_js_1 = require("../buildRestDataProvider.js");
|
|
6
6
|
var remult_proxy_js_1 = require("../remult-proxy.js");
|
|
7
7
|
var remult_static_js_1 = require("../remult-static.js");
|
|
8
|
+
var remult3_js_1 = require("../remult3/remult3.js");
|
|
8
9
|
var SubscriptionChannel_js_1 = require("./SubscriptionChannel.js");
|
|
9
10
|
var SseSubscriptionClient = /** @class */ (function () {
|
|
10
11
|
function SseSubscriptionClient() {
|
|
@@ -12,12 +13,15 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
12
13
|
SseSubscriptionClient.prototype.openConnection = function (onReconnect) {
|
|
13
14
|
var connectionId;
|
|
14
15
|
var channels = new Map();
|
|
16
|
+
var errorHandlers = new Map();
|
|
15
17
|
var provider = (0, buildRestDataProvider_js_1.buildRestDataProvider)(remult_proxy_js_1.remult.apiClient.httpClient);
|
|
16
18
|
var source;
|
|
17
19
|
var retryCount = 0;
|
|
18
20
|
var closed = false;
|
|
19
21
|
var connected = false;
|
|
22
|
+
var everConnected = false;
|
|
20
23
|
var reconnectTimer;
|
|
24
|
+
var lastConnectAt = 0;
|
|
21
25
|
var connectionReady;
|
|
22
26
|
var connectionPromise = new Promise(function (res) {
|
|
23
27
|
connectionReady = res;
|
|
@@ -42,15 +46,16 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
42
46
|
function resume(force) {
|
|
43
47
|
if (closed)
|
|
44
48
|
return;
|
|
45
|
-
|
|
46
|
-
clearReconnectTimer();
|
|
49
|
+
var open = source && source.readyState !== EVENT_SOURCE_CLOSED;
|
|
47
50
|
if (force) {
|
|
51
|
+
// a source still inside its handshake window is not a zombie yet
|
|
52
|
+
if (open && Date.now() - lastConnectAt < remult3_js_1.flags.sseStaleMs)
|
|
53
|
+
return;
|
|
48
54
|
createConnection();
|
|
49
55
|
return;
|
|
50
56
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
source.readyState !== EventSource.CLOSED;
|
|
57
|
+
retryCount = 0;
|
|
58
|
+
clearReconnectTimer();
|
|
54
59
|
if (!open)
|
|
55
60
|
createConnection();
|
|
56
61
|
}
|
|
@@ -62,27 +67,44 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
62
67
|
clearReconnectTimer();
|
|
63
68
|
source === null || source === void 0 ? void 0 : source.close();
|
|
64
69
|
},
|
|
65
|
-
subscribe: function (channel, handler) {
|
|
70
|
+
subscribe: function (channel, handler, onError) {
|
|
66
71
|
return tslib_1.__awaiter(this, void 0, void 0, function () {
|
|
67
|
-
var listeners;
|
|
72
|
+
var listeners, onErrors, err_1;
|
|
68
73
|
return tslib_1.__generator(this, function (_a) {
|
|
69
74
|
switch (_a.label) {
|
|
70
75
|
case 0:
|
|
71
76
|
listeners = channels.get(channel);
|
|
72
|
-
|
|
77
|
+
onErrors = errorHandlers.get(channel);
|
|
78
|
+
if (!!listeners) return [3 /*break*/, 6];
|
|
73
79
|
channels.set(channel, (listeners = []));
|
|
74
|
-
|
|
80
|
+
errorHandlers.set(channel, (onErrors = []));
|
|
81
|
+
_a.label = 1;
|
|
75
82
|
case 1:
|
|
76
|
-
_a.
|
|
77
|
-
|
|
78
|
-
return [4 /*yield*/, subscribeToChannel(channel)];
|
|
83
|
+
_a.trys.push([1, 5, , 6]);
|
|
84
|
+
return [4 /*yield*/, connectionPromise];
|
|
79
85
|
case 2:
|
|
80
86
|
_a.sent();
|
|
81
|
-
|
|
87
|
+
if (!!closed) return [3 /*break*/, 4];
|
|
88
|
+
return [4 /*yield*/, subscribeToChannel(channel)];
|
|
82
89
|
case 3:
|
|
90
|
+
_a.sent();
|
|
91
|
+
_a.label = 4;
|
|
92
|
+
case 4: return [3 /*break*/, 6];
|
|
93
|
+
case 5:
|
|
94
|
+
err_1 = _a.sent();
|
|
95
|
+
// otherwise the next subscribe sees the entry and never retries
|
|
96
|
+
channels.delete(channel);
|
|
97
|
+
errorHandlers.delete(channel);
|
|
98
|
+
throw err_1;
|
|
99
|
+
case 6:
|
|
83
100
|
listeners.push(handler);
|
|
101
|
+
onErrors.push(onError);
|
|
84
102
|
return [2 /*return*/, function () {
|
|
85
|
-
listeners.
|
|
103
|
+
var i = listeners.indexOf(handler);
|
|
104
|
+
if (i >= 0) {
|
|
105
|
+
listeners.splice(i, 1);
|
|
106
|
+
onErrors.splice(i, 1);
|
|
107
|
+
}
|
|
86
108
|
if (listeners.length == 0) {
|
|
87
109
|
remult_static_js_1.remultStatic.actionInfo.runActionWithoutBlockingUI(function () {
|
|
88
110
|
return provider.post(remult_proxy_js_1.remult.apiClient.url + '/' + SubscriptionChannel_js_1.streamUrl + '/unsubscribe', {
|
|
@@ -91,6 +113,7 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
91
113
|
});
|
|
92
114
|
});
|
|
93
115
|
channels.delete(channel);
|
|
116
|
+
errorHandlers.delete(channel);
|
|
94
117
|
}
|
|
95
118
|
}];
|
|
96
119
|
}
|
|
@@ -108,6 +131,7 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
108
131
|
clearReconnectTimer();
|
|
109
132
|
if (source)
|
|
110
133
|
source.close();
|
|
134
|
+
lastConnectAt = Date.now();
|
|
111
135
|
source = SseSubscriptionClient.createEventSource(remult_proxy_js_1.remult.apiClient.url + '/' + SubscriptionChannel_js_1.streamUrl);
|
|
112
136
|
source.onmessage = function (e) {
|
|
113
137
|
noteServerEvent();
|
|
@@ -127,50 +151,59 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
127
151
|
scheduleReconnect();
|
|
128
152
|
};
|
|
129
153
|
source.addEventListener('connectionId', function (e) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
|
|
130
|
-
var _a, _b, channel, e_1_1;
|
|
154
|
+
var wasConnected, _a, _b, channel, err_2, e_1_1;
|
|
131
155
|
var e_1, _c;
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
var _d;
|
|
157
|
+
return tslib_1.__generator(this, function (_e) {
|
|
158
|
+
switch (_e.label) {
|
|
134
159
|
case 0:
|
|
135
160
|
//@ts-ignore
|
|
136
161
|
connectionId = e.data;
|
|
137
162
|
retryCount = 0;
|
|
138
163
|
noteServerEvent();
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
wasConnected = everConnected;
|
|
165
|
+
connected = everConnected = true;
|
|
166
|
+
if (!wasConnected) return [3 /*break*/, 11];
|
|
167
|
+
_e.label = 1;
|
|
141
168
|
case 1:
|
|
142
|
-
|
|
143
|
-
_a = tslib_1.__values(channels.keys()), _b = _a.next();
|
|
144
|
-
|
|
169
|
+
_e.trys.push([1, 8, 9, 10]);
|
|
170
|
+
_a = tslib_1.__values(tslib_1.__spreadArray([], tslib_1.__read(channels.keys()), false)), _b = _a.next();
|
|
171
|
+
_e.label = 2;
|
|
145
172
|
case 2:
|
|
146
|
-
if (!!_b.done) return [3 /*break*/,
|
|
173
|
+
if (!!_b.done) return [3 /*break*/, 7];
|
|
147
174
|
channel = _b.value;
|
|
148
|
-
|
|
175
|
+
_e.label = 3;
|
|
149
176
|
case 3:
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
_e.trys.push([3, 5, , 6]);
|
|
178
|
+
return [4 /*yield*/, subscribeToChannel(channel)];
|
|
152
179
|
case 4:
|
|
180
|
+
_e.sent();
|
|
181
|
+
return [3 /*break*/, 6];
|
|
182
|
+
case 5:
|
|
183
|
+
err_2 = _e.sent();
|
|
184
|
+
// one refused channel must not block the others
|
|
185
|
+
(_d = errorHandlers.get(channel)) === null || _d === void 0 ? void 0 : _d.forEach(function (f) { return f(err_2); });
|
|
186
|
+
return [3 /*break*/, 6];
|
|
187
|
+
case 6:
|
|
153
188
|
_b = _a.next();
|
|
154
189
|
return [3 /*break*/, 2];
|
|
155
|
-
case
|
|
156
|
-
case
|
|
157
|
-
e_1_1 =
|
|
190
|
+
case 7: return [3 /*break*/, 10];
|
|
191
|
+
case 8:
|
|
192
|
+
e_1_1 = _e.sent();
|
|
158
193
|
e_1 = { error: e_1_1 };
|
|
159
|
-
return [3 /*break*/,
|
|
160
|
-
case
|
|
194
|
+
return [3 /*break*/, 10];
|
|
195
|
+
case 9:
|
|
161
196
|
try {
|
|
162
197
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
163
198
|
}
|
|
164
199
|
finally { if (e_1) throw e_1.error; }
|
|
165
200
|
return [7 /*endfinally*/];
|
|
166
|
-
case
|
|
201
|
+
case 10:
|
|
167
202
|
onReconnect();
|
|
168
|
-
|
|
169
|
-
case
|
|
170
|
-
connected = true;
|
|
203
|
+
_e.label = 11;
|
|
204
|
+
case 11:
|
|
171
205
|
connectionReady();
|
|
172
|
-
|
|
173
|
-
case 10: return [2 /*return*/];
|
|
206
|
+
return [2 /*return*/];
|
|
174
207
|
}
|
|
175
208
|
});
|
|
176
209
|
}); });
|
|
@@ -190,7 +223,7 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
190
223
|
})];
|
|
191
224
|
case 1:
|
|
192
225
|
result = _a.sent();
|
|
193
|
-
if (!(result === exports.ConnectionNotFoundError)) return [3 /*break*/,
|
|
226
|
+
if (!(result === exports.ConnectionNotFoundError && connected)) return [3 /*break*/, 3];
|
|
194
227
|
connected = false;
|
|
195
228
|
connectionPromise = new Promise(function (res) {
|
|
196
229
|
connectionReady = res;
|
|
@@ -199,12 +232,8 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
199
232
|
return [4 /*yield*/, connectionPromise];
|
|
200
233
|
case 2:
|
|
201
234
|
_a.sent();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
case 3:
|
|
205
|
-
_a.sent();
|
|
206
|
-
_a.label = 4;
|
|
207
|
-
case 4: return [2 /*return*/];
|
|
235
|
+
_a.label = 3;
|
|
236
|
+
case 3: return [2 /*return*/];
|
|
208
237
|
}
|
|
209
238
|
});
|
|
210
239
|
});
|
|
@@ -219,3 +248,5 @@ var SseSubscriptionClient = /** @class */ (function () {
|
|
|
219
248
|
}());
|
|
220
249
|
exports.SseSubscriptionClient = SseSubscriptionClient;
|
|
221
250
|
exports.ConnectionNotFoundError = 'client connection not found';
|
|
251
|
+
// EventSource.CLOSED; the global is absent in Node
|
|
252
|
+
var EVENT_SOURCE_CLOSED = 2;
|
|
@@ -4,6 +4,8 @@ export interface SubscriptionListener<type> {
|
|
|
4
4
|
next(message: type): void;
|
|
5
5
|
error(err: any): void;
|
|
6
6
|
complete(): void;
|
|
7
|
+
/** The connection was re-established; messages published meanwhile were lost, refetch state if needed. */
|
|
8
|
+
reconnect?(): void;
|
|
7
9
|
}
|
|
8
10
|
export type Unsubscribe = VoidFunction;
|
|
9
11
|
export interface SubscriptionClientConnection {
|
|
@@ -106,6 +108,16 @@ export declare type LiveQueryChange = {
|
|
|
106
108
|
* });
|
|
107
109
|
* ```
|
|
108
110
|
*
|
|
111
|
+
* #### Recovering after a dropped connection
|
|
112
|
+
* Messages published while the connection was down are not replayed.
|
|
113
|
+
* Use the `reconnect` listener to refetch state:
|
|
114
|
+
* ```ts
|
|
115
|
+
* chart.subscribe({
|
|
116
|
+
* next: (message) => (chartData = message.data),
|
|
117
|
+
* reconnect: () => loadChartData(),
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
109
121
|
*/
|
|
110
122
|
export declare class SubscriptionChannel<messageType> {
|
|
111
123
|
channelKey: string;
|
|
@@ -15,6 +15,8 @@ var LiveQuerySubscriber = /** @class */ (function () {
|
|
|
15
15
|
this.unsubscribe = function () { };
|
|
16
16
|
this.unsubscribeChannel = function () { };
|
|
17
17
|
this.unsubscribeQuery = function () { };
|
|
18
|
+
this.snapshotReady = false;
|
|
19
|
+
this.droppedWhilePending = false;
|
|
18
20
|
this.defaultQueryState = [];
|
|
19
21
|
this.listeners = [];
|
|
20
22
|
this.id = String(crypto.randomUUID());
|
|
@@ -31,7 +33,11 @@ var LiveQuerySubscriber = /** @class */ (function () {
|
|
|
31
33
|
var items;
|
|
32
34
|
return tslib_1.__generator(this, function (_a) {
|
|
33
35
|
switch (_a.label) {
|
|
34
|
-
case 0:
|
|
36
|
+
case 0:
|
|
37
|
+
// server re-registers the query at version 0; reset before the await so a
|
|
38
|
+
// version message landing mid-parse is judged against the new baseline
|
|
39
|
+
this.version = 0;
|
|
40
|
+
return [4 /*yield*/, (0, repository_internals_js_1.getRepositoryInternals)(this.repo)._fromJsonArray(result, this.query.options)];
|
|
35
41
|
case 1:
|
|
36
42
|
items = _a.sent();
|
|
37
43
|
this.forListeners(function (listener) {
|
|
@@ -39,7 +45,7 @@ var LiveQuerySubscriber = /** @class */ (function () {
|
|
|
39
45
|
return items;
|
|
40
46
|
});
|
|
41
47
|
}, this.allItemsMessage(items));
|
|
42
|
-
this.
|
|
48
|
+
this.snapshotReady = true;
|
|
43
49
|
return [2 /*return*/];
|
|
44
50
|
}
|
|
45
51
|
});
|
|
@@ -99,18 +105,26 @@ var LiveQuerySubscriber = /** @class */ (function () {
|
|
|
99
105
|
return tslib_1.__generator(this, function (_b) {
|
|
100
106
|
switch (_b.label) {
|
|
101
107
|
case 0:
|
|
108
|
+
// the pending snapshot supersedes anything in flight; the keep-alive
|
|
109
|
+
// after it lands catches whatever was dropped here
|
|
110
|
+
if (!this.snapshotReady) {
|
|
111
|
+
this.droppedWhilePending = true;
|
|
112
|
+
return [2 /*return*/];
|
|
113
|
+
}
|
|
102
114
|
ver = messages.find(function (m) {
|
|
103
115
|
return m.type === 'version';
|
|
104
116
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return [2 /*return*/];
|
|
112
|
-
}
|
|
117
|
+
// any gap, forward or backward, means a message was missed or two servers
|
|
118
|
+
// published the same version with different diffs: refetch instead of drop
|
|
119
|
+
if (ver && ver.from !== this.version) {
|
|
120
|
+
this.version = ver.to;
|
|
121
|
+
(_a = this.subscribeCode) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
122
|
+
return [2 /*return*/];
|
|
113
123
|
}
|
|
124
|
+
// bump before the await below, so a second message delivered in the same
|
|
125
|
+
// tick is judged against this one and not mistaken for a gap
|
|
126
|
+
if (ver)
|
|
127
|
+
this.version = ver.to;
|
|
114
128
|
data = messages.filter(function (m) { return m.type !== 'version'; });
|
|
115
129
|
x = data.filter(function (_a) {
|
|
116
130
|
var type = _a.type;
|
|
@@ -171,7 +185,8 @@ var LiveQuerySubscriber = /** @class */ (function () {
|
|
|
171
185
|
return items;
|
|
172
186
|
});
|
|
173
187
|
}, data);
|
|
174
|
-
|
|
188
|
+
if (!ver && data.length > 0)
|
|
189
|
+
this.version++;
|
|
175
190
|
return [2 /*return*/];
|
|
176
191
|
}
|
|
177
192
|
});
|
|
@@ -265,6 +280,16 @@ exports.liveQueryKeepAliveRoute = '_liveQueryKeepAlive';
|
|
|
265
280
|
* });
|
|
266
281
|
* ```
|
|
267
282
|
*
|
|
283
|
+
* #### Recovering after a dropped connection
|
|
284
|
+
* Messages published while the connection was down are not replayed.
|
|
285
|
+
* Use the `reconnect` listener to refetch state:
|
|
286
|
+
* ```ts
|
|
287
|
+
* chart.subscribe({
|
|
288
|
+
* next: (message) => (chartData = message.data),
|
|
289
|
+
* reconnect: () => loadChartData(),
|
|
290
|
+
* });
|
|
291
|
+
* ```
|
|
292
|
+
*
|
|
268
293
|
*/
|
|
269
294
|
var SubscriptionChannel = /** @class */ (function () {
|
|
270
295
|
/**
|
package/src/remult3/remult3.d.ts
CHANGED
|
@@ -1220,7 +1220,7 @@ export type ClassFieldDecorator<entityType, valueType> = ((target: any, context:
|
|
|
1220
1220
|
};
|
|
1221
1221
|
export declare const flags: {
|
|
1222
1222
|
error500RetryCount: number;
|
|
1223
|
-
/** Client treats SSE as dead if no event for this long */
|
|
1223
|
+
/** Client treats SSE as dead if no event for this long. Server pings every 15s; two missed pings plus slack, so a stalled event loop does not trigger a reconnect storm */
|
|
1224
1224
|
sseStaleMs: number;
|
|
1225
1225
|
/** HTTP keep-alive while SSE is healthy (touches lastUsed) */
|
|
1226
1226
|
liveQueryKeepAliveMs: number;
|
package/src/remult3/remult3.js
CHANGED
|
@@ -19,8 +19,8 @@ var wnn = [
|
|
|
19
19
|
];
|
|
20
20
|
exports.flags = {
|
|
21
21
|
error500RetryCount: 4,
|
|
22
|
-
/** Client treats SSE as dead if no event for this long */
|
|
23
|
-
sseStaleMs:
|
|
22
|
+
/** Client treats SSE as dead if no event for this long. Server pings every 15s; two missed pings plus slack, so a stalled event loop does not trigger a reconnect storm */
|
|
23
|
+
sseStaleMs: 40000,
|
|
24
24
|
/** HTTP keep-alive while SSE is healthy (touches lastUsed) */
|
|
25
25
|
liveQueryKeepAliveMs: 30000,
|
|
26
26
|
/** Version poll while SSE is stale */
|