pulse-rn 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.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # pulse-rn
2
+
3
+ ![NPM Version](https://img.shields.io/npm/v/pulse-rn)
4
+ ![License](https://img.shields.io/npm/l/pulse-rn)
5
+
6
+ A lightweight offline-first synchronization engine for React Native/Expo applications. Provides real-time data synchronization between local SQLite database and remote server through WebSocket connections with conflict resolution and delta sync capabilities. It needs a backend server with the django-pulse library to work with. If this is library is well received, I will add more features to it and others backends support.
7
+
8
+ ## Features
9
+
10
+ - **Offline-first architecture**: Local SQLite database with automatic sync when online
11
+ - **Real-time synchronization**: WebSocket-based bidirectional sync
12
+ - **Conflict resolution**: Version-based conflict handling with server authority
13
+ - **Delta sync**: Only sync changes since last sync to minimize bandwidth
14
+ - **Batch operations**: Efficient batching of local changes
15
+ - **TypeScript support**: Full TypeScript implementation with type safety
16
+ - **React Query integration**: Seamless integration with TanStack Query
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install pulse-rn expo-sqlite expo-crypto @tanstack/react-query
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ ### PulseProvider Setup
27
+
28
+ ```tsx
29
+ import { PulseProvider } from 'pulse-rn';
30
+
31
+ function App() {
32
+ return (
33
+ <PulseProvider
34
+ config={{
35
+ baseUrl: 'localhost:8000',
36
+ userId: 1,
37
+ tablesConfig: {
38
+ items: ['id', 'name', 'description', 'version', 'is_deleted'],
39
+ users: ['id', 'name', 'email', 'version', 'is_deleted']
40
+ }
41
+ }}
42
+ >
43
+ {/* Your app components */}
44
+ </PulseProvider>
45
+ );
46
+ }
47
+ ```
48
+
49
+ #### Configuration Parameters
50
+
51
+ | Parameter | Type | Description |
52
+ |-----------|------|-------------|
53
+ | `baseUrl` | string | WebSocket server base URL |
54
+ | `userId` | number | Current user identifier |
55
+ | `tablesConfig` | Record<string, string[]> | Table schema definitions |
56
+
57
+ ## API Reference
58
+
59
+ ### Hooks
60
+
61
+ #### useSyncTable
62
+
63
+ React hook for querying synchronized table data.
64
+
65
+ ```tsx
66
+ import { useSyncTable } from 'pulse-rn';
67
+
68
+ function ItemsList() {
69
+ const { data: items, isLoading, error } = useSyncTable('items');
70
+
71
+ if (isLoading) return <Text>Loading...</Text>;
72
+ if (error) return <Text>Error: {error.message}</Text>;
73
+
74
+ return (
75
+ <FlatList
76
+ data={items}
77
+ renderItem={({ item }) => <Text>{item.name}</Text>}
78
+ />
79
+ );
80
+ }
81
+ ```
82
+
83
+ | Parameter | Type | Description |
84
+ |-----------|------|-------------|
85
+ | `tableName` | string | Name of the table to query |
86
+
87
+ **Returns**: Query object with `data`, `isLoading`, `error` properties filtered by `is_deleted = 0`
88
+
89
+ ### Functions
90
+
91
+ #### createItem
92
+
93
+ Creates a new record with automatic sync ID generation.
94
+
95
+ ```tsx
96
+ import { createItem } from 'pulse-rn';
97
+ import { useQueryClient } from '@tanstack/react-query';
98
+
99
+ function CreateItemForm() {
100
+ const queryClient = useQueryClient();
101
+
102
+ const handleCreate = () => {
103
+ createItem('items', {
104
+ name: 'New Item',
105
+ description: 'Item description'
106
+ }, queryClient);
107
+ };
108
+
109
+ return <Button title="Create Item" onPress={handleCreate} />;
110
+ }
111
+ ```
112
+
113
+ | Parameter | Type | Description |
114
+ |-----------|------|-------------|
115
+ | `tableName` | string | Target table name |
116
+ | `data` | object | Record data to insert |
117
+ | `queryClient` | QueryClient | TanStack Query client instance |
118
+
119
+ **Behavior**: Generates UUID `sync_id`, sets `version = 0`, marks `is_local_only = 1`
120
+
121
+ #### updateItem
122
+
123
+ Updates an existing record and queues for synchronization.
124
+
125
+ ```tsx
126
+ import { updateItem } from 'pulse-rn';
127
+
128
+ function UpdateItem({ item }) {
129
+ const queryClient = useQueryClient();
130
+
131
+ const handleUpdate = () => {
132
+ updateItem('items', item.sync_id, {
133
+ name: 'Updated Name',
134
+ description: 'Updated description'
135
+ }, queryClient);
136
+ };
137
+
138
+ return <Button title="Update" onPress={handleUpdate} />;
139
+ }
140
+ ```
141
+
142
+ | Parameter | Type | Description |
143
+ |-----------|------|-------------|
144
+ | `tableName` | string | Target table name |
145
+ | `sync_id` | string | Record synchronization identifier |
146
+ | `data` | object | Fields to update |
147
+ | `queryClient` | QueryClient | TanStack Query client instance |
148
+
149
+ **Behavior**: Updates specific fields, marks `is_local_only = 1`, resets `sync_error`
150
+
151
+ #### deleteItem
152
+
153
+ Performs logical deletion of a record.
154
+
155
+ ```tsx
156
+ import { deleteItem } from 'pulse-rn';
157
+
158
+ function DeleteItem({ item }) {
159
+ const queryClient = useQueryClient();
160
+
161
+ const handleDelete = () => {
162
+ deleteItem('items', item.sync_id, queryClient);
163
+ };
164
+
165
+ return <Button title="Delete" onPress={handleDelete} />;
166
+ }
167
+ ```
168
+
169
+ | Parameter | Type | Description |
170
+ |-----------|------|-------------|
171
+ | `tableName` | string | Target table name |
172
+ | `sync_id` | string | Record synchronization identifier |
173
+ | `queryClient` | QueryClient | TanStack Query client instance |
174
+
175
+ **Behavior**: Sets `is_deleted = 1` (logical deletion)
176
+
177
+ ## Synchronization Logic
178
+
179
+ ### Batching System
180
+
181
+ Local changes are batched and sent to the server every 2 seconds to optimize network usage:
182
+
183
+ - Accumulates create/update/delete operations
184
+ - Sends batches of up to 50 records per request
185
+ - Automatic retry on connection failure
186
+
187
+ ### Delta Sync
188
+
189
+ When connecting to the server, pulse-rn requests only changes since the last synchronization:
190
+
191
+ ```typescript
192
+ {
193
+ type: 'SYNC_REQUEST_DELTA',
194
+ table: 'items',
195
+ last_version: 42
196
+ }
197
+ ```
198
+
199
+ - Tracks maximum local version per table
200
+ - Only requests records with version > max_local_version
201
+ - Minimizes bandwidth usage
202
+
203
+ ### Conflict Resolution
204
+
205
+ Version-based conflict resolution with server authority:
206
+
207
+ 1. Client operations increment version numbers
208
+ 2. Server validates and applies changes with final authority
209
+ 3. Conflicts resolved by server version precedence
210
+ 4. Failed operations marked with `sync_error` field
211
+
212
+ ### Connection Management
213
+
214
+ - Automatic reconnection with 3-second backoff
215
+ - WebSocket connection state management
216
+ - Graceful handling of network interruptions
217
+
218
+ ## WebSocket Protocol
219
+
220
+ ### Message Types
221
+
222
+ | Type | Description |
223
+ |-------|-------------|
224
+ | `SYNC_BATCH_UPLOAD` | Batch of local changes |
225
+ | `SYNC_REQUEST_DELTA` | Request changes since version |
226
+ | `BATCH_ACK` | Batch processing acknowledgment |
227
+ | `SYNC_ACK_INDIVIDUAL` | Individual operation result |
228
+ | `SYNC_UPDATE` | Real-time data updates |
229
+
230
+ ## Error Handling
231
+
232
+ - Network errors trigger automatic reconnection
233
+ - Failed operations stored with error messages
234
+ - Sync queue preserved across app restarts
235
+ - Graceful degradation when offline
@@ -0,0 +1,4 @@
1
+ export * from './src/database';
2
+ export * from './src/sync-engine';
3
+ export * from './src/operations';
4
+ export * from './src/context';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./src/database"), exports);
18
+ __exportStar(require("./src/sync-engine"), exports);
19
+ __exportStar(require("./src/operations"), exports);
20
+ __exportStar(require("./src/context"), exports);
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface PulseConfig {
3
+ baseUrl: string;
4
+ userId: number;
5
+ tablesConfig: Record<string, string[]>;
6
+ }
7
+ export declare const PulseProvider: ({ config, children }: {
8
+ config: PulseConfig;
9
+ children: React.ReactNode;
10
+ }) => React.JSX.Element;
11
+ export declare const usePulseConfig: () => PulseConfig;
12
+ export {};
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.usePulseConfig = exports.PulseProvider = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const database_1 = require("./database");
39
+ const sync_engine_1 = require("./sync-engine");
40
+ const PulseContext = (0, react_1.createContext)(null);
41
+ const PulseProvider = ({ config, children }) => {
42
+ (0, react_1.useEffect)(() => {
43
+ (0, database_1.initializeDB)(config.tablesConfig);
44
+ }, []);
45
+ const tableNames = Object.keys(config.tablesConfig);
46
+ (0, sync_engine_1.usePulseSync)(config.userId, config.baseUrl, tableNames);
47
+ return (<PulseContext.Provider value={config}>
48
+ {children}
49
+ </PulseContext.Provider>);
50
+ };
51
+ exports.PulseProvider = PulseProvider;
52
+ const usePulseConfig = () => {
53
+ const context = (0, react_1.useContext)(PulseContext);
54
+ if (!context) {
55
+ throw new Error("usePulseConfig must be used within a PulseProvider");
56
+ }
57
+ return context;
58
+ };
59
+ exports.usePulseConfig = usePulseConfig;
@@ -0,0 +1,3 @@
1
+ import * as SQLite from 'expo-sqlite';
2
+ export declare const db: SQLite.SQLiteDatabase;
3
+ export declare const initializeDB: (schema: Record<string, string[]>) => void;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.initializeDB = exports.db = void 0;
37
+ const SQLite = __importStar(require("expo-sqlite"));
38
+ exports.db = SQLite.openDatabaseSync('djangopulse_v2.db');
39
+ const initializeDB = (schema) => {
40
+ Object.entries(schema).forEach(([tableName, columns]) => {
41
+ const baseFields = ['user_id'];
42
+ const allExpectedColumns = [...baseFields, ...columns];
43
+ exports.db.execSync(`
44
+ CREATE TABLE IF NOT EXISTS ${tableName} (
45
+ sync_id TEXT PRIMARY KEY NOT NULL,
46
+ version INTEGER DEFAULT 0,
47
+ is_local_only INTEGER DEFAULT 0,
48
+ is_deleted INTEGER DEFAULT 0,
49
+ sync_error TEXT DEFAULT NULL
50
+ );
51
+ `);
52
+ const tableInfo = exports.db.getAllSync(`PRAGMA table_info(${tableName})`);
53
+ const existingColumns = tableInfo.map((col) => col.name);
54
+ allExpectedColumns.forEach(colName => {
55
+ if (!existingColumns.includes(colName)) {
56
+ try {
57
+ exports.db.execSync(`ALTER TABLE ${tableName} ADD COLUMN ${colName} TEXT;`);
58
+ }
59
+ catch (e) {
60
+ console.error(`Error migrating ${colName}:`, e);
61
+ }
62
+ }
63
+ });
64
+ });
65
+ };
66
+ exports.initializeDB = initializeDB;
@@ -0,0 +1,3 @@
1
+ export declare const useSyncTable: (tableName: string) => import("@tanstack/react-query").UseQueryResult<any[], Error>;
2
+ export declare const createItem: (tableName: string, data: any, queryClient: any) => void;
3
+ export declare const updateItem: (tableName: string, sync_id: string, data: any, queryClient: any) => void;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.updateItem = exports.createItem = exports.useSyncTable = void 0;
37
+ const react_query_1 = require("@tanstack/react-query");
38
+ const Crypto = __importStar(require("expo-crypto"));
39
+ const sync_engine_1 = require("./sync-engine");
40
+ const database_1 = require("./database");
41
+ let syncBuffer = {};
42
+ let syncTimeout = null;
43
+ const sendBuffer = (tableName, socket) => {
44
+ const payloads = syncBuffer[tableName];
45
+ if (!payloads || payloads.length === 0)
46
+ return;
47
+ socket.send(JSON.stringify({ type: 'SYNC_BATCH_UPLOAD', table: tableName, payloads }));
48
+ syncBuffer[tableName] = [];
49
+ };
50
+ const useSyncTable = (tableName) => {
51
+ return (0, react_query_1.useQuery)({
52
+ queryKey: [tableName],
53
+ queryFn: async () => database_1.db.getAllSync(`SELECT * FROM ${tableName} WHERE is_deleted = 0`),
54
+ });
55
+ };
56
+ exports.useSyncTable = useSyncTable;
57
+ const createItem = (tableName, data, queryClient) => {
58
+ const sync_id = Crypto.randomUUID();
59
+ const keys = Object.keys(data);
60
+ const columns = ['sync_id', 'version', 'is_local_only', 'is_deleted', ...keys].join(', ');
61
+ const placeholders = new Array(keys.length + 4).fill('?').join(', ');
62
+ const values = [sync_id, 0, 1, 0, ...Object.values(data)];
63
+ database_1.db.runSync(`INSERT INTO ${tableName} (${columns}) VALUES (${placeholders})`, values);
64
+ queryClient.invalidateQueries({ queryKey: [tableName] });
65
+ const socket = (0, sync_engine_1.getConnectedSocket)();
66
+ if (socket) {
67
+ if (!syncBuffer[tableName])
68
+ syncBuffer[tableName] = [];
69
+ syncBuffer[tableName].push({ sync_id, ...data, is_local_only: 1, is_deleted: 0 });
70
+ if (syncTimeout)
71
+ clearTimeout(syncTimeout);
72
+ syncTimeout = setTimeout(() => sendBuffer(tableName, socket), 2000);
73
+ }
74
+ };
75
+ exports.createItem = createItem;
76
+ const updateItem = (tableName, sync_id, data, queryClient) => {
77
+ const keys = Object.keys(data);
78
+ const setClause = keys.map(key => `${key} = ?`).join(', ');
79
+ const values = [...Object.values(data), sync_id];
80
+ database_1.db.runSync(`UPDATE ${tableName} SET ${setClause}, is_local_only = 1, sync_error = NULL WHERE sync_id = ?`, values);
81
+ queryClient.invalidateQueries({ queryKey: [tableName] });
82
+ const socket = (0, sync_engine_1.getConnectedSocket)();
83
+ if (socket) {
84
+ if (!syncBuffer[tableName])
85
+ syncBuffer[tableName] = [];
86
+ syncBuffer[tableName].push({ sync_id, ...data, is_local_only: 1 });
87
+ if (syncTimeout)
88
+ clearTimeout(syncTimeout);
89
+ syncTimeout = setTimeout(() => sendBuffer(tableName, socket), 2000);
90
+ }
91
+ };
92
+ exports.updateItem = updateItem;
@@ -0,0 +1,2 @@
1
+ export declare const getConnectedSocket: () => WebSocket | null;
2
+ export declare const usePulseSync: (userId: number, baseUrl: string, tables: string[]) => void;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePulseSync = exports.getConnectedSocket = void 0;
4
+ const react_1 = require("react");
5
+ const react_query_1 = require("@tanstack/react-query");
6
+ const database_1 = require("./database");
7
+ const CHUNK_SIZE = 50;
8
+ let activeSocket = null;
9
+ const getConnectedSocket = () => {
10
+ if (activeSocket && activeSocket.readyState === WebSocket.OPEN)
11
+ return activeSocket;
12
+ return null;
13
+ };
14
+ exports.getConnectedSocket = getConnectedSocket;
15
+ const handleGenericIncomingData = async (tableName, payload, queryClient) => {
16
+ const { sync_id, version, data } = payload;
17
+ const { id, ...cleanData } = data;
18
+ try {
19
+ const tableInfo = database_1.db.getAllSync(`PRAGMA table_info(${tableName})`);
20
+ const existingColumns = tableInfo.map((col) => col.name);
21
+ const finalData = {};
22
+ Object.keys(cleanData).forEach(key => {
23
+ if (existingColumns.includes(key))
24
+ finalData[key] = cleanData[key];
25
+ });
26
+ if (cleanData.is_deleted === 1 || cleanData.is_deleted === true) {
27
+ database_1.db.runSync(`UPDATE ${tableName} SET is_deleted = 1, version = ? WHERE sync_id = ?`, [version, sync_id]);
28
+ queryClient.invalidateQueries({ queryKey: [tableName] });
29
+ return;
30
+ }
31
+ const columns = Object.keys(finalData);
32
+ if (columns.length === 0)
33
+ return;
34
+ const placeholders = columns.map(() => '?').join(', ');
35
+ const values = columns.map(col => finalData[col]);
36
+ database_1.db.runSync(`INSERT OR REPLACE INTO ${tableName} (sync_id, version, is_local_only, sync_error, ${columns.join(', ')})
37
+ VALUES (?, ?, 0, NULL, ${placeholders})`, [sync_id, version, ...values]);
38
+ queryClient.invalidateQueries({ queryKey: [tableName] });
39
+ }
40
+ catch (error) {
41
+ console.error("SQLite Sync Error:", error);
42
+ }
43
+ };
44
+ const usePulseSync = (userId, baseUrl, tables) => {
45
+ const queryClient = (0, react_query_1.useQueryClient)();
46
+ (0, react_1.useEffect)(() => {
47
+ let ws;
48
+ let reconnectTimeout;
49
+ const connect = () => {
50
+ ws = new WebSocket(`ws://${baseUrl}/ws/sync/`);
51
+ activeSocket = ws;
52
+ ws.onopen = () => {
53
+ tables.forEach((tableName) => {
54
+ const pending = database_1.db.getAllSync(`SELECT * FROM ${tableName} WHERE is_local_only = 1`);
55
+ if (pending.length > 0) {
56
+ for (let i = 0; i < pending.length; i += CHUNK_SIZE) {
57
+ const chunk = pending.slice(i, i + CHUNK_SIZE);
58
+ ws.send(JSON.stringify({ type: 'SYNC_BATCH_UPLOAD', table: tableName, payloads: chunk }));
59
+ }
60
+ }
61
+ const res = database_1.db.getFirstSync(`SELECT MAX(version) as max_v FROM ${tableName}`);
62
+ ws.send(JSON.stringify({ type: 'SYNC_REQUEST_DELTA', table: tableName, last_version: res?.max_v || 0 }));
63
+ });
64
+ };
65
+ ws.onmessage = async (e) => {
66
+ const message = JSON.parse(e.data);
67
+ if (message.type === 'BATCH_ACK') {
68
+ const { table, synced_ids, errors } = message;
69
+ if (synced_ids.length > 0) {
70
+ const placeholders = synced_ids.map(() => '?').join(',');
71
+ database_1.db.runSync(`UPDATE ${table} SET is_local_only = 0, sync_error = NULL WHERE sync_id IN (${placeholders})`, synced_ids);
72
+ }
73
+ if (errors?.length > 0) {
74
+ errors.forEach((err) => {
75
+ database_1.db.runSync(`UPDATE ${table} SET sync_error = ? WHERE sync_id = ?`, [err.msg, err.id]);
76
+ });
77
+ }
78
+ queryClient.invalidateQueries({ queryKey: [table] });
79
+ }
80
+ if (message.type === 'SYNC_ACK_INDIVIDUAL') {
81
+ const { table, sync_id, status, error } = message;
82
+ if (status === 'SUCCESS') {
83
+ database_1.db.runSync(`UPDATE ${table} SET is_local_only = 0, sync_error = NULL WHERE sync_id = ?`, [sync_id]);
84
+ }
85
+ else {
86
+ database_1.db.runSync(`UPDATE ${table} SET sync_error = ? WHERE sync_id = ?`, [error, sync_id]);
87
+ }
88
+ queryClient.invalidateQueries({ queryKey: [table] });
89
+ }
90
+ if (message.type === 'SYNC_UPDATE') {
91
+ handleGenericIncomingData(message.data.model.toLowerCase(), message.data, queryClient);
92
+ }
93
+ };
94
+ ws.onclose = () => { reconnectTimeout = setTimeout(connect, 3000); activeSocket = null; };
95
+ ws.onerror = () => ws.close();
96
+ };
97
+ connect();
98
+ return () => { clearTimeout(reconnectTimeout); ws?.close(); activeSocket = null; };
99
+ }, [baseUrl]);
100
+ };
101
+ exports.usePulseSync = usePulseSync;
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "pulse-rn",
3
+ "version": "1.0.0",
4
+ "description": "Powerful offline-first sync engine for React Native. Compatible with Django Pulse and future Node.js pulse-core.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepare": "npm run build"
14
+ },
15
+ "keywords": ["react-native", "expo", "sync", "offline-first", "sqlite", "pulse"],
16
+ "author": "Jesus M.",
17
+ "license": "MIT",
18
+ "peerDependencies": {
19
+ "@tanstack/react-query": "^5.0.0",
20
+ "expo-crypto": "*",
21
+ "expo-sqlite": "*",
22
+ "react": "*"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^18.0.0",
26
+ "@types/react-native": "^0.72.8",
27
+ "typescript": "^5.0.0"
28
+ }
29
+ }