local-lite 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/adapters/local/better-sqlite3.d.ts +23 -0
- package/dist/adapters/local/better-sqlite3.js +46 -0
- package/dist/adapters/local/expo-sqlite.d.ts +8 -0
- package/dist/adapters/local/expo-sqlite.js +30 -0
- package/dist/adapters/local/index.d.ts +13 -0
- package/dist/adapters/local/index.js +22 -0
- package/dist/adapters/local/libsql.d.ts +24 -0
- package/dist/adapters/local/libsql.js +87 -0
- package/dist/adapters/local/node-sqlite.d.ts +24 -0
- package/dist/adapters/local/node-sqlite.js +44 -0
- package/dist/adapters/local/op-sqlite.d.ts +26 -0
- package/dist/adapters/local/op-sqlite.js +59 -0
- package/dist/adapters/local/sql-js.d.ts +26 -0
- package/dist/adapters/local/sql-js.js +63 -0
- package/dist/adapters/network/index.d.ts +11 -0
- package/dist/adapters/network/index.js +53 -0
- package/dist/adapters/remote/convex.d.ts +71 -0
- package/dist/adapters/remote/convex.js +185 -0
- package/dist/adapters/remote/demo.d.ts +21 -0
- package/dist/adapters/remote/demo.js +46 -0
- package/dist/adapters/remote/firestore.d.ts +64 -0
- package/dist/adapters/remote/firestore.js +302 -0
- package/dist/adapters/remote/index.d.ts +10 -0
- package/dist/adapters/remote/index.js +20 -0
- package/dist/adapters/remote/supabase.d.ts +97 -0
- package/dist/adapters/remote/supabase.js +276 -0
- package/dist/config/index.d.ts +24 -0
- package/dist/config/index.js +101 -0
- package/dist/config/resolve-local.d.ts +7 -0
- package/dist/config/resolve-local.js +68 -0
- package/dist/config/resolve-remote.d.ts +7 -0
- package/dist/config/resolve-remote.js +81 -0
- package/dist/config/types.d.ts +159 -0
- package/dist/config/types.js +2 -0
- package/dist/core/client.d.ts +153 -0
- package/dist/core/client.js +445 -0
- package/dist/core/conflict.d.ts +5 -0
- package/dist/core/conflict.js +38 -0
- package/dist/core/meta-store.d.ts +28 -0
- package/dist/core/meta-store.js +64 -0
- package/dist/core/outbox-store.d.ts +76 -0
- package/dist/core/outbox-store.js +265 -0
- package/dist/core/query.d.ts +13 -0
- package/dist/core/query.js +150 -0
- package/dist/core/schema-manager.d.ts +39 -0
- package/dist/core/schema-manager.js +150 -0
- package/dist/core/sync-worker.d.ts +89 -0
- package/dist/core/sync-worker.js +473 -0
- package/dist/core/table-manager.d.ts +19 -0
- package/dist/core/table-manager.js +74 -0
- package/dist/core/types.d.ts +156 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +34 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.js +174 -0
- package/dist/reactive/event-emitter.d.ts +7 -0
- package/dist/reactive/event-emitter.js +34 -0
- package/dist/utils/logger.d.ts +51 -0
- package/dist/utils/logger.js +169 -0
- package/dist/utils/sql.d.ts +4 -0
- package/dist/utils/sql.js +9 -0
- package/dist/utils/uuid.d.ts +7 -0
- package/dist/utils/uuid.js +27 -0
- package/package.json +131 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export type SyncOp = 'INSERT' | 'UPDATE' | 'DELETE';
|
|
2
|
+
export type OutboxStatus = 'PENDING' | 'PROCESSING' | 'FAILED';
|
|
3
|
+
export interface SyncOperation {
|
|
4
|
+
id: string;
|
|
5
|
+
tableName: string;
|
|
6
|
+
rowId: string;
|
|
7
|
+
operation: SyncOp;
|
|
8
|
+
payload: Record<string, any> | null;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
export interface OutboxRecord extends SyncOperation {
|
|
12
|
+
status: OutboxStatus;
|
|
13
|
+
retryCount: number;
|
|
14
|
+
lastError?: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface DeltaChange {
|
|
17
|
+
tableName: string;
|
|
18
|
+
operation: SyncOp;
|
|
19
|
+
rowId: string;
|
|
20
|
+
payload: Record<string, any> | null;
|
|
21
|
+
timestamp?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DeltaChanges {
|
|
24
|
+
timestamp: number;
|
|
25
|
+
changes: DeltaChange[];
|
|
26
|
+
}
|
|
27
|
+
export type ConflictResolution = 'apply-remote' | 'keep-local' | {
|
|
28
|
+
mergePayload: Record<string, any>;
|
|
29
|
+
};
|
|
30
|
+
export interface ConflictContext {
|
|
31
|
+
tableName: string;
|
|
32
|
+
rowId: string;
|
|
33
|
+
remoteChange: DeltaChange;
|
|
34
|
+
localOutboxRecord?: OutboxRecord;
|
|
35
|
+
localRow?: Record<string, any> | null;
|
|
36
|
+
}
|
|
37
|
+
export type ConflictResolver = (context: ConflictContext) => ConflictResolution | Promise<ConflictResolution>;
|
|
38
|
+
export type ConflictStrategy = 'last-write-wins' | 'server-wins' | 'client-wins';
|
|
39
|
+
export interface NetworkStatusProvider {
|
|
40
|
+
isOnline(): boolean | Promise<boolean>;
|
|
41
|
+
subscribe(callback: (isOnline: boolean) => void): () => void;
|
|
42
|
+
}
|
|
43
|
+
export interface RetryConfig {
|
|
44
|
+
/** Maximum retry attempts before marking outbox record as permanently FAILED. Set to `Infinity` or `-1` for infinite retries. Default: 5 */
|
|
45
|
+
maxRetries?: number;
|
|
46
|
+
/** Initial delay before retrying failed operations in milliseconds. Default: 1000 */
|
|
47
|
+
initialDelayMs?: number;
|
|
48
|
+
/** Backoff multiplier applied on consecutive failures (e.g. 2 for exponential). Default: 1.5 */
|
|
49
|
+
backoffMultiplier?: number;
|
|
50
|
+
/** Maximum capped delay between retries in milliseconds. Default: 30000 */
|
|
51
|
+
maxDelayMs?: number;
|
|
52
|
+
/** Whether to apply randomized jitter (±20%) to backoff delays to prevent thundering herd. Default: true */
|
|
53
|
+
jitter?: boolean;
|
|
54
|
+
/** Custom predicate deciding whether an error is retryable. Return false for non-retryable errors like permanent RLS violations. */
|
|
55
|
+
shouldRetry?: (error: any, operation: SyncOperation) => boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface MutateOptions {
|
|
58
|
+
primaryKey?: string;
|
|
59
|
+
tx?: any;
|
|
60
|
+
customTimestamp?: number;
|
|
61
|
+
skipOutbox?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface TableChangeEvent {
|
|
64
|
+
operation: SyncOp;
|
|
65
|
+
rowId: string;
|
|
66
|
+
payload?: Record<string, any> | null;
|
|
67
|
+
source: 'local' | 'remote';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Filter operator comparison options for query predicates.
|
|
71
|
+
*/
|
|
72
|
+
export interface ComparisonOperator<T = any> {
|
|
73
|
+
$eq?: T;
|
|
74
|
+
$ne?: T;
|
|
75
|
+
$gt?: T;
|
|
76
|
+
$gte?: T;
|
|
77
|
+
$lt?: T;
|
|
78
|
+
$lte?: T;
|
|
79
|
+
$in?: T[];
|
|
80
|
+
$nin?: T[];
|
|
81
|
+
$contains?: string;
|
|
82
|
+
$startsWith?: string;
|
|
83
|
+
$endsWith?: string;
|
|
84
|
+
$null?: boolean;
|
|
85
|
+
}
|
|
86
|
+
export type FieldFilter<T = any> = T | ComparisonOperator<T> | ((value: T) => boolean);
|
|
87
|
+
export type WhereFilter = Record<string, FieldFilter> | ((row: Record<string, any>) => boolean) | {
|
|
88
|
+
$or?: (Record<string, FieldFilter> | ((row: Record<string, any>) => boolean))[];
|
|
89
|
+
$and?: (Record<string, FieldFilter> | ((row: Record<string, any>) => boolean))[];
|
|
90
|
+
[key: string]: any;
|
|
91
|
+
};
|
|
92
|
+
export interface QueryOptions {
|
|
93
|
+
where?: WhereFilter;
|
|
94
|
+
orderBy?: string;
|
|
95
|
+
order?: 'asc' | 'desc' | 'ASC' | 'DESC';
|
|
96
|
+
limit?: number;
|
|
97
|
+
offset?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Individual mutation descriptor for atomic batch execution.
|
|
101
|
+
*/
|
|
102
|
+
export interface BatchMutation {
|
|
103
|
+
tableName: string;
|
|
104
|
+
rowId?: string;
|
|
105
|
+
operation: SyncOp;
|
|
106
|
+
payload?: Record<string, any> | null;
|
|
107
|
+
primaryKey?: string;
|
|
108
|
+
skipOutbox?: boolean;
|
|
109
|
+
customTimestamp?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Real-time operational telemetry metrics captured by the sync engine.
|
|
113
|
+
*/
|
|
114
|
+
export interface SyncMetrics {
|
|
115
|
+
outboxPendingCount: number;
|
|
116
|
+
outboxFailedCount: number;
|
|
117
|
+
outboxProcessingCount: number;
|
|
118
|
+
totalPushedOperations: number;
|
|
119
|
+
totalPulledDeltas: number;
|
|
120
|
+
totalPushCycles: number;
|
|
121
|
+
totalPullCycles: number;
|
|
122
|
+
lastPushDurationMs: number;
|
|
123
|
+
lastPullDurationMs: number;
|
|
124
|
+
lastPushTimestamp: number;
|
|
125
|
+
lastPullTimestamp: number;
|
|
126
|
+
lastPushError?: string | null;
|
|
127
|
+
lastPullError?: string | null;
|
|
128
|
+
isOnline: boolean;
|
|
129
|
+
isPushing: boolean;
|
|
130
|
+
isPulling: boolean;
|
|
131
|
+
}
|
|
132
|
+
export interface SyncStatusEvent {
|
|
133
|
+
isOnline: boolean;
|
|
134
|
+
isPushing: boolean;
|
|
135
|
+
isPulling: boolean;
|
|
136
|
+
pendingCount: number;
|
|
137
|
+
}
|
|
138
|
+
export interface PushEvent {
|
|
139
|
+
success: boolean;
|
|
140
|
+
count: number;
|
|
141
|
+
durationMs: number;
|
|
142
|
+
error?: string | null;
|
|
143
|
+
}
|
|
144
|
+
export interface PullEvent {
|
|
145
|
+
success: boolean;
|
|
146
|
+
changesCount: number;
|
|
147
|
+
durationMs: number;
|
|
148
|
+
timestamp: number;
|
|
149
|
+
error?: string | null;
|
|
150
|
+
}
|
|
151
|
+
export interface ConflictEvent {
|
|
152
|
+
tableName: string;
|
|
153
|
+
rowId: string;
|
|
154
|
+
resolution: string;
|
|
155
|
+
timestamp: number;
|
|
156
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './core/client.js';
|
|
2
|
+
export * from './core/types.js';
|
|
3
|
+
export * from './core/query.js';
|
|
4
|
+
export * from './core/sync-worker.js';
|
|
5
|
+
export * from './core/conflict.js';
|
|
6
|
+
export * from './core/outbox-store.js';
|
|
7
|
+
export * from './core/meta-store.js';
|
|
8
|
+
export * from './core/table-manager.js';
|
|
9
|
+
export * from './core/schema-manager.js';
|
|
10
|
+
export * from './reactive/event-emitter.js';
|
|
11
|
+
export * from './adapters/local/index.js';
|
|
12
|
+
export * from './adapters/remote/index.js';
|
|
13
|
+
export * from './adapters/network/index.js';
|
|
14
|
+
export * from './config/index.js';
|
|
15
|
+
export * from './react/index.js';
|
|
16
|
+
export * from './utils/uuid.js';
|
|
17
|
+
export * from './utils/sql.js';
|
|
18
|
+
export * from './utils/logger.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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("./core/client.js"), exports);
|
|
18
|
+
__exportStar(require("./core/types.js"), exports);
|
|
19
|
+
__exportStar(require("./core/query.js"), exports);
|
|
20
|
+
__exportStar(require("./core/sync-worker.js"), exports);
|
|
21
|
+
__exportStar(require("./core/conflict.js"), exports);
|
|
22
|
+
__exportStar(require("./core/outbox-store.js"), exports);
|
|
23
|
+
__exportStar(require("./core/meta-store.js"), exports);
|
|
24
|
+
__exportStar(require("./core/table-manager.js"), exports);
|
|
25
|
+
__exportStar(require("./core/schema-manager.js"), exports);
|
|
26
|
+
__exportStar(require("./reactive/event-emitter.js"), exports);
|
|
27
|
+
__exportStar(require("./adapters/local/index.js"), exports);
|
|
28
|
+
__exportStar(require("./adapters/remote/index.js"), exports);
|
|
29
|
+
__exportStar(require("./adapters/network/index.js"), exports);
|
|
30
|
+
__exportStar(require("./config/index.js"), exports);
|
|
31
|
+
__exportStar(require("./react/index.js"), exports);
|
|
32
|
+
__exportStar(require("./utils/uuid.js"), exports);
|
|
33
|
+
__exportStar(require("./utils/sql.js"), exports);
|
|
34
|
+
__exportStar(require("./utils/logger.js"), exports);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { LocalFirstClient } from '../core/client.js';
|
|
2
|
+
import { QueryOptions, SyncMetrics } from '../core/types.js';
|
|
3
|
+
export type LiveQueryFn<T> = (client: LocalFirstClient) => Promise<T[]> | T[];
|
|
4
|
+
/**
|
|
5
|
+
* Hook to execute a live reactive table query that automatically re-runs
|
|
6
|
+
* whenever the table is mutated locally or via incoming remote delta.
|
|
7
|
+
*
|
|
8
|
+
* Supports 3 flexible usage patterns:
|
|
9
|
+
* 1. Zero-SQL (all rows):
|
|
10
|
+
* const { data: todos } = useLiveQuery<Todo>(client, 'todos');
|
|
11
|
+
*
|
|
12
|
+
* 2. Declarative options (filtering with rich operators & sorting without SQL):
|
|
13
|
+
* const { data: active } = useLiveQuery<Todo>(client, 'todos', {
|
|
14
|
+
* where: { completed: 0, priority: { $gte: 2 } },
|
|
15
|
+
* orderBy: 'created_at',
|
|
16
|
+
* order: 'desc'
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* 3. Custom query callback (advanced raw SQL / joins):
|
|
20
|
+
* const { data } = useLiveQuery(client, 'todos', (c) => c.db.execute('SELECT ...'));
|
|
21
|
+
*/
|
|
22
|
+
export declare function useLiveQuery<T = any>(client: LocalFirstClient | null | undefined, tableName: string, queryOrOptions?: LiveQueryFn<T> | QueryOptions): {
|
|
23
|
+
data: T[];
|
|
24
|
+
refetch: () => Promise<void>;
|
|
25
|
+
loading: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Hook that tracks the status and pending items in sync_outbox in real time.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useOutboxStatus(client: LocalFirstClient | null | undefined): {
|
|
31
|
+
pendingCount: number;
|
|
32
|
+
pendingRecords: any[];
|
|
33
|
+
refresh: () => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Hook that tracks real-time sync connectivity and sync worker activity (pushing/pulling).
|
|
37
|
+
*/
|
|
38
|
+
export declare function useSyncStatus(client: LocalFirstClient | null | undefined): {
|
|
39
|
+
isOnline: boolean;
|
|
40
|
+
isPushing: boolean;
|
|
41
|
+
isPulling: boolean;
|
|
42
|
+
pendingCount: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Hook that provides real-time sync telemetry and engine performance metrics.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useSyncMetrics(client: LocalFirstClient | null | undefined, pollIntervalMs?: number): {
|
|
48
|
+
metrics: SyncMetrics | null;
|
|
49
|
+
refresh: () => Promise<void>;
|
|
50
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useLiveQuery = useLiveQuery;
|
|
4
|
+
exports.useOutboxStatus = useOutboxStatus;
|
|
5
|
+
exports.useSyncStatus = useSyncStatus;
|
|
6
|
+
exports.useSyncMetrics = useSyncMetrics;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* Hook to execute a live reactive table query that automatically re-runs
|
|
10
|
+
* whenever the table is mutated locally or via incoming remote delta.
|
|
11
|
+
*
|
|
12
|
+
* Supports 3 flexible usage patterns:
|
|
13
|
+
* 1. Zero-SQL (all rows):
|
|
14
|
+
* const { data: todos } = useLiveQuery<Todo>(client, 'todos');
|
|
15
|
+
*
|
|
16
|
+
* 2. Declarative options (filtering with rich operators & sorting without SQL):
|
|
17
|
+
* const { data: active } = useLiveQuery<Todo>(client, 'todos', {
|
|
18
|
+
* where: { completed: 0, priority: { $gte: 2 } },
|
|
19
|
+
* orderBy: 'created_at',
|
|
20
|
+
* order: 'desc'
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* 3. Custom query callback (advanced raw SQL / joins):
|
|
24
|
+
* const { data } = useLiveQuery(client, 'todos', (c) => c.db.execute('SELECT ...'));
|
|
25
|
+
*/
|
|
26
|
+
function useLiveQuery(client, tableName, queryOrOptions) {
|
|
27
|
+
const [data, setData] = (0, react_1.useState)([]);
|
|
28
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
29
|
+
const queryParamRef = (0, react_1.useRef)(queryOrOptions);
|
|
30
|
+
queryParamRef.current = queryOrOptions;
|
|
31
|
+
const refetch = (0, react_1.useCallback)(async () => {
|
|
32
|
+
if (!client || !client.db)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
const param = queryParamRef.current;
|
|
36
|
+
let res;
|
|
37
|
+
if (typeof param === 'function') {
|
|
38
|
+
res = (await param(client)) || [];
|
|
39
|
+
}
|
|
40
|
+
else if (typeof param === 'object' && param !== null) {
|
|
41
|
+
res = await client.findMany(tableName, param);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
res = await client.findMany(tableName);
|
|
45
|
+
}
|
|
46
|
+
setData(res || []);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const msg = String(err?.message || err || '');
|
|
50
|
+
if (msg.includes('no such column') || msg.includes('no such table')) {
|
|
51
|
+
// Table or columns have not been populated yet on fresh database; default to empty list
|
|
52
|
+
setData([]);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.error(`Error in useLiveQuery("${tableName}"):`, err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [client, tableName]);
|
|
62
|
+
(0, react_1.useEffect)(() => {
|
|
63
|
+
if (!client || !client.db)
|
|
64
|
+
return;
|
|
65
|
+
refetch();
|
|
66
|
+
const unsubscribe = client.events.on(tableName, () => {
|
|
67
|
+
refetch();
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
unsubscribe();
|
|
71
|
+
};
|
|
72
|
+
}, [client, tableName, refetch]);
|
|
73
|
+
return { data, refetch, loading };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hook that tracks the status and pending items in sync_outbox in real time.
|
|
77
|
+
*/
|
|
78
|
+
function useOutboxStatus(client) {
|
|
79
|
+
const [pendingCount, setPendingCount] = (0, react_1.useState)(0);
|
|
80
|
+
const [pendingRecords, setPendingRecords] = (0, react_1.useState)([]);
|
|
81
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
82
|
+
if (!client || !client.db)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const records = await client.getOutboxRecords();
|
|
86
|
+
setPendingRecords(records || []);
|
|
87
|
+
setPendingCount(records.filter((r) => r.status === 'PENDING').length);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignored if table doesn't exist yet
|
|
91
|
+
}
|
|
92
|
+
}, [client]);
|
|
93
|
+
(0, react_1.useEffect)(() => {
|
|
94
|
+
if (!client || !client.db)
|
|
95
|
+
return;
|
|
96
|
+
refresh();
|
|
97
|
+
const unsubscribe = client.events.on('sync_outbox', () => {
|
|
98
|
+
refresh();
|
|
99
|
+
});
|
|
100
|
+
return () => {
|
|
101
|
+
unsubscribe();
|
|
102
|
+
};
|
|
103
|
+
}, [client, refresh]);
|
|
104
|
+
return { pendingCount, pendingRecords, refresh };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Hook that tracks real-time sync connectivity and sync worker activity (pushing/pulling).
|
|
108
|
+
*/
|
|
109
|
+
function useSyncStatus(client) {
|
|
110
|
+
const [status, setStatus] = (0, react_1.useState)({
|
|
111
|
+
isOnline: true,
|
|
112
|
+
isPushing: false,
|
|
113
|
+
isPulling: false,
|
|
114
|
+
pendingCount: 0,
|
|
115
|
+
});
|
|
116
|
+
(0, react_1.useEffect)(() => {
|
|
117
|
+
if (!client)
|
|
118
|
+
return;
|
|
119
|
+
// Fetch initial metrics
|
|
120
|
+
client.getMetrics().then((m) => {
|
|
121
|
+
setStatus({
|
|
122
|
+
isOnline: m.isOnline,
|
|
123
|
+
isPushing: m.isPushing,
|
|
124
|
+
isPulling: m.isPulling,
|
|
125
|
+
pendingCount: m.outboxPendingCount,
|
|
126
|
+
});
|
|
127
|
+
}).catch(() => { });
|
|
128
|
+
const unsubscribe = client.events.on('sync:status', (e) => {
|
|
129
|
+
setStatus(e);
|
|
130
|
+
});
|
|
131
|
+
return () => {
|
|
132
|
+
unsubscribe();
|
|
133
|
+
};
|
|
134
|
+
}, [client]);
|
|
135
|
+
return status;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Hook that provides real-time sync telemetry and engine performance metrics.
|
|
139
|
+
*/
|
|
140
|
+
function useSyncMetrics(client, pollIntervalMs = 3000) {
|
|
141
|
+
const [metrics, setMetrics] = (0, react_1.useState)(null);
|
|
142
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
143
|
+
if (!client)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
const data = await client.getMetrics();
|
|
147
|
+
setMetrics(data);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Ignored
|
|
151
|
+
}
|
|
152
|
+
}, [client]);
|
|
153
|
+
(0, react_1.useEffect)(() => {
|
|
154
|
+
if (!client)
|
|
155
|
+
return;
|
|
156
|
+
refresh();
|
|
157
|
+
// Listen to sync events for immediate refresh
|
|
158
|
+
const unsubs = [
|
|
159
|
+
client.events.on('sync:push', () => refresh()),
|
|
160
|
+
client.events.on('sync:pull', () => refresh()),
|
|
161
|
+
client.events.on('sync:status', () => refresh()),
|
|
162
|
+
];
|
|
163
|
+
let timer;
|
|
164
|
+
if (pollIntervalMs > 0) {
|
|
165
|
+
timer = setInterval(refresh, pollIntervalMs);
|
|
166
|
+
}
|
|
167
|
+
return () => {
|
|
168
|
+
unsubs.forEach((u) => u());
|
|
169
|
+
if (timer)
|
|
170
|
+
clearInterval(timer);
|
|
171
|
+
};
|
|
172
|
+
}, [client, refresh, pollIntervalMs]);
|
|
173
|
+
return { metrics, refresh };
|
|
174
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventEmitter = void 0;
|
|
4
|
+
class EventEmitter {
|
|
5
|
+
listeners = new Map();
|
|
6
|
+
on(event, callback) {
|
|
7
|
+
if (!this.listeners.has(event)) {
|
|
8
|
+
this.listeners.set(event, new Set());
|
|
9
|
+
}
|
|
10
|
+
this.listeners.get(event).add(callback);
|
|
11
|
+
return () => {
|
|
12
|
+
const eventSet = this.listeners.get(event);
|
|
13
|
+
if (eventSet) {
|
|
14
|
+
eventSet.delete(callback);
|
|
15
|
+
if (eventSet.size === 0)
|
|
16
|
+
this.listeners.delete(event);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
emit(event, data) {
|
|
21
|
+
const eventSet = this.listeners.get(event);
|
|
22
|
+
if (eventSet) {
|
|
23
|
+
eventSet.forEach(callback => {
|
|
24
|
+
try {
|
|
25
|
+
callback(data);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error("Emitter error:", err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.EventEmitter = EventEmitter;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
2
|
+
export interface CustomLogger {
|
|
3
|
+
debug?(message: string, ...args: any[]): void;
|
|
4
|
+
info?(message: string, ...args: any[]): void;
|
|
5
|
+
warn?(message: string, ...args: any[]): void;
|
|
6
|
+
error?(message: string, ...args: any[]): void;
|
|
7
|
+
}
|
|
8
|
+
export interface LoggerConfig {
|
|
9
|
+
level?: LogLevel;
|
|
10
|
+
customLogger?: CustomLogger;
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class SyncLogger {
|
|
14
|
+
private level;
|
|
15
|
+
private customLogger?;
|
|
16
|
+
private enabled;
|
|
17
|
+
constructor(config?: LoggerConfig | boolean);
|
|
18
|
+
setLevel(level: LogLevel): void;
|
|
19
|
+
setEnabled(enabled: boolean): void;
|
|
20
|
+
setCustomLogger(logger?: CustomLogger): void;
|
|
21
|
+
private shouldLog;
|
|
22
|
+
debug(message: string, ...args: any[]): void;
|
|
23
|
+
info(message: string, ...args: any[]): void;
|
|
24
|
+
warn(message: string, ...args: any[]): void;
|
|
25
|
+
error(message: string, ...args: any[]): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Global default logger instance.
|
|
29
|
+
*/
|
|
30
|
+
export declare const defaultLogger: SyncLogger;
|
|
31
|
+
/**
|
|
32
|
+
* Translates PostgreSQL / Supabase PostgREST error codes into actionable, human-readable explanations.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatSupabaseError(error: any, context?: {
|
|
35
|
+
table?: string;
|
|
36
|
+
operation?: string;
|
|
37
|
+
}): string;
|
|
38
|
+
/**
|
|
39
|
+
* Translates Firestore error codes into human-readable explanations.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatFirestoreError(error: any, context?: {
|
|
42
|
+
collection?: string;
|
|
43
|
+
operation?: string;
|
|
44
|
+
}): string;
|
|
45
|
+
/**
|
|
46
|
+
* Translates Convex error codes into human-readable explanations.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatConvexError(error: any, context?: {
|
|
49
|
+
table?: string;
|
|
50
|
+
operation?: string;
|
|
51
|
+
}): string;
|