@giaeulate/baas-sdk 1.0.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.
@@ -0,0 +1,157 @@
1
+ import { RealtimeAction, RealtimeCallback, RealtimePayload } from './types';
2
+
3
+ /**
4
+ * RealtimeService handles WebSocket connections to the BaaS Realtime engine.
5
+ * It allows subscribing to database changes in real-time.
6
+ */
7
+ export class RealtimeService {
8
+ private socket: WebSocket | null = null;
9
+ private subscribers: Map<string, Set<{ action: RealtimeAction; callback: RealtimeCallback }>> = new Map();
10
+ private reconnectAttempts = 0;
11
+ private maxReconnectAttempts = 5;
12
+ private reconnectInterval = 3000;
13
+
14
+ constructor(
15
+ private url: string,
16
+ private projectId: string | null,
17
+ private tokenProvider: () => string | null,
18
+ private wsConstructor: any = typeof WebSocket !== 'undefined' ? WebSocket : null
19
+ ) {}
20
+
21
+ /**
22
+ * Initialize the WebSocket connection
23
+ */
24
+ private connect(): Promise<void> {
25
+ if (this.socket?.readyState === WebSocket.OPEN) return Promise.resolve();
26
+
27
+ return new Promise((resolve, reject) => {
28
+ const token = this.tokenProvider();
29
+ if (!this.projectId || !token) {
30
+ reject(new Error('Missing project ID or authentication token for Realtime connection'));
31
+ return;
32
+ }
33
+
34
+ // Convert http/https to ws/wss
35
+ const wsUrl = this.url.replace(/^http/, 'ws') + '/ws';
36
+ const fullUrl = `${wsUrl}?project_id=${this.projectId}&token=${token}`;
37
+
38
+ if (!this.wsConstructor) {
39
+ reject(new Error('WebSocket constructor not provided or available in this environment'));
40
+ return;
41
+ }
42
+
43
+ const socket = new this.wsConstructor(fullUrl);
44
+ this.socket = socket;
45
+
46
+ socket.onopen = () => {
47
+ console.log('BaaS Realtime: Connected');
48
+ this.reconnectAttempts = 0;
49
+ resolve();
50
+ };
51
+
52
+ socket.onmessage = (event: any) => {
53
+ try {
54
+ const payload: RealtimePayload = JSON.parse(event.data);
55
+ this.handleEvent(payload);
56
+ } catch (err) {
57
+ console.error('BaaS Realtime: Failed to parse message', err);
58
+ }
59
+ };
60
+
61
+ socket.onerror = (error: any) => {
62
+ console.error('BaaS Realtime: WebSocket Error', error);
63
+ reject(error);
64
+ };
65
+
66
+ socket.onclose = () => {
67
+ console.log('BaaS Realtime: Disconnected');
68
+ this.socket = null;
69
+ this.attemptReconnect();
70
+ };
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Subscribe to changes on a specific table
76
+ */
77
+ public async subscribe<T = any>(
78
+ table: string,
79
+ action: RealtimeAction,
80
+ callback: RealtimeCallback<T>
81
+ ): Promise<{ unsubscribe: () => void }> {
82
+ await this.connect();
83
+
84
+ if (!this.subscribers.has(table)) {
85
+ this.subscribers.set(table, new Set());
86
+ }
87
+
88
+ const sub = { action, callback };
89
+ this.subscribers.get(table)!.add(sub);
90
+
91
+ return {
92
+ unsubscribe: () => {
93
+ const tableSubs = this.subscribers.get(table);
94
+ if (tableSubs) {
95
+ tableSubs.delete(sub);
96
+ if (tableSubs.size === 0) {
97
+ this.subscribers.delete(table);
98
+ }
99
+ }
100
+ }
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Handle incoming CDC events from the server
106
+ */
107
+ private handleEvent(payload: RealtimePayload) {
108
+ // Notify specific table subscribers
109
+ const tableSubs = this.subscribers.get(payload.table);
110
+ if (tableSubs) {
111
+ for (const sub of tableSubs) {
112
+ if (sub.action === '*' || sub.action.toLowerCase() === payload.action.toLowerCase()) {
113
+ sub.callback(payload);
114
+ }
115
+ }
116
+ }
117
+
118
+ // Notify wildcard ('*') subscribers
119
+ const wildcardSubs = this.subscribers.get('*');
120
+ if (wildcardSubs) {
121
+ for (const sub of wildcardSubs) {
122
+ if (sub.action === '*' || sub.action.toLowerCase() === payload.action.toLowerCase()) {
123
+ sub.callback(payload);
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Attempt to reconnect on connection loss
131
+ */
132
+ private attemptReconnect() {
133
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
134
+ this.reconnectAttempts++;
135
+ console.log(`BaaS Realtime: Attempting reconnect ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`);
136
+ setTimeout(() => this.connect(), this.reconnectInterval);
137
+ } else {
138
+ console.error('BaaS Realtime: Max reconnect attempts reached');
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Close the connection
144
+ */
145
+ public disconnect() {
146
+ if (this.socket) {
147
+ this.socket.onclose = null; // Prevent reconnect
148
+ this.socket.close();
149
+ this.socket = null;
150
+ }
151
+ }
152
+
153
+ /** @internal - For testing only */
154
+ public get _socket(): WebSocket | null {
155
+ return this.socket;
156
+ }
157
+ }
package/src/types.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * SDK Shared Types
3
+ */
4
+
5
+ /** HTTP method types */
6
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
7
+
8
+ /** Request options for the HTTP client */
9
+ export interface RequestOptions {
10
+ method?: HttpMethod;
11
+ body?: unknown;
12
+ headers?: Record<string, string>;
13
+ skipAuth?: boolean;
14
+ }
15
+
16
+ /** Query filter for data operations */
17
+ export type QueryFilter = {
18
+ column: string;
19
+ operator: string;
20
+ value: any;
21
+ };
22
+
23
+ /** Pagination options */
24
+ export interface PaginationOptions {
25
+ limit?: number;
26
+ offset?: number;
27
+ }
28
+
29
+ /** Generic API response with error */
30
+ export interface ApiResponse<T = any> {
31
+ data?: T;
32
+ error?: string;
33
+ success?: boolean;
34
+ }
35
+
36
+ /** Realtime Event types */
37
+ export type RealtimeAction = 'INSERT' | 'UPDATE' | 'DELETE' | '*';
38
+
39
+ /** Structure of a Realtime event payload */
40
+ export interface RealtimePayload<T = any> {
41
+ table: string;
42
+ action: 'insert' | 'update' | 'delete';
43
+ record: T;
44
+ old_record?: T;
45
+ timestamp: string;
46
+ }
47
+
48
+ /** Callback function for realtime subscriptions */
49
+ export type RealtimeCallback<T = any> = (payload: RealtimePayload<T>) => void;
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "dist",
11
+ "esModuleInterop": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "skipLibCheck": true,
14
+ "isolatedModules": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
@@ -0,0 +1,14 @@
1
+ /// <reference types="vitest" />
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ environment: 'jsdom',
7
+ globals: true,
8
+ setupFiles: [],
9
+ coverage: {
10
+ provider: 'v8',
11
+ reporter: ['text', 'json', 'html'],
12
+ },
13
+ },
14
+ });