@koolbase/core 12.12.0 → 12.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,8 +11,9 @@ export interface CollectionQuery {
11
11
  /**
12
12
  * Re-read page one, silently, when Koolbase realtime reports a record
13
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.
14
+ * read). Signed out, only a collection anyone can read (read rule
15
+ * "public") is live; any other behaves as a normal list until a user signs
16
+ * in, and goes live then.
16
17
  */
17
18
  live?: boolean;
18
19
  }
@@ -9,6 +9,9 @@ export declare class KoolbaseRealtime {
9
9
  private reconnectAttempts;
10
10
  private reconnectTimer;
11
11
  private connecting;
12
+ private anonymous;
13
+ private connectedAs;
14
+ private generation;
12
15
  private idleTimer;
13
16
  /** How long an unused connection stays open, in ms: a screen that comes
14
17
  * straight back (navigating back, React's development double start)
@@ -30,6 +33,9 @@ export declare class KoolbaseRealtime {
30
33
  private connect;
31
34
  private sendSubscribe;
32
35
  private sendUnsubscribe;
36
+ /** A signed-out visitor's message leaves project_id out: it has none to
37
+ * give, and the server uses the public key's project. */
38
+ private send;
33
39
  /**
34
40
  * Reconnects with backoff, rather than every three seconds forever.
35
41
  *
@@ -51,6 +57,16 @@ export declare class KoolbaseRealtime {
51
57
  * it: reconnecting needs a subscriber.
52
58
  */
53
59
  private scheduleIdleClose;
60
+ /**
61
+ * The signed-in user changed: signed in, signed out, or another user. The
62
+ * open connection was made for the previous session -- signed out it sees
63
+ * only public collections, signed in it carries that user's access -- so it
64
+ * is replaced by one for the new session, resubscribing every collection.
65
+ * The same user again (a token refresh) changes nothing, and with nothing
66
+ * open there is nothing to do: the next subscribe connects as whoever is
67
+ * signed in then.
68
+ */
69
+ sessionChanged(): void;
54
70
  disconnect(): void;
55
71
  }
56
72
  export {};
@@ -41,6 +41,15 @@ class KoolbaseRealtime {
41
41
  this.reconnectAttempts = 0;
42
42
  this.reconnectTimer = null;
43
43
  this.connecting = false;
44
+ // The open connection is a signed-out visitor's (the public key): its
45
+ // messages leave project_id out.
46
+ this.anonymous = false;
47
+ // Whose session the open connection was made for: the user id, or null
48
+ // when signed out. sessionChanged compares against it.
49
+ this.connectedAs = null;
50
+ // Bumped by sessionChanged: a connect still waiting for its token gives up
51
+ // rather than open a connection for the old session.
52
+ this.generation = 0;
44
53
  // Pending close of an unused connection (scheduleIdleClose); null while
45
54
  // anyone is subscribed.
46
55
  this.idleTimer = null;
@@ -101,28 +110,49 @@ class KoolbaseRealtime {
101
110
  // double start -- found nobody connecting and opened its own socket; only
102
111
  // the last was remembered, and the others stayed open for good.
103
112
  this.connecting = true;
113
+ const generation = this.generation;
104
114
  let token;
105
115
  try {
106
116
  token = await this.getToken();
107
117
  }
108
118
  catch {
109
- token = null;
119
+ // A session that could not be refreshed is still a session: try again
120
+ // later, never as a signed-out visitor.
121
+ if (generation !== this.generation)
122
+ return;
123
+ this.connecting = false;
124
+ this.scheduleReconnect();
125
+ return;
110
126
  }
127
+ // The session changed while the token was on its way: sessionChanged
128
+ // has started the connection for the new one.
129
+ if (generation !== this.generation)
130
+ return;
111
131
  if (this.listeners.size === 0) {
112
132
  // Everyone left while the token was on its way: nothing to connect for.
113
133
  this.connecting = false;
114
134
  return;
115
135
  }
116
- if (!token) {
136
+ const userId = this.getUserId?.() ?? null;
137
+ if (!token && userId) {
138
+ // Signed in, but no usable token right now: try again later rather
139
+ // than drop to public-only.
117
140
  this.connecting = false;
118
- this.scheduleReconnect(); // sign-in may be in flight
141
+ this.scheduleReconnect();
119
142
  return;
120
143
  }
121
- this.projectId = projectIdFromToken(token);
144
+ // Signed out: the project's public key, and only collections anyone can
145
+ // read. The server pins the connection to the key's project.
146
+ this.anonymous = !token;
147
+ this.connectedAs = token ? userId : null;
148
+ this.projectId = token ? projectIdFromToken(token) : null;
149
+ const credential = token
150
+ ? `token=${encodeURIComponent(token)}`
151
+ : `public_key=${encodeURIComponent(this.config.publicKey)}`;
122
152
  const wsUrl = this.config.baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
123
153
  let ws;
124
154
  try {
125
- ws = new WebSocket(`${wsUrl}/v1/realtime/ws?token=${encodeURIComponent(token)}`);
155
+ ws = new WebSocket(`${wsUrl}/v1/realtime/ws?${credential}`);
126
156
  }
127
157
  catch {
128
158
  this.connecting = false;
@@ -186,14 +216,22 @@ class KoolbaseRealtime {
186
216
  ws.onerror = () => { };
187
217
  }
188
218
  sendSubscribe(collection) {
189
- if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
190
- return;
191
- this.ws.send(JSON.stringify({ action: 'subscribe', project_id: this.projectId, collection }));
219
+ this.send('subscribe', collection);
192
220
  }
193
221
  sendUnsubscribe(collection) {
194
- if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
222
+ this.send('unsubscribe', collection);
223
+ }
224
+ /** A signed-out visitor's message leaves project_id out: it has none to
225
+ * give, and the server uses the public key's project. */
226
+ send(action, collection) {
227
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
195
228
  return;
196
- this.ws.send(JSON.stringify({ action: 'unsubscribe', project_id: this.projectId, collection }));
229
+ if (this.anonymous) {
230
+ this.ws.send(JSON.stringify({ action, collection }));
231
+ }
232
+ else if (this.projectId) {
233
+ this.ws.send(JSON.stringify({ action, project_id: this.projectId, collection }));
234
+ }
197
235
  }
198
236
  /**
199
237
  * Reconnects with backoff, rather than every three seconds forever.
@@ -252,6 +290,42 @@ class KoolbaseRealtime {
252
290
  }, KoolbaseRealtime.idleGraceMs);
253
291
  this.idleTimer.unref?.();
254
292
  }
293
+ /**
294
+ * The signed-in user changed: signed in, signed out, or another user. The
295
+ * open connection was made for the previous session -- signed out it sees
296
+ * only public collections, signed in it carries that user's access -- so it
297
+ * is replaced by one for the new session, resubscribing every collection.
298
+ * The same user again (a token refresh) changes nothing, and with nothing
299
+ * open there is nothing to do: the next subscribe connects as whoever is
300
+ * signed in then.
301
+ */
302
+ sessionChanged() {
303
+ if (!this.ws && !this.connecting && !this.reconnectTimer)
304
+ return;
305
+ if (this.ws && !this.connecting && (this.getUserId?.() ?? null) === this.connectedAs)
306
+ return;
307
+ this.generation += 1;
308
+ if (this.reconnectTimer) {
309
+ clearTimeout(this.reconnectTimer);
310
+ this.reconnectTimer = null;
311
+ }
312
+ const ws = this.ws;
313
+ this.ws = null;
314
+ this.connecting = false;
315
+ this.anonymous = false;
316
+ this.projectId = null;
317
+ this.reconnectAttempts = 0;
318
+ if (ws) {
319
+ // Detached first: this close is ours, not a dropped connection.
320
+ ws.onopen = null;
321
+ ws.onmessage = null;
322
+ ws.onclose = null;
323
+ ws.onerror = null;
324
+ ws.close();
325
+ }
326
+ if (this.listeners.size > 0)
327
+ void this.connect();
328
+ }
255
329
  disconnect() {
256
330
  if (this.idleTimer) {
257
331
  clearTimeout(this.idleTimer);
@@ -11,8 +11,9 @@ export interface CollectionQuery {
11
11
  /**
12
12
  * Re-read page one, silently, when Koolbase realtime reports a record
13
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.
14
+ * read). Signed out, only a collection anyone can read (read rule
15
+ * "public") is live; any other behaves as a normal list until a user signs
16
+ * in, and goes live then.
16
17
  */
17
18
  live?: boolean;
18
19
  }
@@ -9,6 +9,9 @@ export declare class KoolbaseRealtime {
9
9
  private reconnectAttempts;
10
10
  private reconnectTimer;
11
11
  private connecting;
12
+ private anonymous;
13
+ private connectedAs;
14
+ private generation;
12
15
  private idleTimer;
13
16
  /** How long an unused connection stays open, in ms: a screen that comes
14
17
  * straight back (navigating back, React's development double start)
@@ -30,6 +33,9 @@ export declare class KoolbaseRealtime {
30
33
  private connect;
31
34
  private sendSubscribe;
32
35
  private sendUnsubscribe;
36
+ /** A signed-out visitor's message leaves project_id out: it has none to
37
+ * give, and the server uses the public key's project. */
38
+ private send;
33
39
  /**
34
40
  * Reconnects with backoff, rather than every three seconds forever.
35
41
  *
@@ -51,6 +57,16 @@ export declare class KoolbaseRealtime {
51
57
  * it: reconnecting needs a subscriber.
52
58
  */
53
59
  private scheduleIdleClose;
60
+ /**
61
+ * The signed-in user changed: signed in, signed out, or another user. The
62
+ * open connection was made for the previous session -- signed out it sees
63
+ * only public collections, signed in it carries that user's access -- so it
64
+ * is replaced by one for the new session, resubscribing every collection.
65
+ * The same user again (a token refresh) changes nothing, and with nothing
66
+ * open there is nothing to do: the next subscribe connects as whoever is
67
+ * signed in then.
68
+ */
69
+ sessionChanged(): void;
54
70
  disconnect(): void;
55
71
  }
56
72
  export {};
@@ -38,6 +38,15 @@ export class KoolbaseRealtime {
38
38
  this.reconnectAttempts = 0;
39
39
  this.reconnectTimer = null;
40
40
  this.connecting = false;
41
+ // The open connection is a signed-out visitor's (the public key): its
42
+ // messages leave project_id out.
43
+ this.anonymous = false;
44
+ // Whose session the open connection was made for: the user id, or null
45
+ // when signed out. sessionChanged compares against it.
46
+ this.connectedAs = null;
47
+ // Bumped by sessionChanged: a connect still waiting for its token gives up
48
+ // rather than open a connection for the old session.
49
+ this.generation = 0;
41
50
  // Pending close of an unused connection (scheduleIdleClose); null while
42
51
  // anyone is subscribed.
43
52
  this.idleTimer = null;
@@ -98,28 +107,49 @@ export class KoolbaseRealtime {
98
107
  // double start -- found nobody connecting and opened its own socket; only
99
108
  // the last was remembered, and the others stayed open for good.
100
109
  this.connecting = true;
110
+ const generation = this.generation;
101
111
  let token;
102
112
  try {
103
113
  token = await this.getToken();
104
114
  }
105
115
  catch {
106
- token = null;
116
+ // A session that could not be refreshed is still a session: try again
117
+ // later, never as a signed-out visitor.
118
+ if (generation !== this.generation)
119
+ return;
120
+ this.connecting = false;
121
+ this.scheduleReconnect();
122
+ return;
107
123
  }
124
+ // The session changed while the token was on its way: sessionChanged
125
+ // has started the connection for the new one.
126
+ if (generation !== this.generation)
127
+ return;
108
128
  if (this.listeners.size === 0) {
109
129
  // Everyone left while the token was on its way: nothing to connect for.
110
130
  this.connecting = false;
111
131
  return;
112
132
  }
113
- if (!token) {
133
+ const userId = this.getUserId?.() ?? null;
134
+ if (!token && userId) {
135
+ // Signed in, but no usable token right now: try again later rather
136
+ // than drop to public-only.
114
137
  this.connecting = false;
115
- this.scheduleReconnect(); // sign-in may be in flight
138
+ this.scheduleReconnect();
116
139
  return;
117
140
  }
118
- this.projectId = projectIdFromToken(token);
141
+ // Signed out: the project's public key, and only collections anyone can
142
+ // read. The server pins the connection to the key's project.
143
+ this.anonymous = !token;
144
+ this.connectedAs = token ? userId : null;
145
+ this.projectId = token ? projectIdFromToken(token) : null;
146
+ const credential = token
147
+ ? `token=${encodeURIComponent(token)}`
148
+ : `public_key=${encodeURIComponent(this.config.publicKey)}`;
119
149
  const wsUrl = this.config.baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
120
150
  let ws;
121
151
  try {
122
- ws = new WebSocket(`${wsUrl}/v1/realtime/ws?token=${encodeURIComponent(token)}`);
152
+ ws = new WebSocket(`${wsUrl}/v1/realtime/ws?${credential}`);
123
153
  }
124
154
  catch {
125
155
  this.connecting = false;
@@ -183,14 +213,22 @@ export class KoolbaseRealtime {
183
213
  ws.onerror = () => { };
184
214
  }
185
215
  sendSubscribe(collection) {
186
- if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
187
- return;
188
- this.ws.send(JSON.stringify({ action: 'subscribe', project_id: this.projectId, collection }));
216
+ this.send('subscribe', collection);
189
217
  }
190
218
  sendUnsubscribe(collection) {
191
- if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
219
+ this.send('unsubscribe', collection);
220
+ }
221
+ /** A signed-out visitor's message leaves project_id out: it has none to
222
+ * give, and the server uses the public key's project. */
223
+ send(action, collection) {
224
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
192
225
  return;
193
- this.ws.send(JSON.stringify({ action: 'unsubscribe', project_id: this.projectId, collection }));
226
+ if (this.anonymous) {
227
+ this.ws.send(JSON.stringify({ action, collection }));
228
+ }
229
+ else if (this.projectId) {
230
+ this.ws.send(JSON.stringify({ action, project_id: this.projectId, collection }));
231
+ }
194
232
  }
195
233
  /**
196
234
  * Reconnects with backoff, rather than every three seconds forever.
@@ -249,6 +287,42 @@ export class KoolbaseRealtime {
249
287
  }, KoolbaseRealtime.idleGraceMs);
250
288
  this.idleTimer.unref?.();
251
289
  }
290
+ /**
291
+ * The signed-in user changed: signed in, signed out, or another user. The
292
+ * open connection was made for the previous session -- signed out it sees
293
+ * only public collections, signed in it carries that user's access -- so it
294
+ * is replaced by one for the new session, resubscribing every collection.
295
+ * The same user again (a token refresh) changes nothing, and with nothing
296
+ * open there is nothing to do: the next subscribe connects as whoever is
297
+ * signed in then.
298
+ */
299
+ sessionChanged() {
300
+ if (!this.ws && !this.connecting && !this.reconnectTimer)
301
+ return;
302
+ if (this.ws && !this.connecting && (this.getUserId?.() ?? null) === this.connectedAs)
303
+ return;
304
+ this.generation += 1;
305
+ if (this.reconnectTimer) {
306
+ clearTimeout(this.reconnectTimer);
307
+ this.reconnectTimer = null;
308
+ }
309
+ const ws = this.ws;
310
+ this.ws = null;
311
+ this.connecting = false;
312
+ this.anonymous = false;
313
+ this.projectId = null;
314
+ this.reconnectAttempts = 0;
315
+ if (ws) {
316
+ // Detached first: this close is ours, not a dropped connection.
317
+ ws.onopen = null;
318
+ ws.onmessage = null;
319
+ ws.onclose = null;
320
+ ws.onerror = null;
321
+ ws.close();
322
+ }
323
+ if (this.listeners.size > 0)
324
+ void this.connect();
325
+ }
252
326
  disconnect() {
253
327
  if (this.idleTimer) {
254
328
  clearTimeout(this.idleTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koolbase/core",
3
- "version": "12.12.0",
3
+ "version": "12.13.0",
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",