remult 3.3.18 → 3.4.0-next.1
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/SseSubscriptionServer.d.ts +2 -0
- package/SseSubscriptionServer.js +4 -2
- package/esm/SseSubscriptionServer.js +3 -1
- package/esm/live-query/data-provider-live-query-storage.js +3 -1
- package/esm/server/remult-api-server.js +6 -1
- package/esm/src/data-api.js +1 -0
- package/esm/src/live-query/LiveQueryClient.js +163 -52
- package/esm/src/live-query/SseSubscriptionClient.js +101 -43
- package/esm/src/live-query/SubscriptionChannel.js +37 -3
- package/esm/src/live-query/SubscriptionServer.js +11 -3
- package/esm/src/remult3/remult3.js +6 -0
- package/live-query/data-provider-live-query-storage.d.ts +4 -1
- package/live-query/data-provider-live-query-storage.js +18 -15
- package/package.json +1 -1
- package/remult-knex/index.d.ts +2 -2
- package/server/remult-api-server.js +13 -9
- package/src/column-interfaces.d.ts +4 -0
- package/src/data-api.js +1 -0
- package/src/live-query/LiveQueryClient.js +215 -97
- package/src/live-query/SseSubscriptionClient.js +143 -83
- package/src/live-query/SubscriptionChannel.d.ts +8 -0
- package/src/live-query/SubscriptionChannel.js +47 -9
- package/src/live-query/SubscriptionServer.d.ts +7 -2
- package/src/live-query/SubscriptionServer.js +20 -10
- package/src/remult3/remult3.d.ts +6 -0
- package/src/remult3/remult3.js +6 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { GenericResponse } from './server/index.js';
|
|
2
2
|
import type { Remult } from './src/context.js';
|
|
3
3
|
import type { SubscriptionServer } from './src/live-query/SubscriptionServer.js';
|
|
4
|
+
/** Keep below typical proxy idle (30s) and Heroku H15 (55s). */
|
|
5
|
+
export declare const ssePingMs = 15000;
|
|
4
6
|
export declare class SseSubscriptionServer implements SubscriptionServer {
|
|
5
7
|
private canUserConnectToChannel?;
|
|
6
8
|
constructor(canUserConnectToChannel?: ((channel: string, remult: Remult) => boolean) | undefined);
|
package/SseSubscriptionServer.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.clientConnection = exports.SseSubscriptionServer = void 0;
|
|
3
|
+
exports.clientConnection = exports.SseSubscriptionServer = exports.ssePingMs = void 0;
|
|
4
4
|
var tslib_1 = require("tslib");
|
|
5
5
|
var SseSubscriptionClient_js_1 = require("./src/live-query/SseSubscriptionClient.js");
|
|
6
|
+
/** Keep below typical proxy idle (30s) and Heroku H15 (55s). */
|
|
7
|
+
exports.ssePingMs = 15000;
|
|
6
8
|
var SseSubscriptionServer = /** @class */ (function () {
|
|
7
9
|
function SseSubscriptionServer(canUserConnectToChannel) {
|
|
8
10
|
this.canUserConnectToChannel = canUserConnectToChannel;
|
|
@@ -137,7 +139,7 @@ var clientConnection = /** @class */ (function () {
|
|
|
137
139
|
this.write('', 'keep-alive');
|
|
138
140
|
this.timeOutRef = setTimeout(function () {
|
|
139
141
|
_this.sendLiveMessage();
|
|
140
|
-
},
|
|
142
|
+
}, exports.ssePingMs);
|
|
141
143
|
};
|
|
142
144
|
return clientConnection;
|
|
143
145
|
}());
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ConnectionNotFoundError } from './src/live-query/SseSubscriptionClient.js';
|
|
2
|
+
/** Keep below typical proxy idle (30s) and Heroku H15 (55s). */
|
|
3
|
+
export const ssePingMs = 15_000;
|
|
2
4
|
export class SseSubscriptionServer {
|
|
3
5
|
canUserConnectToChannel;
|
|
4
6
|
//@internal
|
|
@@ -101,6 +103,6 @@ export class clientConnection {
|
|
|
101
103
|
this.write('', 'keep-alive');
|
|
102
104
|
this.timeOutRef = setTimeout(() => {
|
|
103
105
|
this.sendLiveMessage();
|
|
104
|
-
},
|
|
106
|
+
}, ssePingMs);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
@@ -52,12 +52,14 @@ export class DataProviderLiveQueryStorage {
|
|
|
52
52
|
}
|
|
53
53
|
async keepAliveAndReturnUnknownQueryIds(queryIds) {
|
|
54
54
|
const repo = await this.repo;
|
|
55
|
+
const versions = {};
|
|
55
56
|
for (const query of await repo.find({ where: { id: queryIds } })) {
|
|
56
57
|
query.lastUsedIso = new Date().toISOString();
|
|
57
58
|
await repo.save(query);
|
|
59
|
+
versions[query.id] = query.data?.version ?? 0;
|
|
58
60
|
queryIds = queryIds.filter((x) => x !== query.id);
|
|
59
61
|
}
|
|
60
|
-
return queryIds;
|
|
62
|
+
return { unknownQueryIds: queryIds, versions };
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
let LiveQueryStorageEntity = class LiveQueryStorageEntity extends EntityBase {
|
|
@@ -281,7 +281,12 @@ export class RemultServerImplementation {
|
|
|
281
281
|
}));
|
|
282
282
|
}
|
|
283
283
|
r.route(this.options.rootPath + '/' + liveQueryKeepAliveRoute).post(this.process(async (remult, req, res, reqInfo, origRes, origReq) => {
|
|
284
|
-
|
|
284
|
+
const body = await this.coreOptions.getRequestBody(origReq);
|
|
285
|
+
const queryIds = Array.isArray(body)
|
|
286
|
+
? body
|
|
287
|
+
: (body?.queryIds ?? []);
|
|
288
|
+
const result = await remult.liveQueryStorage.keepAliveAndReturnUnknownQueryIds(queryIds);
|
|
289
|
+
res.success(Array.isArray(body) ? result.unknownQueryIds : result);
|
|
285
290
|
}));
|
|
286
291
|
}
|
|
287
292
|
this.entities.forEach((e) => {
|
package/esm/src/data-api.js
CHANGED
|
@@ -358,6 +358,7 @@ export class DataApi {
|
|
|
358
358
|
requestJson: await serializeContext(),
|
|
359
359
|
findOptionsJson: findOptionsToJson(r.findOptions, this.repository.metadata),
|
|
360
360
|
lastIds: r.r.map((y) => this.repository.metadata.idMetadata.getId(y)),
|
|
361
|
+
version: 0,
|
|
361
362
|
};
|
|
362
363
|
await this.remult.liveQueryStorage.add({
|
|
363
364
|
entityKey: this.repository.metadata.key,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { buildRestDataProvider } from '../buildRestDataProvider.js';
|
|
2
2
|
import { RestDataProvider } from '../data-providers/rest-data-provider.js';
|
|
3
3
|
import { remultStatic } from '../remult-static.js';
|
|
4
|
+
import { flags } from '../remult3/remult3.js';
|
|
4
5
|
import { getRepositoryInternals } from '../remult3/repository-internals.js';
|
|
5
|
-
import { liveQueryKeepAliveRoute, LiveQuerySubscriber, } from './SubscriptionChannel.js';
|
|
6
|
+
import { liveQueryKeepAliveRoute, LiveQuerySubscriber, onBrowserForeground, } from './SubscriptionChannel.js';
|
|
6
7
|
/* @internal*/
|
|
7
8
|
export class LiveQueryClient {
|
|
8
9
|
apiProvider;
|
|
@@ -15,10 +16,10 @@ export class LiveQueryClient {
|
|
|
15
16
|
handleMessage();
|
|
16
17
|
}
|
|
17
18
|
queries = new Map();
|
|
19
|
+
channels = new Map();
|
|
18
20
|
hasQueriesForTesting() {
|
|
19
21
|
return this.queries.size > 0;
|
|
20
22
|
}
|
|
21
|
-
channels = new Map();
|
|
22
23
|
constructor(apiProvider, getUserId) {
|
|
23
24
|
this.apiProvider = apiProvider;
|
|
24
25
|
this.getUserId = getUserId;
|
|
@@ -29,6 +30,7 @@ export class LiveQueryClient {
|
|
|
29
30
|
close() {
|
|
30
31
|
this.queries.clear();
|
|
31
32
|
this.channels.clear();
|
|
33
|
+
this.detachForeground();
|
|
32
34
|
this.closeIfNoListeners();
|
|
33
35
|
}
|
|
34
36
|
async subscribeChannel(key, onResult) {
|
|
@@ -68,13 +70,15 @@ export class LiveQueryClient {
|
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
closeIfNoListeners() {
|
|
71
|
-
if (this.
|
|
72
|
-
if (this.
|
|
73
|
-
this.runPromise(this.client.then((x) => x.close()));
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
if (this.queries.size === 0 && this.channels.size === 0) {
|
|
74
|
+
if (this.client)
|
|
75
|
+
this.runPromise(this.client.then((x) => x.close()).catch(() => { }));
|
|
76
|
+
this.client = undefined;
|
|
77
|
+
this.openedConnection = undefined;
|
|
78
|
+
this.detachForeground();
|
|
79
|
+
clearInterval(this.interval);
|
|
80
|
+
this.interval = undefined;
|
|
81
|
+
}
|
|
78
82
|
}
|
|
79
83
|
subscribe(repo, options, listener) {
|
|
80
84
|
let alive = true;
|
|
@@ -94,43 +98,56 @@ export class LiveQueryClient {
|
|
|
94
98
|
if (!q) {
|
|
95
99
|
this.queries.set(eventTypeKey, (q = new LiveQuerySubscriber(repo, { entityKey: repo.metadata.key, options }, this.getUserId())));
|
|
96
100
|
q.subscribeCode = () => {
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
q.unsubscribeChannel();
|
|
102
|
+
let unsubscribeToChannel = () => { };
|
|
103
|
+
q.unsubscribeChannel = () => {
|
|
104
|
+
unsubscribeToChannel();
|
|
105
|
+
unsubscribeToChannel = () => { };
|
|
106
|
+
};
|
|
107
|
+
q.unsubscribe = () => {
|
|
99
108
|
q.unsubscribe = () => { };
|
|
100
|
-
|
|
109
|
+
q.unsubscribeChannel();
|
|
110
|
+
q.unsubscribeQuery();
|
|
111
|
+
};
|
|
112
|
+
let snapshotDone = false;
|
|
113
|
+
const applySnapshot = (r) => {
|
|
114
|
+
if (q.listeners.length === 0) {
|
|
115
|
+
r.unsubscribe();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
q.unsubscribeQuery = () => {
|
|
119
|
+
this.runPromise(r.unsubscribe());
|
|
120
|
+
};
|
|
121
|
+
snapshotDone = true;
|
|
122
|
+
return this.runPromise(q.setAllItems(r.result));
|
|
123
|
+
};
|
|
101
124
|
this.runPromise(this.subscribeChannel(q.queryChannel, {
|
|
102
125
|
next: (value) => this.runPromise(q.handle(value)),
|
|
103
126
|
complete: () => { },
|
|
104
127
|
error: (er) => {
|
|
105
128
|
q.listeners.forEach((l) => l.error(er));
|
|
106
129
|
},
|
|
107
|
-
})
|
|
130
|
+
})
|
|
131
|
+
.then((unsub) => {
|
|
108
132
|
if (q.listeners.length == 0) {
|
|
109
|
-
|
|
133
|
+
unsub();
|
|
110
134
|
return;
|
|
111
135
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
this.runPromise(q.setAllItems(r.result));
|
|
120
|
-
q.unsubscribe = () => {
|
|
121
|
-
q.unsubscribe = () => { };
|
|
122
|
-
unsubscribeToChannel();
|
|
123
|
-
this.runPromise(r.unsubscribe());
|
|
124
|
-
};
|
|
125
|
-
})
|
|
126
|
-
.catch((err) => {
|
|
127
|
-
q.listeners.forEach((l) => l.error(err));
|
|
128
|
-
unsubscribeToChannel();
|
|
129
|
-
this.queries.delete(eventTypeKey);
|
|
130
|
-
}));
|
|
131
|
-
})).catch((err) => {
|
|
136
|
+
unsubscribeToChannel = unsub;
|
|
137
|
+
if (snapshotDone)
|
|
138
|
+
return this.runPromise(this.runKeepAlive({ checkVersions: true }));
|
|
139
|
+
})
|
|
140
|
+
.catch((err) => {
|
|
132
141
|
q.listeners.forEach((l) => l.error(err));
|
|
133
|
-
});
|
|
142
|
+
}));
|
|
143
|
+
this.runPromise(subscribe(q.queryChannel)
|
|
144
|
+
.then(applySnapshot)
|
|
145
|
+
.catch((err) => {
|
|
146
|
+
q.listeners.forEach((l) => l.error(err));
|
|
147
|
+
unsubscribeToChannel();
|
|
148
|
+
this.queries.delete(eventTypeKey);
|
|
149
|
+
this.closeIfNoListeners();
|
|
150
|
+
}));
|
|
134
151
|
};
|
|
135
152
|
q.subscribeCode();
|
|
136
153
|
}
|
|
@@ -156,29 +173,123 @@ export class LiveQueryClient {
|
|
|
156
173
|
};
|
|
157
174
|
}
|
|
158
175
|
client;
|
|
176
|
+
openedConnection;
|
|
159
177
|
interval;
|
|
178
|
+
lastKeepAliveAt = 0;
|
|
179
|
+
keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
180
|
+
keepAliveInFlight;
|
|
181
|
+
foregroundKeepAliveTimer;
|
|
182
|
+
isSseStale() {
|
|
183
|
+
const conn = this.openedConnection;
|
|
184
|
+
if (!conn || conn.lastServerEvent === undefined)
|
|
185
|
+
return false;
|
|
186
|
+
return Date.now() - conn.lastServerEvent > flags.sseStaleMs;
|
|
187
|
+
}
|
|
188
|
+
async runKeepAlive(opts) {
|
|
189
|
+
if (this.keepAliveInFlight)
|
|
190
|
+
return this.keepAliveInFlight;
|
|
191
|
+
this.keepAliveInFlight = this.sendKeepAlive(opts).finally(() => {
|
|
192
|
+
this.keepAliveInFlight = undefined;
|
|
193
|
+
});
|
|
194
|
+
return this.keepAliveInFlight;
|
|
195
|
+
}
|
|
196
|
+
async sendKeepAlive(opts) {
|
|
197
|
+
const ids = [];
|
|
198
|
+
for (const q of this.queries.values()) {
|
|
199
|
+
ids.push(q.queryChannel);
|
|
200
|
+
}
|
|
201
|
+
if (ids.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
let p = this.apiProvider();
|
|
204
|
+
const raw = await this.runPromise(remultStatic.actionInfo.runActionWithoutBlockingUI(() => buildRestDataProvider(p.httpClient).post(p.url + '/' + liveQueryKeepAliveRoute, { queryIds: ids })));
|
|
205
|
+
const unknownIds = Array.isArray(raw)
|
|
206
|
+
? raw
|
|
207
|
+
: (raw?.unknownQueryIds ?? []);
|
|
208
|
+
const versions = Array.isArray(raw)
|
|
209
|
+
? {}
|
|
210
|
+
: (raw?.versions ?? {});
|
|
211
|
+
const stale = opts?.checkVersions || this.isSseStale();
|
|
212
|
+
let reloaded = false;
|
|
213
|
+
for (const q of this.queries.values()) {
|
|
214
|
+
const serverVersion = versions[q.queryChannel];
|
|
215
|
+
const unknown = unknownIds.includes(q.queryChannel);
|
|
216
|
+
const mismatch = stale && serverVersion !== undefined && serverVersion !== q.version;
|
|
217
|
+
if (unknown || mismatch) {
|
|
218
|
+
reloaded = true;
|
|
219
|
+
if (serverVersion !== undefined)
|
|
220
|
+
q.version = serverVersion;
|
|
221
|
+
q.subscribeCode();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (this.isSseStale()) {
|
|
225
|
+
this.keepAliveBackoffMs = reloaded
|
|
226
|
+
? flags.liveQueryPollWhenStaleMs
|
|
227
|
+
: Math.min(this.keepAliveBackoffMs * 2, flags.liveQueryKeepAliveMs);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async maybeKeepAlive() {
|
|
234
|
+
const stale = this.isSseStale();
|
|
235
|
+
if (!stale)
|
|
236
|
+
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
237
|
+
const interval = stale
|
|
238
|
+
? this.keepAliveBackoffMs
|
|
239
|
+
: flags.liveQueryKeepAliveMs;
|
|
240
|
+
if (Date.now() - this.lastKeepAliveAt < interval)
|
|
241
|
+
return;
|
|
242
|
+
this.lastKeepAliveAt = Date.now();
|
|
243
|
+
if (stale)
|
|
244
|
+
this.openedConnection?.resume?.(true);
|
|
245
|
+
await this.runKeepAlive();
|
|
246
|
+
}
|
|
247
|
+
detachForeground = () => { };
|
|
248
|
+
attachForeground() {
|
|
249
|
+
this.detachForeground();
|
|
250
|
+
const unsub = onBrowserForeground(() => {
|
|
251
|
+
this.openedConnection?.resume?.();
|
|
252
|
+
if (this.foregroundKeepAliveTimer !== undefined)
|
|
253
|
+
clearTimeout(this.foregroundKeepAliveTimer);
|
|
254
|
+
this.foregroundKeepAliveTimer = setTimeout(() => {
|
|
255
|
+
this.foregroundKeepAliveTimer = undefined;
|
|
256
|
+
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
257
|
+
this.lastKeepAliveAt = 0;
|
|
258
|
+
this.runPromise(this.runKeepAlive({ checkVersions: true }));
|
|
259
|
+
}, 300);
|
|
260
|
+
});
|
|
261
|
+
this.detachForeground = () => {
|
|
262
|
+
unsub();
|
|
263
|
+
if (this.foregroundKeepAliveTimer !== undefined) {
|
|
264
|
+
clearTimeout(this.foregroundKeepAliveTimer);
|
|
265
|
+
this.foregroundKeepAliveTimer = undefined;
|
|
266
|
+
}
|
|
267
|
+
this.detachForeground = () => { };
|
|
268
|
+
};
|
|
269
|
+
}
|
|
160
270
|
openIfNoOpened() {
|
|
161
271
|
if (!this.client) {
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
for (const id of invalidIds) {
|
|
171
|
-
for (const q of this.queries.values()) {
|
|
172
|
-
if (q.queryChannel === id)
|
|
173
|
-
q.subscribeCode();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}, 30000);
|
|
178
|
-
return this.runPromise((this.client = this.apiProvider().subscriptionClient.openConnection(() => {
|
|
272
|
+
this.lastKeepAliveAt = Date.now();
|
|
273
|
+
this.keepAliveBackoffMs = flags.liveQueryPollWhenStaleMs;
|
|
274
|
+
this.attachForeground();
|
|
275
|
+
this.interval = setInterval(() => {
|
|
276
|
+
this.runPromise(this.maybeKeepAlive());
|
|
277
|
+
}, Math.min(flags.liveQueryPollWhenStaleMs, flags.liveQueryKeepAliveMs));
|
|
278
|
+
return this.runPromise((this.client = this.apiProvider()
|
|
279
|
+
.subscriptionClient.openConnection(() => {
|
|
179
280
|
for (const q of this.queries.values()) {
|
|
180
281
|
q.subscribeCode();
|
|
181
282
|
}
|
|
283
|
+
})
|
|
284
|
+
.then((c) => {
|
|
285
|
+
this.openedConnection = c;
|
|
286
|
+
return c;
|
|
287
|
+
})
|
|
288
|
+
.catch((err) => {
|
|
289
|
+
this.client = undefined;
|
|
290
|
+
this.openedConnection = undefined;
|
|
291
|
+
this.closeIfNoListeners();
|
|
292
|
+
throw err;
|
|
182
293
|
})));
|
|
183
294
|
}
|
|
184
295
|
return this.client;
|
|
@@ -1,24 +1,68 @@
|
|
|
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';
|
|
5
4
|
import { streamUrl } from './SubscriptionChannel.js';
|
|
6
5
|
export class SseSubscriptionClient {
|
|
7
6
|
openConnection(onReconnect) {
|
|
8
7
|
let connectionId;
|
|
9
8
|
const channels = new Map();
|
|
10
9
|
const provider = buildRestDataProvider(remult.apiClient.httpClient);
|
|
11
|
-
let connected = false;
|
|
12
10
|
let source;
|
|
11
|
+
let retryCount = 0;
|
|
12
|
+
let closed = false;
|
|
13
|
+
let connected = false;
|
|
14
|
+
let reconnectTimer;
|
|
15
|
+
let connectionReady;
|
|
16
|
+
let connectionPromise = new Promise((res) => {
|
|
17
|
+
connectionReady = res;
|
|
18
|
+
});
|
|
19
|
+
function clearReconnectTimer() {
|
|
20
|
+
if (reconnectTimer !== undefined) {
|
|
21
|
+
clearTimeout(reconnectTimer);
|
|
22
|
+
reconnectTimer = undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function scheduleReconnect() {
|
|
26
|
+
if (closed)
|
|
27
|
+
return;
|
|
28
|
+
clearReconnectTimer();
|
|
29
|
+
const delay = Math.min(500 * 2 ** Math.min(retryCount, 5), 8_000);
|
|
30
|
+
retryCount++;
|
|
31
|
+
reconnectTimer = setTimeout(() => {
|
|
32
|
+
reconnectTimer = undefined;
|
|
33
|
+
createConnection();
|
|
34
|
+
}, delay);
|
|
35
|
+
}
|
|
36
|
+
function resume(force) {
|
|
37
|
+
if (closed)
|
|
38
|
+
return;
|
|
39
|
+
retryCount = 0;
|
|
40
|
+
clearReconnectTimer();
|
|
41
|
+
if (force) {
|
|
42
|
+
createConnection();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const open = source &&
|
|
46
|
+
typeof EventSource !== 'undefined' &&
|
|
47
|
+
source.readyState !== EventSource.CLOSED;
|
|
48
|
+
if (!open)
|
|
49
|
+
createConnection();
|
|
50
|
+
}
|
|
13
51
|
const client = {
|
|
52
|
+
lastServerEvent: Date.now(),
|
|
53
|
+
resume,
|
|
14
54
|
close() {
|
|
15
|
-
|
|
55
|
+
closed = true;
|
|
56
|
+
clearReconnectTimer();
|
|
57
|
+
source?.close();
|
|
16
58
|
},
|
|
17
59
|
async subscribe(channel, handler) {
|
|
18
60
|
let listeners = channels.get(channel);
|
|
19
61
|
if (!listeners) {
|
|
20
62
|
channels.set(channel, (listeners = []));
|
|
21
|
-
await
|
|
63
|
+
await connectionPromise;
|
|
64
|
+
if (!closed)
|
|
65
|
+
await subscribeToChannel(channel);
|
|
22
66
|
}
|
|
23
67
|
listeners.push(handler);
|
|
24
68
|
return () => {
|
|
@@ -33,45 +77,52 @@ export class SseSubscriptionClient {
|
|
|
33
77
|
};
|
|
34
78
|
},
|
|
35
79
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
source.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
function noteServerEvent() {
|
|
81
|
+
client.lastServerEvent = Date.now();
|
|
82
|
+
}
|
|
83
|
+
function createConnection() {
|
|
84
|
+
if (closed)
|
|
85
|
+
return;
|
|
86
|
+
clearReconnectTimer();
|
|
87
|
+
if (source)
|
|
88
|
+
source.close();
|
|
89
|
+
source = SseSubscriptionClient.createEventSource(remult.apiClient.url + '/' + streamUrl);
|
|
90
|
+
source.onmessage = (e) => {
|
|
91
|
+
noteServerEvent();
|
|
92
|
+
let message = JSON.parse(e.data);
|
|
93
|
+
const listeners = channels.get(message.channel);
|
|
94
|
+
if (listeners)
|
|
95
|
+
listeners.forEach((x) => x(message.data));
|
|
96
|
+
};
|
|
97
|
+
source.addEventListener('keep-alive', () => {
|
|
98
|
+
noteServerEvent();
|
|
99
|
+
});
|
|
100
|
+
source.onerror = (e) => {
|
|
101
|
+
console.error('Live Query Event Source Error', e);
|
|
102
|
+
source.close();
|
|
103
|
+
if (closed)
|
|
104
|
+
return;
|
|
105
|
+
scheduleReconnect();
|
|
106
|
+
};
|
|
107
|
+
source.addEventListener('connectionId', async (e) => {
|
|
108
|
+
//@ts-ignore
|
|
109
|
+
connectionId = e.data;
|
|
110
|
+
retryCount = 0;
|
|
111
|
+
noteServerEvent();
|
|
112
|
+
if (connected) {
|
|
113
|
+
for (const channel of channels.keys()) {
|
|
114
|
+
await subscribeToChannel(channel);
|
|
70
115
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
116
|
+
onReconnect();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
connected = true;
|
|
120
|
+
connectionReady();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
createConnection();
|
|
125
|
+
return Promise.resolve(client);
|
|
75
126
|
async function subscribeToChannel(channel) {
|
|
76
127
|
const result = await remultStatic.actionInfo.runActionWithoutBlockingUI(() => {
|
|
77
128
|
return provider.post(remult.apiClient.url + '/' + streamUrl + '/subscribe', {
|
|
@@ -80,7 +131,14 @@ export class SseSubscriptionClient {
|
|
|
80
131
|
});
|
|
81
132
|
});
|
|
82
133
|
if (result === ConnectionNotFoundError) {
|
|
83
|
-
|
|
134
|
+
connected = false;
|
|
135
|
+
connectionPromise = new Promise((res) => {
|
|
136
|
+
connectionReady = res;
|
|
137
|
+
});
|
|
138
|
+
createConnection();
|
|
139
|
+
await connectionPromise;
|
|
140
|
+
if (!closed)
|
|
141
|
+
await subscribeToChannel(channel);
|
|
84
142
|
}
|
|
85
143
|
}
|
|
86
144
|
}
|
|
@@ -12,6 +12,8 @@ export class LiveQuerySubscriber {
|
|
|
12
12
|
queryChannel;
|
|
13
13
|
subscribeCode;
|
|
14
14
|
unsubscribe = () => { };
|
|
15
|
+
unsubscribeChannel = () => { };
|
|
16
|
+
unsubscribeQuery = () => { };
|
|
15
17
|
async setAllItems(result) {
|
|
16
18
|
const items = await getRepositoryInternals(this.repo)._fromJsonArray(result, this.query.options);
|
|
17
19
|
this.forListeners((listener) => {
|
|
@@ -19,6 +21,7 @@ export class LiveQuerySubscriber {
|
|
|
19
21
|
return items;
|
|
20
22
|
});
|
|
21
23
|
}, this.allItemsMessage(items));
|
|
24
|
+
this.version = 0;
|
|
22
25
|
}
|
|
23
26
|
allItemsMessage(items) {
|
|
24
27
|
return [
|
|
@@ -52,8 +55,19 @@ export class LiveQuerySubscriber {
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
async handle(messages) {
|
|
58
|
+
const ver = messages.find((m) => m.type === 'version');
|
|
59
|
+
if (ver) {
|
|
60
|
+
if (ver.from < this.version)
|
|
61
|
+
return;
|
|
62
|
+
if (ver.from > this.version) {
|
|
63
|
+
this.version = ver.to;
|
|
64
|
+
this.subscribeCode?.();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const data = messages.filter((m) => m.type !== 'version');
|
|
55
69
|
{
|
|
56
|
-
let x =
|
|
70
|
+
let x = data.filter(({ type }) => type == 'add' || type == 'replace');
|
|
57
71
|
let loadedItems = await getRepositoryInternals(this.repo)._fromJsonArray(x.map((m) => m.data.item), this.query.options);
|
|
58
72
|
for (let index = 0; index < x.length; index++) {
|
|
59
73
|
const element = x[index];
|
|
@@ -64,7 +78,7 @@ export class LiveQuerySubscriber {
|
|
|
64
78
|
listener((items) => {
|
|
65
79
|
if (!items)
|
|
66
80
|
items = [];
|
|
67
|
-
for (const message of
|
|
81
|
+
for (const message of data) {
|
|
68
82
|
switch (message.type) {
|
|
69
83
|
case 'all':
|
|
70
84
|
this.setAllItems(message.data);
|
|
@@ -87,11 +101,13 @@ export class LiveQuerySubscriber {
|
|
|
87
101
|
}
|
|
88
102
|
return items;
|
|
89
103
|
});
|
|
90
|
-
},
|
|
104
|
+
}, data);
|
|
105
|
+
this.version = ver ? ver.to : this.version + (data.length > 0 ? 1 : 0);
|
|
91
106
|
}
|
|
92
107
|
defaultQueryState = [];
|
|
93
108
|
listeners = [];
|
|
94
109
|
id = String(crypto.randomUUID());
|
|
110
|
+
version = 0;
|
|
95
111
|
constructor(repo, query, userId) {
|
|
96
112
|
this.repo = repo;
|
|
97
113
|
this.query = query;
|
|
@@ -99,6 +115,24 @@ export class LiveQuerySubscriber {
|
|
|
99
115
|
this.id = this.queryChannel;
|
|
100
116
|
}
|
|
101
117
|
}
|
|
118
|
+
//@internal
|
|
119
|
+
export function onBrowserForeground(handler) {
|
|
120
|
+
if (typeof document === 'undefined')
|
|
121
|
+
return () => { };
|
|
122
|
+
function onForeground() {
|
|
123
|
+
if (document.visibilityState === 'hidden')
|
|
124
|
+
return;
|
|
125
|
+
handler();
|
|
126
|
+
}
|
|
127
|
+
document.addEventListener('visibilitychange', onForeground);
|
|
128
|
+
window.addEventListener('online', onForeground);
|
|
129
|
+
window.addEventListener('pageshow', onForeground);
|
|
130
|
+
return () => {
|
|
131
|
+
document.removeEventListener('visibilitychange', onForeground);
|
|
132
|
+
window.removeEventListener('online', onForeground);
|
|
133
|
+
window.removeEventListener('pageshow', onForeground);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
102
136
|
export const liveQueryKeepAliveRoute = '_liveQueryKeepAlive';
|
|
103
137
|
/**
|
|
104
138
|
* The `SubscriptionChannel` class is used to send messages from the backend to the frontend,
|
|
@@ -57,6 +57,11 @@ export class LiveQueryPublisher {
|
|
|
57
57
|
messages,
|
|
58
58
|
});
|
|
59
59
|
query.lastIds = currentIds;
|
|
60
|
+
if (messages.length > 0) {
|
|
61
|
+
const from = query.version ?? 0;
|
|
62
|
+
query.version = from + 1;
|
|
63
|
+
messages.push({ type: 'version', from, to: query.version });
|
|
64
|
+
}
|
|
60
65
|
await setData(query);
|
|
61
66
|
if (messages.length > 0)
|
|
62
67
|
this.subscriptionServer().publishMessage(q.id, messages);
|
|
@@ -70,21 +75,24 @@ export class InMemoryLiveQueryStorage {
|
|
|
70
75
|
this.debugFileSaver(this.queries);
|
|
71
76
|
}
|
|
72
77
|
async keepAliveAndReturnUnknownQueryIds(ids) {
|
|
73
|
-
const
|
|
78
|
+
const unknownQueryIds = [];
|
|
79
|
+
const versions = {};
|
|
74
80
|
for (const id of ids) {
|
|
75
81
|
let q = this.queries.find((q) => q.id === id);
|
|
76
82
|
if (q) {
|
|
77
83
|
q.lastUsed = new Date().toISOString();
|
|
84
|
+
versions[id] = q.data?.version ?? 0;
|
|
78
85
|
}
|
|
79
86
|
else
|
|
80
|
-
|
|
87
|
+
unknownQueryIds.push(id);
|
|
81
88
|
}
|
|
82
89
|
this.debug();
|
|
83
|
-
return
|
|
90
|
+
return { unknownQueryIds, versions };
|
|
84
91
|
}
|
|
85
92
|
queries = [];
|
|
86
93
|
constructor() { }
|
|
87
94
|
async add(query) {
|
|
95
|
+
this.queries = this.queries.filter((q) => q.id !== query.id);
|
|
88
96
|
this.queries.push({ ...query, lastUsed: new Date().toISOString() });
|
|
89
97
|
this.debug();
|
|
90
98
|
}
|