local-lite 0.0.3 → 0.0.4
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 +421 -362
- package/dist/adapters/local/better-sqlite3.d.ts +2 -0
- package/dist/adapters/local/better-sqlite3.js +46 -25
- package/dist/adapters/local/expo-sqlite.d.ts +5 -1
- package/dist/adapters/local/expo-sqlite.js +40 -7
- package/dist/adapters/local/index.d.ts +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +2 -0
- package/dist/adapters/local/node-sqlite.js +45 -22
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +6 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +5 -0
- package/dist/adapters/network/index.js +33 -0
- package/dist/adapters/remote/convex.d.ts +2 -2
- package/dist/adapters/remote/convex.js +8 -12
- package/dist/adapters/remote/demo.d.ts +1 -1
- package/dist/adapters/remote/demo.js +3 -0
- package/dist/adapters/remote/firestore.d.ts +3 -2
- package/dist/adapters/remote/firestore.js +69 -46
- package/dist/adapters/remote/index.d.ts +6 -4
- package/dist/adapters/remote/supabase.d.ts +34 -6
- package/dist/adapters/remote/supabase.js +340 -79
- package/dist/config/index.js +7 -10
- package/dist/config/resolve-local.js +12 -1
- package/dist/config/resolve-remote.js +33 -28
- package/dist/config/types.d.ts +30 -5
- package/dist/core/client.d.ts +86 -3
- package/dist/core/client.js +423 -41
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +29 -0
- package/dist/core/outbox-store.d.ts +8 -4
- package/dist/core/outbox-store.js +84 -65
- package/dist/core/query.d.ts +8 -0
- package/dist/core/query.js +37 -0
- package/dist/core/schema-manager.d.ts +9 -0
- package/dist/core/schema-manager.js +68 -0
- package/dist/core/sync-worker.d.ts +38 -1
- package/dist/core/sync-worker.js +351 -104
- package/dist/core/table-manager.js +26 -29
- package/dist/core/types.d.ts +8 -0
- package/dist/react/BenchmarkModal.d.ts +32 -0
- package/dist/react/BenchmarkModal.js +777 -0
- package/dist/react/SyncInspector.d.ts +31 -0
- package/dist/react/SyncInspector.js +655 -0
- package/dist/react/index.d.ts +51 -19
- package/dist/react/index.js +265 -105
- package/dist/react/runner.d.ts +7 -0
- package/dist/react/runner.js +42 -0
- package/dist/react/status.d.ts +24 -0
- package/dist/react/status.js +123 -0
- package/dist/react/theme.d.ts +18 -0
- package/dist/react/theme.js +46 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +20 -0
- package/package.json +138 -133
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import { RemoteDBAdapter } from './index.js';
|
|
3
|
-
import { DeltaChanges, SyncOperation } from '../../core/types.js';
|
|
3
|
+
import { DeltaChanges, PushResult, SyncOperation } from '../../core/types.js';
|
|
4
4
|
import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
|
|
5
5
|
export interface SupabaseAdapterOptions {
|
|
6
6
|
/**
|
|
@@ -13,6 +13,12 @@ export interface SupabaseAdapterOptions {
|
|
|
13
13
|
* Default: 'updated_at'
|
|
14
14
|
*/
|
|
15
15
|
updatedAtColumn?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional subset of tables to which dynamic `syncScope` filters should apply.
|
|
18
|
+
* If provided, scope filters (like `business_id`) will only be attached to queries for these tables.
|
|
19
|
+
* Tables not in this list (e.g. 'businesses', 'profiles') will be fetched without the scope filter.
|
|
20
|
+
*/
|
|
21
|
+
scopedTables?: string[];
|
|
16
22
|
/**
|
|
17
23
|
* Primary key column name on Postgres tables.
|
|
18
24
|
* Default: 'id'
|
|
@@ -55,10 +61,18 @@ export interface SupabaseAdapterOptions {
|
|
|
55
61
|
* Minimum log level. Default: 'info'
|
|
56
62
|
*/
|
|
57
63
|
logLevel?: LogLevel;
|
|
64
|
+
/**
|
|
65
|
+
* Maximum records pulled per page from Supabase. Default: 1000
|
|
66
|
+
*/
|
|
67
|
+
fetchLimit?: number;
|
|
58
68
|
}
|
|
69
|
+
export type AnySupabaseClient = SupabaseClient<any, any, any> | any;
|
|
70
|
+
export type SupabaseClientResolver = AnySupabaseClient | (() => AnySupabaseClient | null | undefined) | null | undefined;
|
|
59
71
|
export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
|
|
60
|
-
private
|
|
72
|
+
private clientResolver?;
|
|
73
|
+
private activeLiveCallback?;
|
|
61
74
|
private tables;
|
|
75
|
+
private scopedTables?;
|
|
62
76
|
private updatedAtColumn;
|
|
63
77
|
private primaryKey;
|
|
64
78
|
private deletedColumn;
|
|
@@ -67,8 +81,22 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
|
|
|
67
81
|
private schema;
|
|
68
82
|
private channelName;
|
|
69
83
|
private activeChannel?;
|
|
84
|
+
private fetchLimit;
|
|
70
85
|
logger: SyncLogger;
|
|
71
|
-
constructor(
|
|
86
|
+
constructor(clientOrOptions?: SupabaseClientResolver | SupabaseAdapterOptions, maybeOptions?: SupabaseAdapterOptions);
|
|
87
|
+
/**
|
|
88
|
+
* Resolves the active Supabase client instance dynamically.
|
|
89
|
+
*/
|
|
90
|
+
getClient(): AnySupabaseClient;
|
|
91
|
+
/**
|
|
92
|
+
* Property getter for backwards-compatibility.
|
|
93
|
+
*/
|
|
94
|
+
get client(): AnySupabaseClient;
|
|
95
|
+
/**
|
|
96
|
+
* Dynamically sets or updates the Supabase client or getter function.
|
|
97
|
+
* Automatically cleans up the previous realtime channel and re-subscribes if active.
|
|
98
|
+
*/
|
|
99
|
+
setClient(client: SupabaseClientResolver): void;
|
|
72
100
|
/**
|
|
73
101
|
* Set or update tracked tables dynamically.
|
|
74
102
|
*/
|
|
@@ -76,12 +104,12 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
|
|
|
76
104
|
/**
|
|
77
105
|
* Pushes outbox operations to Supabase.
|
|
78
106
|
*/
|
|
79
|
-
pushChanges(operations: SyncOperation[]): Promise<boolean>;
|
|
107
|
+
pushChanges(operations: SyncOperation[]): Promise<boolean | PushResult>;
|
|
80
108
|
/**
|
|
81
109
|
* Fetches delta changes from Supabase since a given timestamp.
|
|
82
110
|
* Reads updated records and checks `deleted: true` to propagate soft deletes.
|
|
83
111
|
*/
|
|
84
|
-
fetchChanges(sinceTimestamp: number): Promise<DeltaChanges>;
|
|
112
|
+
fetchChanges(sinceTimestamp: number, scope?: Record<string, any>): Promise<DeltaChanges>;
|
|
85
113
|
/**
|
|
86
114
|
* Helper to determine if a record represents a soft delete.
|
|
87
115
|
*/
|
|
@@ -89,7 +117,7 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
|
|
|
89
117
|
/**
|
|
90
118
|
* Subscribes to live realtime postgres_changes from Supabase.
|
|
91
119
|
*/
|
|
92
|
-
subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): void;
|
|
120
|
+
subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): (() => void) | void;
|
|
93
121
|
/**
|
|
94
122
|
* Clean up active realtime channels.
|
|
95
123
|
*/
|
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SupabaseRemoteAdapter = void 0;
|
|
4
4
|
const logger_js_1 = require("../../utils/logger.js");
|
|
5
5
|
class SupabaseRemoteAdapter {
|
|
6
|
-
|
|
6
|
+
clientResolver;
|
|
7
|
+
activeLiveCallback;
|
|
7
8
|
tables;
|
|
9
|
+
scopedTables;
|
|
8
10
|
updatedAtColumn;
|
|
9
11
|
primaryKey;
|
|
10
12
|
deletedColumn;
|
|
@@ -13,10 +15,27 @@ class SupabaseRemoteAdapter {
|
|
|
13
15
|
schema;
|
|
14
16
|
channelName;
|
|
15
17
|
activeChannel;
|
|
18
|
+
fetchLimit;
|
|
16
19
|
logger;
|
|
17
|
-
constructor(
|
|
18
|
-
|
|
20
|
+
constructor(clientOrOptions, maybeOptions = {}) {
|
|
21
|
+
let client;
|
|
22
|
+
let options;
|
|
23
|
+
// Allow new SupabaseRemoteAdapter(options) shorthand when no client is passed initially
|
|
24
|
+
if (clientOrOptions &&
|
|
25
|
+
typeof clientOrOptions === 'object' &&
|
|
26
|
+
!('channel' in clientOrOptions) &&
|
|
27
|
+
!('from' in clientOrOptions) &&
|
|
28
|
+
!('auth' in clientOrOptions)) {
|
|
29
|
+
client = null;
|
|
30
|
+
options = clientOrOptions;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
client = clientOrOptions;
|
|
34
|
+
options = maybeOptions;
|
|
35
|
+
}
|
|
36
|
+
this.clientResolver = client;
|
|
19
37
|
this.tables = options.tables ?? [];
|
|
38
|
+
this.scopedTables = options.scopedTables;
|
|
20
39
|
this.updatedAtColumn = options.updatedAtColumn ?? 'updated_at';
|
|
21
40
|
this.primaryKey = options.primaryKey ?? 'id';
|
|
22
41
|
this.deletedColumn = options.deletedColumn ?? options.deletedAtColumn ?? 'deleted';
|
|
@@ -24,17 +43,46 @@ class SupabaseRemoteAdapter {
|
|
|
24
43
|
this.pushRpcName = options.pushRpcName;
|
|
25
44
|
this.schema = options.schema ?? 'public';
|
|
26
45
|
this.channelName = options.channelName ?? 'local_first_sync';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
this.fetchLimit = options.fetchLimit ?? 1000;
|
|
47
|
+
this.logger = (0, logger_js_1.createSyncLogger)(options.logger, options.logLevel);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the active Supabase client instance dynamically.
|
|
51
|
+
*/
|
|
52
|
+
getClient() {
|
|
53
|
+
if (typeof this.clientResolver === 'function') {
|
|
54
|
+
try {
|
|
55
|
+
return this.clientResolver() ?? null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
32
60
|
}
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
return this.clientResolver ?? null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Property getter for backwards-compatibility.
|
|
65
|
+
*/
|
|
66
|
+
get client() {
|
|
67
|
+
return this.getClient();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Dynamically sets or updates the Supabase client or getter function.
|
|
71
|
+
* Automatically cleans up the previous realtime channel and re-subscribes if active.
|
|
72
|
+
*/
|
|
73
|
+
setClient(client) {
|
|
74
|
+
const previousClient = this.getClient();
|
|
75
|
+
this.clientResolver = client;
|
|
76
|
+
const newClient = this.getClient();
|
|
77
|
+
if (this.activeChannel && previousClient?.removeChannel && previousClient !== newClient) {
|
|
78
|
+
try {
|
|
79
|
+
previousClient.removeChannel(this.activeChannel);
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
this.activeChannel = undefined;
|
|
35
83
|
}
|
|
36
|
-
|
|
37
|
-
this.
|
|
84
|
+
if (this.activeLiveCallback && newClient && typeof newClient.channel === 'function') {
|
|
85
|
+
this.subscribeToLiveChanges(this.activeLiveCallback);
|
|
38
86
|
}
|
|
39
87
|
}
|
|
40
88
|
/**
|
|
@@ -50,9 +98,14 @@ class SupabaseRemoteAdapter {
|
|
|
50
98
|
if (!operations || operations.length === 0) {
|
|
51
99
|
return true;
|
|
52
100
|
}
|
|
101
|
+
const client = this.getClient();
|
|
102
|
+
if (!client) {
|
|
103
|
+
this.logger.debug('Supabase client is not available; skipping remote push.');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
53
106
|
// 1. If custom RPC is configured, perform atomic batch RPC
|
|
54
107
|
if (this.pushRpcName) {
|
|
55
|
-
const { error } = await
|
|
108
|
+
const { error } = await client.rpc(this.pushRpcName, {
|
|
56
109
|
operations: operations.map((op) => ({
|
|
57
110
|
id: op.id,
|
|
58
111
|
table_name: op.tableName,
|
|
@@ -68,28 +121,129 @@ class SupabaseRemoteAdapter {
|
|
|
68
121
|
}
|
|
69
122
|
return true;
|
|
70
123
|
}
|
|
71
|
-
// 2. Direct table operations
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
124
|
+
// 2. Direct table operations with consecutive batching and per-operation tracking
|
|
125
|
+
const successfulIds = [];
|
|
126
|
+
const failedIds = [];
|
|
127
|
+
const errors = {};
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < operations.length) {
|
|
130
|
+
const op = operations[i];
|
|
131
|
+
// Batch consecutive INSERTs for the same table
|
|
132
|
+
if (op.operation === 'INSERT') {
|
|
133
|
+
const batch = [];
|
|
134
|
+
while (i < operations.length &&
|
|
135
|
+
operations[i].operation === 'INSERT' &&
|
|
136
|
+
operations[i].tableName === op.tableName) {
|
|
137
|
+
batch.push(operations[i]);
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
const buildRowData = (item) => {
|
|
141
|
+
const row = {
|
|
142
|
+
...(item.payload || {}),
|
|
143
|
+
[this.primaryKey]: item.rowId,
|
|
144
|
+
[this.deletedColumn]: false,
|
|
145
|
+
};
|
|
146
|
+
if (this.updatedAtColumn && !row[this.updatedAtColumn]) {
|
|
147
|
+
row[this.updatedAtColumn] = new Date(item.timestamp).toISOString();
|
|
148
|
+
}
|
|
149
|
+
return row;
|
|
150
|
+
};
|
|
151
|
+
if (batch.length === 1) {
|
|
152
|
+
try {
|
|
153
|
+
const rowData = buildRowData(batch[0]);
|
|
154
|
+
const { error } = await client.from(op.tableName).upsert(rowData);
|
|
155
|
+
if (error) {
|
|
156
|
+
const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'INSERT' });
|
|
157
|
+
this.logger.error(msg);
|
|
158
|
+
failedIds.push(batch[0].id);
|
|
159
|
+
errors[batch[0].id] = msg;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
successfulIds.push(batch[0].id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'INSERT' });
|
|
167
|
+
this.logger.error(msg);
|
|
168
|
+
failedIds.push(batch[0].id);
|
|
169
|
+
errors[batch[0].id] = msg;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Bulk upsert for multiple consecutive inserts
|
|
174
|
+
const retryIndividually = async () => {
|
|
175
|
+
for (const item of batch) {
|
|
176
|
+
try {
|
|
177
|
+
const singleRow = buildRowData(item);
|
|
178
|
+
const { error: singleErr } = await client.from(op.tableName).upsert(singleRow);
|
|
179
|
+
if (singleErr) {
|
|
180
|
+
const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'INSERT' });
|
|
181
|
+
failedIds.push(item.id);
|
|
182
|
+
errors[item.id] = msg;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
successfulIds.push(item.id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (singleErr) {
|
|
189
|
+
const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'INSERT' });
|
|
190
|
+
failedIds.push(item.id);
|
|
191
|
+
errors[item.id] = msg;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
const rows = batch.map(buildRowData);
|
|
197
|
+
const { error } = await client.from(op.tableName).upsert(rows);
|
|
198
|
+
if (!error) {
|
|
199
|
+
for (const item of batch)
|
|
200
|
+
successfulIds.push(item.id);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
await retryIndividually();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
await retryIndividually();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// Handle DELETE
|
|
213
|
+
if (op.operation === 'DELETE') {
|
|
214
|
+
try {
|
|
76
215
|
const patch = {
|
|
77
216
|
[this.deletedColumn]: this.deletedValue,
|
|
78
217
|
};
|
|
79
218
|
if (this.updatedAtColumn) {
|
|
80
219
|
patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
|
|
81
220
|
}
|
|
82
|
-
const { error } = await
|
|
221
|
+
const { error } = await client
|
|
83
222
|
.from(op.tableName)
|
|
84
223
|
.update(patch)
|
|
85
224
|
.eq(this.primaryKey, op.rowId);
|
|
86
225
|
if (error) {
|
|
87
|
-
|
|
88
|
-
|
|
226
|
+
const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'DELETE' });
|
|
227
|
+
this.logger.error(msg);
|
|
228
|
+
failedIds.push(op.id);
|
|
229
|
+
errors[op.id] = msg;
|
|
89
230
|
}
|
|
231
|
+
else {
|
|
232
|
+
successfulIds.push(op.id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'DELETE' });
|
|
237
|
+
this.logger.error(msg);
|
|
238
|
+
failedIds.push(op.id);
|
|
239
|
+
errors[op.id] = msg;
|
|
90
240
|
}
|
|
91
|
-
|
|
92
|
-
|
|
241
|
+
i++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// Handle UPDATE
|
|
245
|
+
if (op.operation === 'UPDATE') {
|
|
246
|
+
try {
|
|
93
247
|
const patch = {
|
|
94
248
|
...(op.payload || {}),
|
|
95
249
|
[this.deletedColumn]: false,
|
|
@@ -97,91 +251,130 @@ class SupabaseRemoteAdapter {
|
|
|
97
251
|
if (this.updatedAtColumn) {
|
|
98
252
|
patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
|
|
99
253
|
}
|
|
100
|
-
const { error, data } = await
|
|
254
|
+
const { error, data } = await client
|
|
101
255
|
.from(op.tableName)
|
|
102
256
|
.update(patch)
|
|
103
257
|
.eq(this.primaryKey, op.rowId)
|
|
104
258
|
.select(this.primaryKey);
|
|
105
259
|
if (error) {
|
|
106
|
-
|
|
107
|
-
|
|
260
|
+
const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'UPDATE' });
|
|
261
|
+
this.logger.error(msg);
|
|
262
|
+
failedIds.push(op.id);
|
|
263
|
+
errors[op.id] = msg;
|
|
108
264
|
}
|
|
109
|
-
|
|
110
|
-
|
|
265
|
+
else if (!data || data.length === 0) {
|
|
266
|
+
// If row did not exist remotely, fallback to upsert
|
|
111
267
|
const rowData = {
|
|
112
268
|
...patch,
|
|
113
269
|
[this.primaryKey]: op.rowId,
|
|
114
270
|
};
|
|
115
|
-
const { error: upsertErr } = await
|
|
116
|
-
.from(op.tableName)
|
|
117
|
-
.upsert(rowData);
|
|
271
|
+
const { error: upsertErr } = await client.from(op.tableName).upsert(rowData);
|
|
118
272
|
if (upsertErr) {
|
|
119
|
-
|
|
120
|
-
|
|
273
|
+
const msg = (0, logger_js_1.formatSupabaseError)(upsertErr, { table: op.tableName, operation: 'UPSERT_FALLBACK' });
|
|
274
|
+
this.logger.error(msg);
|
|
275
|
+
failedIds.push(op.id);
|
|
276
|
+
errors[op.id] = msg;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
successfulIds.push(op.id);
|
|
121
280
|
}
|
|
122
281
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// INSERT -> upsert into table with deleted: false
|
|
126
|
-
const rowData = {
|
|
127
|
-
...(op.payload || {}),
|
|
128
|
-
[this.primaryKey]: op.rowId,
|
|
129
|
-
[this.deletedColumn]: false,
|
|
130
|
-
};
|
|
131
|
-
if (this.updatedAtColumn && !rowData[this.updatedAtColumn]) {
|
|
132
|
-
rowData[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
|
|
133
|
-
}
|
|
134
|
-
const { error } = await this.client
|
|
135
|
-
.from(op.tableName)
|
|
136
|
-
.upsert(rowData);
|
|
137
|
-
if (error) {
|
|
138
|
-
this.logger.error((0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'INSERT' }));
|
|
139
|
-
return false;
|
|
282
|
+
else {
|
|
283
|
+
successfulIds.push(op.id);
|
|
140
284
|
}
|
|
141
285
|
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'UPDATE' });
|
|
288
|
+
this.logger.error(msg);
|
|
289
|
+
failedIds.push(op.id);
|
|
290
|
+
errors[op.id] = msg;
|
|
291
|
+
}
|
|
292
|
+
i++;
|
|
293
|
+
continue;
|
|
142
294
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
if (failedIds.length === 0) {
|
|
298
|
+
return true;
|
|
147
299
|
}
|
|
148
|
-
return
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
successfulIds,
|
|
303
|
+
failedIds,
|
|
304
|
+
errors,
|
|
305
|
+
};
|
|
149
306
|
}
|
|
150
307
|
/**
|
|
151
308
|
* Fetches delta changes from Supabase since a given timestamp.
|
|
152
309
|
* Reads updated records and checks `deleted: true` to propagate soft deletes.
|
|
153
310
|
*/
|
|
154
|
-
async fetchChanges(sinceTimestamp) {
|
|
311
|
+
async fetchChanges(sinceTimestamp, scope) {
|
|
312
|
+
const client = this.getClient();
|
|
313
|
+
if (!client) {
|
|
314
|
+
this.logger.debug('Supabase client is not available; skipping fetchChanges.');
|
|
315
|
+
return {
|
|
316
|
+
timestamp: sinceTimestamp,
|
|
317
|
+
changes: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
155
320
|
const changes = [];
|
|
156
321
|
const sinceIso = sinceTimestamp > 0 ? new Date(sinceTimestamp).toISOString() : null;
|
|
157
322
|
let maxServerTimestamp = sinceTimestamp;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
323
|
+
const fetchTableDeltas = async (table) => {
|
|
324
|
+
const isTableScoped = !this.scopedTables || this.scopedTables.includes(table);
|
|
325
|
+
const tableChanges = [];
|
|
326
|
+
let hasMore = true;
|
|
327
|
+
let currentSinceIso = sinceIso;
|
|
328
|
+
while (hasMore) {
|
|
329
|
+
let query = client.from(table).select('*');
|
|
330
|
+
if (isTableScoped && scope && typeof scope === 'object') {
|
|
331
|
+
for (const [col, val] of Object.entries(scope)) {
|
|
332
|
+
if (val !== undefined && val !== null && typeof query.eq === 'function') {
|
|
333
|
+
query = query.eq(col, val);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (currentSinceIso) {
|
|
338
|
+
query = query.gt(this.updatedAtColumn, currentSinceIso);
|
|
165
339
|
}
|
|
166
340
|
if (typeof query.order === 'function') {
|
|
167
341
|
query = query.order(this.updatedAtColumn, { ascending: true });
|
|
168
342
|
}
|
|
169
|
-
|
|
343
|
+
if (typeof query.limit === 'function') {
|
|
344
|
+
query = query.limit(this.fetchLimit);
|
|
345
|
+
}
|
|
346
|
+
let { data, error } = await query;
|
|
347
|
+
// If PostgreSQL returns 42703 (undefined column) because a scoped column does not exist on this table,
|
|
348
|
+
// gracefully fallback to querying without the scope filter so synchronization doesn't halt.
|
|
349
|
+
if (error &&
|
|
350
|
+
(error.code === '42703' || String(error.message || '').includes('does not exist')) &&
|
|
351
|
+
isTableScoped) {
|
|
352
|
+
this.logger.debug(`[local-lite] Column in scope does not exist on table "${table}". Retrying without scope filter.`);
|
|
353
|
+
let fallbackQuery = client.from(table).select('*');
|
|
354
|
+
if (currentSinceIso) {
|
|
355
|
+
fallbackQuery = fallbackQuery.gt(this.updatedAtColumn, currentSinceIso);
|
|
356
|
+
}
|
|
357
|
+
if (typeof fallbackQuery.order === 'function') {
|
|
358
|
+
fallbackQuery = fallbackQuery.order(this.updatedAtColumn, { ascending: true });
|
|
359
|
+
}
|
|
360
|
+
if (typeof fallbackQuery.limit === 'function') {
|
|
361
|
+
fallbackQuery = fallbackQuery.limit(this.fetchLimit);
|
|
362
|
+
}
|
|
363
|
+
const fallbackResult = await fallbackQuery;
|
|
364
|
+
data = fallbackResult.data;
|
|
365
|
+
error = fallbackResult.error;
|
|
366
|
+
}
|
|
170
367
|
if (error) {
|
|
171
|
-
|
|
172
|
-
continue;
|
|
368
|
+
throw error;
|
|
173
369
|
}
|
|
174
|
-
if (data && Array.isArray(data)) {
|
|
370
|
+
if (data && Array.isArray(data) && data.length > 0) {
|
|
175
371
|
for (const row of data) {
|
|
176
372
|
const isSoftDeleted = this.isSoftDeleted(row);
|
|
177
373
|
const operation = isSoftDeleted ? 'DELETE' : 'UPDATE';
|
|
178
374
|
const rowTime = row[this.updatedAtColumn]
|
|
179
375
|
? new Date(row[this.updatedAtColumn]).getTime()
|
|
180
376
|
: Date.now();
|
|
181
|
-
|
|
182
|
-
maxServerTimestamp = rowTime;
|
|
183
|
-
}
|
|
184
|
-
changes.push({
|
|
377
|
+
tableChanges.push({
|
|
185
378
|
tableName: table,
|
|
186
379
|
operation,
|
|
187
380
|
rowId: String(row[this.primaryKey]),
|
|
@@ -189,10 +382,55 @@ class SupabaseRemoteAdapter {
|
|
|
189
382
|
timestamp: rowTime,
|
|
190
383
|
});
|
|
191
384
|
}
|
|
385
|
+
// Stop paging if unpaginated query or fewer rows than limit returned
|
|
386
|
+
if (typeof query.limit !== 'function' || data.length < this.fetchLimit) {
|
|
387
|
+
hasMore = false;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const lastRow = data[data.length - 1];
|
|
391
|
+
const nextIso = lastRow[this.updatedAtColumn]
|
|
392
|
+
? new Date(lastRow[this.updatedAtColumn]).toISOString()
|
|
393
|
+
: null;
|
|
394
|
+
if (nextIso && nextIso !== currentSinceIso) {
|
|
395
|
+
currentSinceIso = nextIso;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
hasMore = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
hasMore = false;
|
|
192
404
|
}
|
|
193
405
|
}
|
|
194
|
-
|
|
195
|
-
|
|
406
|
+
return tableChanges;
|
|
407
|
+
};
|
|
408
|
+
// Query tables in concurrent chunks of 5 to avoid 100+ second serial roundtrips
|
|
409
|
+
const CHUNK_SIZE = 5;
|
|
410
|
+
for (let i = 0; i < this.tables.length; i += CHUNK_SIZE) {
|
|
411
|
+
const chunk = this.tables.slice(i, i + CHUNK_SIZE);
|
|
412
|
+
const results = await Promise.all(chunk.map(async (table) => {
|
|
413
|
+
try {
|
|
414
|
+
return await fetchTableDeltas(table);
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
const msg = (0, logger_js_1.formatSupabaseError)(err, { table, operation: 'PULL' });
|
|
418
|
+
if (msg.includes('Network Offline') || msg.includes('Failed to reach')) {
|
|
419
|
+
this.logger.warn(msg);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
this.logger.error(msg);
|
|
423
|
+
}
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
}));
|
|
427
|
+
for (const tableChanges of results) {
|
|
428
|
+
for (const change of tableChanges) {
|
|
429
|
+
if (change.timestamp !== undefined && change.timestamp > maxServerTimestamp) {
|
|
430
|
+
maxServerTimestamp = change.timestamp;
|
|
431
|
+
}
|
|
432
|
+
changes.push(change);
|
|
433
|
+
}
|
|
196
434
|
}
|
|
197
435
|
}
|
|
198
436
|
return {
|
|
@@ -218,14 +456,29 @@ class SupabaseRemoteAdapter {
|
|
|
218
456
|
* Subscribes to live realtime postgres_changes from Supabase.
|
|
219
457
|
*/
|
|
220
458
|
subscribeToLiveChanges(callback) {
|
|
221
|
-
|
|
222
|
-
|
|
459
|
+
this.activeLiveCallback = callback;
|
|
460
|
+
const client = this.getClient();
|
|
461
|
+
if (this.activeChannel && client?.removeChannel) {
|
|
462
|
+
try {
|
|
463
|
+
client.removeChannel(this.activeChannel);
|
|
464
|
+
}
|
|
465
|
+
catch { }
|
|
223
466
|
this.activeChannel = undefined;
|
|
224
467
|
}
|
|
468
|
+
if (!client || typeof client.channel !== 'function') {
|
|
469
|
+
this.logger.debug('Supabase client is not available; skipping live changes subscription.');
|
|
470
|
+
return () => {
|
|
471
|
+
this.activeLiveCallback = undefined;
|
|
472
|
+
this.destroy();
|
|
473
|
+
};
|
|
474
|
+
}
|
|
225
475
|
if (this.tables.length === 0) {
|
|
226
|
-
return
|
|
476
|
+
return () => {
|
|
477
|
+
this.activeLiveCallback = undefined;
|
|
478
|
+
this.destroy();
|
|
479
|
+
};
|
|
227
480
|
}
|
|
228
|
-
const channel =
|
|
481
|
+
const channel = client.channel(this.channelName);
|
|
229
482
|
for (const table of this.tables) {
|
|
230
483
|
channel.on('postgres_changes', { event: '*', schema: this.schema, table }, (payload) => {
|
|
231
484
|
let op = 'UPDATE';
|
|
@@ -262,13 +515,21 @@ class SupabaseRemoteAdapter {
|
|
|
262
515
|
}
|
|
263
516
|
});
|
|
264
517
|
this.activeChannel = channel;
|
|
518
|
+
return () => {
|
|
519
|
+
this.activeLiveCallback = undefined;
|
|
520
|
+
this.destroy();
|
|
521
|
+
};
|
|
265
522
|
}
|
|
266
523
|
/**
|
|
267
524
|
* Clean up active realtime channels.
|
|
268
525
|
*/
|
|
269
526
|
destroy() {
|
|
270
|
-
|
|
271
|
-
|
|
527
|
+
const client = this.getClient();
|
|
528
|
+
if (this.activeChannel && client?.removeChannel) {
|
|
529
|
+
try {
|
|
530
|
+
client.removeChannel(this.activeChannel);
|
|
531
|
+
}
|
|
532
|
+
catch { }
|
|
272
533
|
this.activeChannel = undefined;
|
|
273
534
|
}
|
|
274
535
|
}
|
package/dist/config/index.js
CHANGED
|
@@ -67,9 +67,7 @@ async function createSyncClient(config) {
|
|
|
67
67
|
const remoteAdapter = await (0, resolve_remote_js_1.resolveRemoteAdapter)(config);
|
|
68
68
|
// 3. Resolve Network Provider
|
|
69
69
|
const network = config.network ?? new index_js_1.DefaultNetworkStatusProvider(true);
|
|
70
|
-
const autoSchema = config.autoSchema
|
|
71
|
-
? config.autoSchema
|
|
72
|
-
: !config.migrations || config.migrations.length === 0;
|
|
70
|
+
const autoSchema = config.autoSchema ?? false;
|
|
73
71
|
// 4. Instantiate Local-First Client
|
|
74
72
|
const client = new client_js_1.LocalFirstClient({
|
|
75
73
|
local: localAdapter,
|
|
@@ -85,17 +83,16 @@ async function createSyncClient(config) {
|
|
|
85
83
|
logLevel: config.logLevel,
|
|
86
84
|
autoSchema,
|
|
87
85
|
tables: config.tables ?? [],
|
|
86
|
+
migrations: config.migrations,
|
|
87
|
+
autoStart: config.autoStart,
|
|
88
|
+
jsonColumns: config.jsonColumns,
|
|
89
|
+
autoHydrateJSON: config.autoHydrateJSON,
|
|
90
|
+
syncScope: config.syncScope,
|
|
91
|
+
autoPauseOnBackground: config.autoPauseOnBackground,
|
|
88
92
|
});
|
|
89
93
|
// 5. Execute Migrations & Auto-initialize
|
|
90
94
|
if (config.autoInitialize !== false) {
|
|
91
95
|
await client.initialize();
|
|
92
|
-
if (config.migrations && config.migrations.length > 0) {
|
|
93
|
-
for (const query of config.migrations) {
|
|
94
|
-
if (query && query.trim()) {
|
|
95
|
-
await client.db.execute(query);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
96
|
}
|
|
100
97
|
return client;
|
|
101
98
|
}
|