@techfinityedge/koolbase-react-native 4.0.0 → 4.2.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.
package/README.md CHANGED
@@ -297,15 +297,27 @@ await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
297
297
 
298
298
  ## Realtime
299
299
 
300
- ```typescript
300
+ Subscribe to live changes on a collection. Realtime uses the signed-in user's
301
+ session, so subscribe after login. It streams `created` and `updated` events for
302
+ collections whose read rule is `public` or `authenticated`.
303
+
304
+ ```ts
305
+ import { Koolbase } from '@techfinityedge/koolbase-react-native';
306
+
301
307
  const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
302
- if (event.type === 'created') setMessages(prev => [event.record, ...prev]);
308
+ // event.type -> 'created' | 'updated'
309
+ // event.collection -> 'messages'
310
+ // event.record -> KoolbaseRecord
311
+ console.log(event.type, event.record.data);
303
312
  });
304
313
 
305
- // Cleanup
306
- unsubscribe();
314
+ unsubscribe(); // stop listening
307
315
  ```
308
316
 
317
+ The socket opens lazily on first `subscribe`, is shared across all subscriptions,
318
+ and reconnects automatically. The project is taken from the user's session — you
319
+ don't pass it.
320
+
309
321
  ---
310
322
 
311
323
  ## Functions
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ exports.Koolbase = {
69
69
  _auth = new auth_1.KoolbaseAuth(config);
70
70
  _db = new database_1.KoolbaseDatabase(config, () => _auth?.currentUser?.id ?? null, () => _auth?.validAccessToken() ?? Promise.resolve(null));
71
71
  _storage = new storage_1.KoolbaseStorage(config, () => _auth?.validAccessToken() ?? Promise.resolve(null));
72
- _realtime = new realtime_1.KoolbaseRealtime(config);
72
+ _realtime = new realtime_1.KoolbaseRealtime(config, () => _auth?.validAccessToken() ?? Promise.resolve(null));
73
73
  _functions = new functions_1.KoolbaseFunctions(config, () => _auth?.validAccessToken() ?? Promise.resolve(null));
74
74
  _flags = new flags_1.KoolbaseFlags(config, 'rn-device');
75
75
  _codePush = new code_push_1.KoolbaseCodePush(config, config.codePushChannel ?? 'stable');
@@ -1,11 +1,19 @@
1
1
  import { KoolbaseConfig, RealtimeCallback } from './types';
2
+ type TokenProvider = () => Promise<string | null>;
2
3
  export declare class KoolbaseRealtime {
3
4
  private config;
5
+ private getToken;
4
6
  private ws;
7
+ private projectId;
5
8
  private listeners;
6
9
  private reconnectTimer;
7
- constructor(config: KoolbaseConfig);
10
+ private connecting;
11
+ constructor(config: KoolbaseConfig, getToken: TokenProvider);
8
12
  subscribe(collection: string, callback: RealtimeCallback): () => void;
9
13
  private connect;
14
+ private sendSubscribe;
15
+ private sendUnsubscribe;
16
+ private scheduleReconnect;
10
17
  disconnect(): void;
11
18
  }
19
+ export {};
package/dist/realtime.js CHANGED
@@ -2,58 +2,141 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KoolbaseRealtime = void 0;
4
4
  const record_1 = require("./record");
5
+ const EVENT_TYPE_MAP = {
6
+ 'db.record.created': 'created',
7
+ 'db.record.updated': 'updated',
8
+ 'db.record.deleted': 'deleted',
9
+ };
10
+ function projectIdFromToken(token) {
11
+ try {
12
+ const part = token.split('.')[1];
13
+ if (!part)
14
+ return null;
15
+ const b64 = part.replace(/-/g, '+').replace(/_/g, '/');
16
+ const g = globalThis;
17
+ let json;
18
+ if (typeof g.atob === 'function') {
19
+ const bin = g.atob(b64);
20
+ json = decodeURIComponent(bin.split('').map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0')).join(''));
21
+ }
22
+ else if (g.Buffer) {
23
+ json = g.Buffer.from(b64, 'base64').toString('utf8');
24
+ }
25
+ else {
26
+ return null;
27
+ }
28
+ return JSON.parse(json).project_id ?? null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
5
34
  class KoolbaseRealtime {
6
- constructor(config) {
35
+ constructor(config, getToken) {
7
36
  this.ws = null;
37
+ this.projectId = null;
8
38
  this.listeners = new Map();
9
39
  this.reconnectTimer = null;
40
+ this.connecting = false;
10
41
  this.config = config;
42
+ this.getToken = getToken;
11
43
  }
12
44
  subscribe(collection, callback) {
13
- if (!this.listeners.has(collection)) {
45
+ if (!this.listeners.has(collection))
14
46
  this.listeners.set(collection, []);
15
- }
16
47
  this.listeners.get(collection).push(callback);
17
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
18
- this.connect();
48
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
49
+ this.sendSubscribe(collection);
50
+ }
51
+ else {
52
+ void this.connect();
19
53
  }
20
- // Return unsubscribe function
21
54
  return () => {
22
55
  const callbacks = this.listeners.get(collection) ?? [];
23
- const index = callbacks.indexOf(callback);
24
- if (index > -1)
25
- callbacks.splice(index, 1);
56
+ const i = callbacks.indexOf(callback);
57
+ if (i > -1)
58
+ callbacks.splice(i, 1);
59
+ if (callbacks.length === 0) {
60
+ this.listeners.delete(collection);
61
+ this.sendUnsubscribe(collection);
62
+ }
26
63
  };
27
64
  }
28
- connect() {
29
- const wsUrl = this.config.baseUrl
30
- .replace('https://', 'wss://')
31
- .replace('http://', 'ws://');
32
- this.ws = new WebSocket(`${wsUrl}/v1/sdk/realtime?key=${this.config.publicKey}`);
33
- this.ws.onmessage = (event) => {
65
+ async connect() {
66
+ if (this.connecting)
67
+ return;
68
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING))
69
+ return;
70
+ const token = await this.getToken();
71
+ if (!token) {
72
+ this.scheduleReconnect(); // sign-in may be in flight
73
+ return;
74
+ }
75
+ this.projectId = projectIdFromToken(token);
76
+ this.connecting = true;
77
+ const wsUrl = this.config.baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
78
+ const ws = new WebSocket(`${wsUrl}/v1/realtime/ws?token=${encodeURIComponent(token)}`);
79
+ this.ws = ws;
80
+ ws.onopen = () => {
81
+ this.connecting = false;
82
+ for (const collection of this.listeners.keys())
83
+ this.sendSubscribe(collection); // (re)subscribe all
84
+ };
85
+ ws.onmessage = (event) => {
86
+ let raw;
34
87
  try {
35
- const raw = JSON.parse(event.data);
36
- if (!raw || !raw.record)
37
- return;
38
- const msg = {
39
- type: raw.type,
40
- collection: raw.collection,
41
- record: (0, record_1.recordFromWire)(raw.record),
42
- };
43
- const callbacks = this.listeners.get(msg.collection) ?? [];
44
- callbacks.forEach((cb) => cb(msg));
88
+ raw = JSON.parse(event.data);
89
+ }
90
+ catch {
91
+ return;
45
92
  }
46
- catch (_) { }
93
+ const mapped = EVENT_TYPE_MAP[raw?.type];
94
+ if (!mapped)
95
+ return; // ignore subscribed / unsubscribed / error / unknown
96
+ const payload = raw.payload;
97
+ if (!payload || !payload.collection || !payload.record)
98
+ return; // created/updated carry a record
99
+ const msg = {
100
+ type: mapped,
101
+ collection: payload.collection,
102
+ record: (0, record_1.recordFromWire)(payload.record),
103
+ };
104
+ (this.listeners.get(payload.collection) ?? []).forEach((cb) => cb(msg));
47
105
  };
48
- this.ws.onclose = () => {
49
- this.reconnectTimer = setTimeout(() => this.connect(), 3000);
106
+ ws.onclose = () => {
107
+ this.connecting = false;
108
+ if (this.ws === ws)
109
+ this.ws = null;
110
+ this.scheduleReconnect();
50
111
  };
112
+ ws.onerror = () => { };
113
+ }
114
+ sendSubscribe(collection) {
115
+ if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
116
+ return;
117
+ this.ws.send(JSON.stringify({ action: 'subscribe', project_id: this.projectId, collection }));
118
+ }
119
+ sendUnsubscribe(collection) {
120
+ if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
121
+ return;
122
+ this.ws.send(JSON.stringify({ action: 'unsubscribe', project_id: this.projectId, collection }));
123
+ }
124
+ scheduleReconnect() {
125
+ if (this.listeners.size === 0 || this.reconnectTimer)
126
+ return;
127
+ this.reconnectTimer = setTimeout(() => {
128
+ this.reconnectTimer = null;
129
+ void this.connect();
130
+ }, 3000);
51
131
  }
52
132
  disconnect() {
53
- if (this.reconnectTimer)
133
+ if (this.reconnectTimer) {
54
134
  clearTimeout(this.reconnectTimer);
135
+ this.reconnectTimer = null;
136
+ }
55
137
  this.ws?.close();
56
138
  this.ws = null;
139
+ this.projectId = null;
57
140
  this.listeners.clear();
58
141
  }
59
142
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "4.0.0",
4
- "description": "React Native SDK for Koolbase \u2014 auth, database, storage, realtime, feature flags, and functions in one package.",
3
+ "version": "4.2.0",
4
+ "description": "React Native SDK for Koolbase auth, database, storage, realtime, feature flags, and functions in one package.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [