@koolbase/core 12.10.0 → 12.11.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/dist/cjs/collection-controller.d.ts +14 -0
- package/dist/cjs/collection-controller.js +29 -0
- package/dist/cjs/live-source.d.ts +6 -0
- package/dist/cjs/live-source.js +16 -0
- package/dist/cjs/realtime.d.ts +12 -0
- package/dist/cjs/realtime.js +84 -7
- package/dist/esm/collection-controller.d.ts +14 -0
- package/dist/esm/collection-controller.js +29 -0
- package/dist/esm/live-source.d.ts +6 -0
- package/dist/esm/live-source.js +12 -0
- package/dist/esm/realtime.d.ts +12 -0
- package/dist/esm/realtime.js +84 -7
- package/package.json +1 -1
|
@@ -8,6 +8,13 @@ export interface CollectionQuery {
|
|
|
8
8
|
orderDesc?: boolean;
|
|
9
9
|
/** Page size; the server's default (20) when absent. */
|
|
10
10
|
limit?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Re-read page one, silently, when Koolbase realtime reports a record
|
|
13
|
+
* created, updated or deleted in this collection (debounced: a burst is one
|
|
14
|
+
* read). Realtime needs a signed-in user; until there is one, the list
|
|
15
|
+
* behaves as a normal list and goes live once the session exists.
|
|
16
|
+
*/
|
|
17
|
+
live?: boolean;
|
|
11
18
|
}
|
|
12
19
|
export interface CollectionState {
|
|
13
20
|
readonly status: CollectionStatus;
|
|
@@ -39,6 +46,8 @@ export declare class KoolbaseCollectionController {
|
|
|
39
46
|
private laterPages;
|
|
40
47
|
private nextOffset;
|
|
41
48
|
private unregister;
|
|
49
|
+
private unlive;
|
|
50
|
+
private liveTimer;
|
|
42
51
|
constructor(db: QueryReader, collection: string, query?: CollectionQuery);
|
|
43
52
|
getState(): CollectionState;
|
|
44
53
|
/** Called on every change. Returns the unsubscribe. */
|
|
@@ -53,6 +62,11 @@ export declare class KoolbaseCollectionController {
|
|
|
53
62
|
* shown stays while it runs, and stays if it fails.
|
|
54
63
|
*/
|
|
55
64
|
private refreshAfterWrite;
|
|
65
|
+
/**
|
|
66
|
+
* A change someone made, reported by realtime: page one again, silently, as
|
|
67
|
+
* after a write -- once per burst (250 ms), not once per event.
|
|
68
|
+
*/
|
|
69
|
+
private scheduleLiveRefresh;
|
|
56
70
|
/**
|
|
57
71
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
58
72
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.KoolbaseCollectionController = exports.initialCollectionState = void 0;
|
|
27
27
|
exports.collectionQueryKey = collectionQueryKey;
|
|
28
|
+
const live_source_js_1 = require("./live-source.js");
|
|
28
29
|
const query_refresh_js_1 = require("./query-refresh.js");
|
|
29
30
|
exports.initialCollectionState = Object.freeze({
|
|
30
31
|
status: 'loading',
|
|
@@ -49,6 +50,8 @@ function collectionQueryKey(collection, query = {}) {
|
|
|
49
50
|
query.orderBy ?? null,
|
|
50
51
|
query.orderDesc === true,
|
|
51
52
|
query.limit ?? null,
|
|
53
|
+
// Only when live: every other query keeps the key it always had.
|
|
54
|
+
...(query.live === true ? ['live'] : []),
|
|
52
55
|
]);
|
|
53
56
|
}
|
|
54
57
|
/** Page one, then the later pages without any record already shown. */
|
|
@@ -76,6 +79,9 @@ class KoolbaseCollectionController {
|
|
|
76
79
|
this.nextOffset = 0;
|
|
77
80
|
// A write to this collection re-runs this query (query-refresh.ts).
|
|
78
81
|
this.unregister = null;
|
|
82
|
+
// A live list's realtime subscription, and its pending debounced re-read.
|
|
83
|
+
this.unlive = null;
|
|
84
|
+
this.liveTimer = null;
|
|
79
85
|
this.db = db;
|
|
80
86
|
this.collection = collection;
|
|
81
87
|
this.spec = query;
|
|
@@ -97,6 +103,9 @@ class KoolbaseCollectionController {
|
|
|
97
103
|
if (this.disposed)
|
|
98
104
|
return;
|
|
99
105
|
this.unregister ?? (this.unregister = (0, query_refresh_js_1.registerQueryRefresher)(this.collection, () => this.refreshAfterWrite()));
|
|
106
|
+
if (this.spec.live === true) {
|
|
107
|
+
this.unlive ?? (this.unlive = (0, live_source_js_1.liveSource)()?.subscribe(this.collection, () => this.scheduleLiveRefresh()) ?? null);
|
|
108
|
+
}
|
|
100
109
|
await this.firstPage('default');
|
|
101
110
|
}
|
|
102
111
|
/** Page one again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
@@ -118,6 +127,20 @@ class KoolbaseCollectionController {
|
|
|
118
127
|
return;
|
|
119
128
|
await this.firstPage('network-only');
|
|
120
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* A change someone made, reported by realtime: page one again, silently, as
|
|
132
|
+
* after a write -- once per burst (250 ms), not once per event.
|
|
133
|
+
*/
|
|
134
|
+
scheduleLiveRefresh() {
|
|
135
|
+
if (this.disposed || this.liveTimer)
|
|
136
|
+
return;
|
|
137
|
+
this.liveTimer = setTimeout(() => {
|
|
138
|
+
this.liveTimer = null;
|
|
139
|
+
void this.refreshAfterWrite();
|
|
140
|
+
}, 250);
|
|
141
|
+
// A pending re-read must not be what keeps a Node process alive.
|
|
142
|
+
this.liveTimer.unref?.();
|
|
143
|
+
}
|
|
121
144
|
/**
|
|
122
145
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
123
146
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -153,6 +176,12 @@ class KoolbaseCollectionController {
|
|
|
153
176
|
this.disposed = true;
|
|
154
177
|
this.unregister?.();
|
|
155
178
|
this.unregister = null;
|
|
179
|
+
this.unlive?.();
|
|
180
|
+
this.unlive = null;
|
|
181
|
+
if (this.liveTimer) {
|
|
182
|
+
clearTimeout(this.liveTimer);
|
|
183
|
+
this.liveTimer = null;
|
|
184
|
+
}
|
|
156
185
|
this.listeners.clear();
|
|
157
186
|
}
|
|
158
187
|
async firstPage(cache) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface LiveSource {
|
|
2
|
+
/** Calls back on every record event in the collection; returns the unsubscribe. */
|
|
3
|
+
subscribe(collection: string, callback: (event: unknown) => void): () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function setLiveSource(source: LiveSource | null): void;
|
|
6
|
+
export declare function liveSource(): LiveSource | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The realtime client a live list subscribes through. KoolbaseRealtime
|
|
3
|
+
// registers itself when it is created, as query-refresh.ts registers the
|
|
4
|
+
// refreshers writes run: a list reaches realtime without the host packages
|
|
5
|
+
// passing it in. None registered (realtime never created): a live list is
|
|
6
|
+
// simply a list.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.setLiveSource = setLiveSource;
|
|
9
|
+
exports.liveSource = liveSource;
|
|
10
|
+
let active = null;
|
|
11
|
+
function setLiveSource(source) {
|
|
12
|
+
active = source;
|
|
13
|
+
}
|
|
14
|
+
function liveSource() {
|
|
15
|
+
return active;
|
|
16
|
+
}
|
package/dist/cjs/realtime.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export declare class KoolbaseRealtime {
|
|
|
9
9
|
private reconnectAttempts;
|
|
10
10
|
private reconnectTimer;
|
|
11
11
|
private connecting;
|
|
12
|
+
private idleTimer;
|
|
13
|
+
/** How long an unused connection stays open, in ms: a screen that comes
|
|
14
|
+
* straight back (navigating back, React's development double start)
|
|
15
|
+
* reuses it instead of opening another. */
|
|
16
|
+
static idleGraceMs: number;
|
|
12
17
|
/**
|
|
13
18
|
* Identifies whose cache a seen record belongs in.
|
|
14
19
|
*
|
|
@@ -39,6 +44,13 @@ export declare class KoolbaseRealtime {
|
|
|
39
44
|
* does not accumulate delay.
|
|
40
45
|
*/
|
|
41
46
|
private scheduleReconnect;
|
|
47
|
+
/**
|
|
48
|
+
* Closes the connection once nobody is subscribed -- after a moment, not at
|
|
49
|
+
* once: a screen that unmounts and mounts again keeps the connection it had.
|
|
50
|
+
* A subscribe within the moment cancels the close. Nothing reconnects after
|
|
51
|
+
* it: reconnecting needs a subscriber.
|
|
52
|
+
*/
|
|
53
|
+
private scheduleIdleClose;
|
|
42
54
|
disconnect(): void;
|
|
43
55
|
}
|
|
44
56
|
export {};
|
package/dist/cjs/realtime.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.KoolbaseRealtime = void 0;
|
|
4
4
|
const cache_store_js_1 = require("./cache-store.js");
|
|
5
5
|
const record_js_1 = require("./record.js");
|
|
6
|
+
const live_source_js_1 = require("./live-source.js");
|
|
6
7
|
const EVENT_TYPE_MAP = {
|
|
7
8
|
'db.record.created': 'created',
|
|
8
9
|
'db.record.updated': 'updated',
|
|
@@ -40,9 +41,14 @@ class KoolbaseRealtime {
|
|
|
40
41
|
this.reconnectAttempts = 0;
|
|
41
42
|
this.reconnectTimer = null;
|
|
42
43
|
this.connecting = false;
|
|
44
|
+
// Pending close of an unused connection (scheduleIdleClose); null while
|
|
45
|
+
// anyone is subscribed.
|
|
46
|
+
this.idleTimer = null;
|
|
43
47
|
this.config = config;
|
|
44
48
|
this.getToken = getToken;
|
|
45
49
|
this.getUserId = getUserId;
|
|
50
|
+
// Live lists (CollectionQuery.live) subscribe through this client.
|
|
51
|
+
(0, live_source_js_1.setLiveSource)(this);
|
|
46
52
|
}
|
|
47
53
|
/** Files a record seen over the socket, if we know whose it is. */
|
|
48
54
|
async cacheSeenRecord(collection, record) {
|
|
@@ -58,6 +64,10 @@ class KoolbaseRealtime {
|
|
|
58
64
|
await (0, cache_store_js_1.removeCachedRecord)(userId, recordId);
|
|
59
65
|
}
|
|
60
66
|
subscribe(collection, callback) {
|
|
67
|
+
if (this.idleTimer) {
|
|
68
|
+
clearTimeout(this.idleTimer);
|
|
69
|
+
this.idleTimer = null;
|
|
70
|
+
}
|
|
61
71
|
if (!this.listeners.has(collection))
|
|
62
72
|
this.listeners.set(collection, []);
|
|
63
73
|
this.listeners.get(collection).push(callback);
|
|
@@ -68,13 +78,16 @@ class KoolbaseRealtime {
|
|
|
68
78
|
void this.connect();
|
|
69
79
|
}
|
|
70
80
|
return () => {
|
|
71
|
-
const callbacks = this.listeners.get(collection)
|
|
72
|
-
const i = callbacks.indexOf(callback);
|
|
73
|
-
if (i
|
|
74
|
-
|
|
81
|
+
const callbacks = this.listeners.get(collection);
|
|
82
|
+
const i = callbacks ? callbacks.indexOf(callback) : -1;
|
|
83
|
+
if (!callbacks || i === -1)
|
|
84
|
+
return; // already unsubscribed: a second call does nothing
|
|
85
|
+
callbacks.splice(i, 1);
|
|
75
86
|
if (callbacks.length === 0) {
|
|
76
87
|
this.listeners.delete(collection);
|
|
77
88
|
this.sendUnsubscribe(collection);
|
|
89
|
+
if (this.listeners.size === 0)
|
|
90
|
+
this.scheduleIdleClose();
|
|
78
91
|
}
|
|
79
92
|
};
|
|
80
93
|
}
|
|
@@ -83,15 +96,39 @@ class KoolbaseRealtime {
|
|
|
83
96
|
return;
|
|
84
97
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING))
|
|
85
98
|
return;
|
|
86
|
-
|
|
99
|
+
// Claimed BEFORE the token is awaited. Claimed after it, every subscribe
|
|
100
|
+
// arriving meanwhile -- two live lists on one screen, React's development
|
|
101
|
+
// double start -- found nobody connecting and opened its own socket; only
|
|
102
|
+
// the last was remembered, and the others stayed open for good.
|
|
103
|
+
this.connecting = true;
|
|
104
|
+
let token;
|
|
105
|
+
try {
|
|
106
|
+
token = await this.getToken();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
token = null;
|
|
110
|
+
}
|
|
111
|
+
if (this.listeners.size === 0) {
|
|
112
|
+
// Everyone left while the token was on its way: nothing to connect for.
|
|
113
|
+
this.connecting = false;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
87
116
|
if (!token) {
|
|
117
|
+
this.connecting = false;
|
|
88
118
|
this.scheduleReconnect(); // sign-in may be in flight
|
|
89
119
|
return;
|
|
90
120
|
}
|
|
91
121
|
this.projectId = projectIdFromToken(token);
|
|
92
|
-
this.connecting = true;
|
|
93
122
|
const wsUrl = this.config.baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
|
|
94
|
-
|
|
123
|
+
let ws;
|
|
124
|
+
try {
|
|
125
|
+
ws = new WebSocket(`${wsUrl}/v1/realtime/ws?token=${encodeURIComponent(token)}`);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
this.connecting = false;
|
|
129
|
+
this.scheduleReconnect();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
95
132
|
this.ws = ws;
|
|
96
133
|
ws.onopen = () => {
|
|
97
134
|
this.connecting = false;
|
|
@@ -184,11 +221,47 @@ class KoolbaseRealtime {
|
|
|
184
221
|
}, delay);
|
|
185
222
|
this.reconnectTimer.unref?.();
|
|
186
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Closes the connection once nobody is subscribed -- after a moment, not at
|
|
226
|
+
* once: a screen that unmounts and mounts again keeps the connection it had.
|
|
227
|
+
* A subscribe within the moment cancels the close. Nothing reconnects after
|
|
228
|
+
* it: reconnecting needs a subscriber.
|
|
229
|
+
*/
|
|
230
|
+
scheduleIdleClose() {
|
|
231
|
+
if (this.idleTimer)
|
|
232
|
+
clearTimeout(this.idleTimer);
|
|
233
|
+
this.idleTimer = setTimeout(() => {
|
|
234
|
+
this.idleTimer = null;
|
|
235
|
+
if (this.listeners.size > 0)
|
|
236
|
+
return;
|
|
237
|
+
if (this.reconnectTimer) {
|
|
238
|
+
clearTimeout(this.reconnectTimer);
|
|
239
|
+
this.reconnectTimer = null;
|
|
240
|
+
}
|
|
241
|
+
const ws = this.ws;
|
|
242
|
+
this.ws = null;
|
|
243
|
+
this.connecting = false;
|
|
244
|
+
if (ws) {
|
|
245
|
+
// Detached first: this close is ours, not a dropped connection.
|
|
246
|
+
ws.onopen = null;
|
|
247
|
+
ws.onmessage = null;
|
|
248
|
+
ws.onclose = null;
|
|
249
|
+
ws.onerror = null;
|
|
250
|
+
ws.close();
|
|
251
|
+
}
|
|
252
|
+
}, KoolbaseRealtime.idleGraceMs);
|
|
253
|
+
this.idleTimer.unref?.();
|
|
254
|
+
}
|
|
187
255
|
disconnect() {
|
|
256
|
+
if (this.idleTimer) {
|
|
257
|
+
clearTimeout(this.idleTimer);
|
|
258
|
+
this.idleTimer = null;
|
|
259
|
+
}
|
|
188
260
|
if (this.reconnectTimer) {
|
|
189
261
|
clearTimeout(this.reconnectTimer);
|
|
190
262
|
this.reconnectTimer = null;
|
|
191
263
|
}
|
|
264
|
+
this.connecting = false;
|
|
192
265
|
this.ws?.close();
|
|
193
266
|
this.ws = null;
|
|
194
267
|
this.projectId = null;
|
|
@@ -196,3 +269,7 @@ class KoolbaseRealtime {
|
|
|
196
269
|
}
|
|
197
270
|
}
|
|
198
271
|
exports.KoolbaseRealtime = KoolbaseRealtime;
|
|
272
|
+
/** How long an unused connection stays open, in ms: a screen that comes
|
|
273
|
+
* straight back (navigating back, React's development double start)
|
|
274
|
+
* reuses it instead of opening another. */
|
|
275
|
+
KoolbaseRealtime.idleGraceMs = 1000;
|
|
@@ -8,6 +8,13 @@ export interface CollectionQuery {
|
|
|
8
8
|
orderDesc?: boolean;
|
|
9
9
|
/** Page size; the server's default (20) when absent. */
|
|
10
10
|
limit?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Re-read page one, silently, when Koolbase realtime reports a record
|
|
13
|
+
* created, updated or deleted in this collection (debounced: a burst is one
|
|
14
|
+
* read). Realtime needs a signed-in user; until there is one, the list
|
|
15
|
+
* behaves as a normal list and goes live once the session exists.
|
|
16
|
+
*/
|
|
17
|
+
live?: boolean;
|
|
11
18
|
}
|
|
12
19
|
export interface CollectionState {
|
|
13
20
|
readonly status: CollectionStatus;
|
|
@@ -39,6 +46,8 @@ export declare class KoolbaseCollectionController {
|
|
|
39
46
|
private laterPages;
|
|
40
47
|
private nextOffset;
|
|
41
48
|
private unregister;
|
|
49
|
+
private unlive;
|
|
50
|
+
private liveTimer;
|
|
42
51
|
constructor(db: QueryReader, collection: string, query?: CollectionQuery);
|
|
43
52
|
getState(): CollectionState;
|
|
44
53
|
/** Called on every change. Returns the unsubscribe. */
|
|
@@ -53,6 +62,11 @@ export declare class KoolbaseCollectionController {
|
|
|
53
62
|
* shown stays while it runs, and stays if it fails.
|
|
54
63
|
*/
|
|
55
64
|
private refreshAfterWrite;
|
|
65
|
+
/**
|
|
66
|
+
* A change someone made, reported by realtime: page one again, silently, as
|
|
67
|
+
* after a write -- once per burst (250 ms), not once per event.
|
|
68
|
+
*/
|
|
69
|
+
private scheduleLiveRefresh;
|
|
56
70
|
/**
|
|
57
71
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
58
72
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// Known limit: if rows are added or removed on the server between pages,
|
|
22
22
|
// the pages shift and a row can be skipped until refresh(), which reloads
|
|
23
23
|
// from page one. Duplicates are removed; a skipped row cannot be detected.
|
|
24
|
+
import { liveSource } from './live-source.js';
|
|
24
25
|
import { registerQueryRefresher } from './query-refresh.js';
|
|
25
26
|
export const initialCollectionState = Object.freeze({
|
|
26
27
|
status: 'loading',
|
|
@@ -45,6 +46,8 @@ export function collectionQueryKey(collection, query = {}) {
|
|
|
45
46
|
query.orderBy ?? null,
|
|
46
47
|
query.orderDesc === true,
|
|
47
48
|
query.limit ?? null,
|
|
49
|
+
// Only when live: every other query keeps the key it always had.
|
|
50
|
+
...(query.live === true ? ['live'] : []),
|
|
48
51
|
]);
|
|
49
52
|
}
|
|
50
53
|
/** Page one, then the later pages without any record already shown. */
|
|
@@ -72,6 +75,9 @@ export class KoolbaseCollectionController {
|
|
|
72
75
|
this.nextOffset = 0;
|
|
73
76
|
// A write to this collection re-runs this query (query-refresh.ts).
|
|
74
77
|
this.unregister = null;
|
|
78
|
+
// A live list's realtime subscription, and its pending debounced re-read.
|
|
79
|
+
this.unlive = null;
|
|
80
|
+
this.liveTimer = null;
|
|
75
81
|
this.db = db;
|
|
76
82
|
this.collection = collection;
|
|
77
83
|
this.spec = query;
|
|
@@ -93,6 +99,9 @@ export class KoolbaseCollectionController {
|
|
|
93
99
|
if (this.disposed)
|
|
94
100
|
return;
|
|
95
101
|
this.unregister ?? (this.unregister = registerQueryRefresher(this.collection, () => this.refreshAfterWrite()));
|
|
102
|
+
if (this.spec.live === true) {
|
|
103
|
+
this.unlive ?? (this.unlive = liveSource()?.subscribe(this.collection, () => this.scheduleLiveRefresh()) ?? null);
|
|
104
|
+
}
|
|
96
105
|
await this.firstPage('default');
|
|
97
106
|
}
|
|
98
107
|
/** Page one again, from the server. What is shown stays while it runs, and stays if it fails. */
|
|
@@ -114,6 +123,20 @@ export class KoolbaseCollectionController {
|
|
|
114
123
|
return;
|
|
115
124
|
await this.firstPage('network-only');
|
|
116
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* A change someone made, reported by realtime: page one again, silently, as
|
|
128
|
+
* after a write -- once per burst (250 ms), not once per event.
|
|
129
|
+
*/
|
|
130
|
+
scheduleLiveRefresh() {
|
|
131
|
+
if (this.disposed || this.liveTimer)
|
|
132
|
+
return;
|
|
133
|
+
this.liveTimer = setTimeout(() => {
|
|
134
|
+
this.liveTimer = null;
|
|
135
|
+
void this.refreshAfterWrite();
|
|
136
|
+
}, 250);
|
|
137
|
+
// A pending re-read must not be what keeps a Node process alive.
|
|
138
|
+
this.liveTimer.unref?.();
|
|
139
|
+
}
|
|
117
140
|
/**
|
|
118
141
|
* The next page, appended. Does nothing unless loaded with more to load.
|
|
119
142
|
* If rows are added or removed on the server between pages, one can be
|
|
@@ -149,6 +172,12 @@ export class KoolbaseCollectionController {
|
|
|
149
172
|
this.disposed = true;
|
|
150
173
|
this.unregister?.();
|
|
151
174
|
this.unregister = null;
|
|
175
|
+
this.unlive?.();
|
|
176
|
+
this.unlive = null;
|
|
177
|
+
if (this.liveTimer) {
|
|
178
|
+
clearTimeout(this.liveTimer);
|
|
179
|
+
this.liveTimer = null;
|
|
180
|
+
}
|
|
152
181
|
this.listeners.clear();
|
|
153
182
|
}
|
|
154
183
|
async firstPage(cache) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface LiveSource {
|
|
2
|
+
/** Calls back on every record event in the collection; returns the unsubscribe. */
|
|
3
|
+
subscribe(collection: string, callback: (event: unknown) => void): () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function setLiveSource(source: LiveSource | null): void;
|
|
6
|
+
export declare function liveSource(): LiveSource | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The realtime client a live list subscribes through. KoolbaseRealtime
|
|
2
|
+
// registers itself when it is created, as query-refresh.ts registers the
|
|
3
|
+
// refreshers writes run: a list reaches realtime without the host packages
|
|
4
|
+
// passing it in. None registered (realtime never created): a live list is
|
|
5
|
+
// simply a list.
|
|
6
|
+
let active = null;
|
|
7
|
+
export function setLiveSource(source) {
|
|
8
|
+
active = source;
|
|
9
|
+
}
|
|
10
|
+
export function liveSource() {
|
|
11
|
+
return active;
|
|
12
|
+
}
|
package/dist/esm/realtime.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export declare class KoolbaseRealtime {
|
|
|
9
9
|
private reconnectAttempts;
|
|
10
10
|
private reconnectTimer;
|
|
11
11
|
private connecting;
|
|
12
|
+
private idleTimer;
|
|
13
|
+
/** How long an unused connection stays open, in ms: a screen that comes
|
|
14
|
+
* straight back (navigating back, React's development double start)
|
|
15
|
+
* reuses it instead of opening another. */
|
|
16
|
+
static idleGraceMs: number;
|
|
12
17
|
/**
|
|
13
18
|
* Identifies whose cache a seen record belongs in.
|
|
14
19
|
*
|
|
@@ -39,6 +44,13 @@ export declare class KoolbaseRealtime {
|
|
|
39
44
|
* does not accumulate delay.
|
|
40
45
|
*/
|
|
41
46
|
private scheduleReconnect;
|
|
47
|
+
/**
|
|
48
|
+
* Closes the connection once nobody is subscribed -- after a moment, not at
|
|
49
|
+
* once: a screen that unmounts and mounts again keeps the connection it had.
|
|
50
|
+
* A subscribe within the moment cancels the close. Nothing reconnects after
|
|
51
|
+
* it: reconnecting needs a subscriber.
|
|
52
|
+
*/
|
|
53
|
+
private scheduleIdleClose;
|
|
42
54
|
disconnect(): void;
|
|
43
55
|
}
|
|
44
56
|
export {};
|
package/dist/esm/realtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cacheRecord, removeCachedRecord } from './cache-store.js';
|
|
2
2
|
import { recordFromWire } from './record.js';
|
|
3
|
+
import { setLiveSource } from './live-source.js';
|
|
3
4
|
const EVENT_TYPE_MAP = {
|
|
4
5
|
'db.record.created': 'created',
|
|
5
6
|
'db.record.updated': 'updated',
|
|
@@ -37,9 +38,14 @@ export class KoolbaseRealtime {
|
|
|
37
38
|
this.reconnectAttempts = 0;
|
|
38
39
|
this.reconnectTimer = null;
|
|
39
40
|
this.connecting = false;
|
|
41
|
+
// Pending close of an unused connection (scheduleIdleClose); null while
|
|
42
|
+
// anyone is subscribed.
|
|
43
|
+
this.idleTimer = null;
|
|
40
44
|
this.config = config;
|
|
41
45
|
this.getToken = getToken;
|
|
42
46
|
this.getUserId = getUserId;
|
|
47
|
+
// Live lists (CollectionQuery.live) subscribe through this client.
|
|
48
|
+
setLiveSource(this);
|
|
43
49
|
}
|
|
44
50
|
/** Files a record seen over the socket, if we know whose it is. */
|
|
45
51
|
async cacheSeenRecord(collection, record) {
|
|
@@ -55,6 +61,10 @@ export class KoolbaseRealtime {
|
|
|
55
61
|
await removeCachedRecord(userId, recordId);
|
|
56
62
|
}
|
|
57
63
|
subscribe(collection, callback) {
|
|
64
|
+
if (this.idleTimer) {
|
|
65
|
+
clearTimeout(this.idleTimer);
|
|
66
|
+
this.idleTimer = null;
|
|
67
|
+
}
|
|
58
68
|
if (!this.listeners.has(collection))
|
|
59
69
|
this.listeners.set(collection, []);
|
|
60
70
|
this.listeners.get(collection).push(callback);
|
|
@@ -65,13 +75,16 @@ export class KoolbaseRealtime {
|
|
|
65
75
|
void this.connect();
|
|
66
76
|
}
|
|
67
77
|
return () => {
|
|
68
|
-
const callbacks = this.listeners.get(collection)
|
|
69
|
-
const i = callbacks.indexOf(callback);
|
|
70
|
-
if (i
|
|
71
|
-
|
|
78
|
+
const callbacks = this.listeners.get(collection);
|
|
79
|
+
const i = callbacks ? callbacks.indexOf(callback) : -1;
|
|
80
|
+
if (!callbacks || i === -1)
|
|
81
|
+
return; // already unsubscribed: a second call does nothing
|
|
82
|
+
callbacks.splice(i, 1);
|
|
72
83
|
if (callbacks.length === 0) {
|
|
73
84
|
this.listeners.delete(collection);
|
|
74
85
|
this.sendUnsubscribe(collection);
|
|
86
|
+
if (this.listeners.size === 0)
|
|
87
|
+
this.scheduleIdleClose();
|
|
75
88
|
}
|
|
76
89
|
};
|
|
77
90
|
}
|
|
@@ -80,15 +93,39 @@ export class KoolbaseRealtime {
|
|
|
80
93
|
return;
|
|
81
94
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING))
|
|
82
95
|
return;
|
|
83
|
-
|
|
96
|
+
// Claimed BEFORE the token is awaited. Claimed after it, every subscribe
|
|
97
|
+
// arriving meanwhile -- two live lists on one screen, React's development
|
|
98
|
+
// double start -- found nobody connecting and opened its own socket; only
|
|
99
|
+
// the last was remembered, and the others stayed open for good.
|
|
100
|
+
this.connecting = true;
|
|
101
|
+
let token;
|
|
102
|
+
try {
|
|
103
|
+
token = await this.getToken();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
token = null;
|
|
107
|
+
}
|
|
108
|
+
if (this.listeners.size === 0) {
|
|
109
|
+
// Everyone left while the token was on its way: nothing to connect for.
|
|
110
|
+
this.connecting = false;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
84
113
|
if (!token) {
|
|
114
|
+
this.connecting = false;
|
|
85
115
|
this.scheduleReconnect(); // sign-in may be in flight
|
|
86
116
|
return;
|
|
87
117
|
}
|
|
88
118
|
this.projectId = projectIdFromToken(token);
|
|
89
|
-
this.connecting = true;
|
|
90
119
|
const wsUrl = this.config.baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
|
|
91
|
-
|
|
120
|
+
let ws;
|
|
121
|
+
try {
|
|
122
|
+
ws = new WebSocket(`${wsUrl}/v1/realtime/ws?token=${encodeURIComponent(token)}`);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
this.connecting = false;
|
|
126
|
+
this.scheduleReconnect();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
92
129
|
this.ws = ws;
|
|
93
130
|
ws.onopen = () => {
|
|
94
131
|
this.connecting = false;
|
|
@@ -181,14 +218,54 @@ export class KoolbaseRealtime {
|
|
|
181
218
|
}, delay);
|
|
182
219
|
this.reconnectTimer.unref?.();
|
|
183
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Closes the connection once nobody is subscribed -- after a moment, not at
|
|
223
|
+
* once: a screen that unmounts and mounts again keeps the connection it had.
|
|
224
|
+
* A subscribe within the moment cancels the close. Nothing reconnects after
|
|
225
|
+
* it: reconnecting needs a subscriber.
|
|
226
|
+
*/
|
|
227
|
+
scheduleIdleClose() {
|
|
228
|
+
if (this.idleTimer)
|
|
229
|
+
clearTimeout(this.idleTimer);
|
|
230
|
+
this.idleTimer = setTimeout(() => {
|
|
231
|
+
this.idleTimer = null;
|
|
232
|
+
if (this.listeners.size > 0)
|
|
233
|
+
return;
|
|
234
|
+
if (this.reconnectTimer) {
|
|
235
|
+
clearTimeout(this.reconnectTimer);
|
|
236
|
+
this.reconnectTimer = null;
|
|
237
|
+
}
|
|
238
|
+
const ws = this.ws;
|
|
239
|
+
this.ws = null;
|
|
240
|
+
this.connecting = false;
|
|
241
|
+
if (ws) {
|
|
242
|
+
// Detached first: this close is ours, not a dropped connection.
|
|
243
|
+
ws.onopen = null;
|
|
244
|
+
ws.onmessage = null;
|
|
245
|
+
ws.onclose = null;
|
|
246
|
+
ws.onerror = null;
|
|
247
|
+
ws.close();
|
|
248
|
+
}
|
|
249
|
+
}, KoolbaseRealtime.idleGraceMs);
|
|
250
|
+
this.idleTimer.unref?.();
|
|
251
|
+
}
|
|
184
252
|
disconnect() {
|
|
253
|
+
if (this.idleTimer) {
|
|
254
|
+
clearTimeout(this.idleTimer);
|
|
255
|
+
this.idleTimer = null;
|
|
256
|
+
}
|
|
185
257
|
if (this.reconnectTimer) {
|
|
186
258
|
clearTimeout(this.reconnectTimer);
|
|
187
259
|
this.reconnectTimer = null;
|
|
188
260
|
}
|
|
261
|
+
this.connecting = false;
|
|
189
262
|
this.ws?.close();
|
|
190
263
|
this.ws = null;
|
|
191
264
|
this.projectId = null;
|
|
192
265
|
this.listeners.clear();
|
|
193
266
|
}
|
|
194
267
|
}
|
|
268
|
+
/** How long an unused connection stays open, in ms: a screen that comes
|
|
269
|
+
* straight back (navigating back, React's development double start)
|
|
270
|
+
* reuses it instead of opening another. */
|
|
271
|
+
KoolbaseRealtime.idleGraceMs = 1000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koolbase/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.11.1",
|
|
4
4
|
"description": "Koolbase SDK core \u2014 shared behaviour behind @koolbase/react-native and @koolbase/js. Install one of those, not this.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"types": "./dist/esm/index.d.ts",
|