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.
@@ -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);
@@ -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
- }, 45000);
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
- }, 45000);
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
- res.success(await remult.liveQueryStorage.keepAliveAndReturnUnknownQueryIds(await this.coreOptions.getRequestBody(origReq)));
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) => {
@@ -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.client)
72
- if (this.queries.size === 0 && this.channels.size === 0) {
73
- this.runPromise(this.client.then((x) => x.close()));
74
- this.client = undefined;
75
- clearInterval(this.interval);
76
- this.interval = undefined;
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
- if (q.unsubscribe) {
98
- q.unsubscribe();
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
- }).then((unsubscribeToChannel) => {
130
+ })
131
+ .then((unsub) => {
108
132
  if (q.listeners.length == 0) {
109
- unsubscribeToChannel();
133
+ unsub();
110
134
  return;
111
135
  }
112
- this.runPromise(subscribe(q.queryChannel)
113
- .then((r) => {
114
- if (q.listeners.length === 0) {
115
- r.unsubscribe();
116
- unsubscribeToChannel();
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.interval = setInterval(async () => {
163
- const ids = [];
164
- for (const q of this.queries.values()) {
165
- ids.push(q.queryChannel);
166
- }
167
- if (ids.length > 0) {
168
- let p = this.apiProvider();
169
- const invalidIds = await this.runPromise(await remultStatic.actionInfo.runActionWithoutBlockingUI(() => buildRestDataProvider(p.httpClient).post(p.url + '/' + liveQueryKeepAliveRoute, ids)));
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
- source.close();
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 subscribeToChannel(channel);
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
- const createConnectionPromise = () => new Promise((res) => {
37
- createConnection();
38
- let retryCount = 0;
39
- function createConnection() {
40
- if (source)
41
- source.close();
42
- source = SseSubscriptionClient.createEventSource(remult.apiClient.url + '/' + streamUrl);
43
- source.onmessage = (e) => {
44
- let message = JSON.parse(e.data);
45
- const listeners = channels.get(message.channel);
46
- if (listeners)
47
- listeners.forEach((x) => x(message.data));
48
- };
49
- source.onerror = (e) => {
50
- console.error('Live Query Event Source Error', e);
51
- source.close();
52
- if (retryCount++ < flags.error500RetryCount) {
53
- setTimeout(() => {
54
- createConnection();
55
- }, 500);
56
- }
57
- };
58
- source.addEventListener('connectionId', async (e) => {
59
- //@ts-ignore
60
- connectionId = e.data;
61
- if (connected) {
62
- for (const channel of channels.keys()) {
63
- await subscribeToChannel(channel);
64
- }
65
- onReconnect();
66
- }
67
- else {
68
- connected = true;
69
- res(client);
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
- return createConnectionPromise();
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
- await createConnectionPromise();
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 = messages.filter(({ type }) => type == 'add' || type == 'replace');
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 messages) {
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
- }, messages);
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 result = [];
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
- result.push(id);
87
+ unknownQueryIds.push(id);
81
88
  }
82
89
  this.debug();
83
- return result;
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
  }