@techfinityedge/koolbase-react-native 4.0.0 → 4.2.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/README.md CHANGED
@@ -297,15 +297,26 @@ 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. Uses the signed-in user's session, so
301
+ subscribe after login. Streams `created`, `updated`, and `deleted` events for
302
+ collections whose read rule is `public` or `authenticated`.
303
+
304
+ ```ts
301
305
  const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
302
- if (event.type === 'created') setMessages(prev => [event.record, ...prev]);
306
+ // event.type -> 'created' | 'updated' | 'deleted'
307
+ if (event.type === 'deleted') {
308
+ console.log('deleted', event.recordId); // recordId on deletes
309
+ } else {
310
+ console.log(event.type, event.record!.data); // record on created/updated
311
+ }
303
312
  });
304
313
 
305
- // Cleanup
306
314
  unsubscribe();
307
315
  ```
308
316
 
317
+ The socket opens lazily, is shared, and reconnects automatically. The project is
318
+ taken from the user's session..
319
+
309
320
  ---
310
321
 
311
322
  ## 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,146 @@
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;
92
+ }
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)
98
+ return;
99
+ let msg;
100
+ if (mapped === 'deleted') {
101
+ msg = { type: 'deleted', collection: payload.collection, recordId: payload.record_id };
102
+ }
103
+ else if (payload.record) {
104
+ msg = { type: mapped, collection: payload.collection, record: (0, record_1.recordFromWire)(payload.record) };
105
+ }
106
+ else {
107
+ return;
45
108
  }
46
- catch (_) { }
109
+ (this.listeners.get(payload.collection) ?? []).forEach((cb) => cb(msg));
47
110
  };
48
- this.ws.onclose = () => {
49
- this.reconnectTimer = setTimeout(() => this.connect(), 3000);
111
+ ws.onclose = () => {
112
+ this.connecting = false;
113
+ if (this.ws === ws)
114
+ this.ws = null;
115
+ this.scheduleReconnect();
50
116
  };
117
+ ws.onerror = () => { };
118
+ }
119
+ sendSubscribe(collection) {
120
+ if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
121
+ return;
122
+ this.ws.send(JSON.stringify({ action: 'subscribe', project_id: this.projectId, collection }));
123
+ }
124
+ sendUnsubscribe(collection) {
125
+ if (!this.projectId || !this.ws || this.ws.readyState !== WebSocket.OPEN)
126
+ return;
127
+ this.ws.send(JSON.stringify({ action: 'unsubscribe', project_id: this.projectId, collection }));
128
+ }
129
+ scheduleReconnect() {
130
+ if (this.listeners.size === 0 || this.reconnectTimer)
131
+ return;
132
+ this.reconnectTimer = setTimeout(() => {
133
+ this.reconnectTimer = null;
134
+ void this.connect();
135
+ }, 3000);
51
136
  }
52
137
  disconnect() {
53
- if (this.reconnectTimer)
138
+ if (this.reconnectTimer) {
54
139
  clearTimeout(this.reconnectTimer);
140
+ this.reconnectTimer = null;
141
+ }
55
142
  this.ws?.close();
56
143
  this.ws = null;
144
+ this.projectId = null;
57
145
  this.listeners.clear();
58
146
  }
59
147
  }
package/dist/types.d.ts CHANGED
@@ -160,7 +160,8 @@ export interface UploadOptions {
160
160
  export interface RealtimeEvent {
161
161
  type: 'created' | 'updated' | 'deleted';
162
162
  collection: string;
163
- record: KoolbaseRecord;
163
+ record?: KoolbaseRecord;
164
+ recordId?: string;
164
165
  }
165
166
  export type RealtimeCallback = (event: RealtimeEvent) => void;
166
167
  export interface BootstrapPayload {
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.1",
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": [