@pylonsync/react-native 0.3.53
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/package.json +28 -0
- package/src/index.ts +61 -0
- package/src/storage.ts +145 -0
- package/src/useNetworkStatus.ts +41 -0
- package/tsconfig.json +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pylonsync/react-native",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.3.53",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.ts",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json --noEmit",
|
|
12
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@pylonsync/react": "0.3.53",
|
|
16
|
+
"@pylonsync/sdk": "0.3.53",
|
|
17
|
+
"@pylonsync/sync": "0.3.53"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=19",
|
|
21
|
+
"react-native": ">=0.74",
|
|
22
|
+
"@react-native-async-storage/async-storage": ">=1.19",
|
|
23
|
+
"@react-native-community/netinfo": ">=9.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^19.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @pylonsync/react-native
|
|
3
|
+
//
|
|
4
|
+
// Thin layer on top of @pylonsync/react. The hooks, db, useRoom, useSession
|
|
5
|
+
// are all the React versions — RN has React + fetch + WebSocket and shares
|
|
6
|
+
// the rendering layer, so the entire React API works as-is. This package
|
|
7
|
+
// adds:
|
|
8
|
+
//
|
|
9
|
+
// - `init()` that bootstraps an AsyncStorage-backed Storage adapter so
|
|
10
|
+
// the sync engine and React free helpers persist tokens / client_id
|
|
11
|
+
// across cold launches
|
|
12
|
+
// - `useNetworkStatus` — RN-specific NetInfo subscription
|
|
13
|
+
// - `AsyncStoragePersistence` / `OfflineStore` — optional manual cache
|
|
14
|
+
// for apps that want a separate offline layer
|
|
15
|
+
//
|
|
16
|
+
// Why no parallel hook implementations? The previous RN hooks were a
|
|
17
|
+
// stripped-down copy that lost `useSession`, `useShard`, `useInfiniteQuery`,
|
|
18
|
+
// `usePaginatedQuery`, `where` / `orderBy` / `include`. Re-exporting from
|
|
19
|
+
// React keeps the two surfaces in lockstep.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
init as reactInit,
|
|
24
|
+
setReactStorage,
|
|
25
|
+
} from "@pylonsync/react";
|
|
26
|
+
import type { SyncEngineConfig } from "@pylonsync/sync";
|
|
27
|
+
import { createAsyncStorageBridge } from "./storage";
|
|
28
|
+
|
|
29
|
+
// All hooks, db, callFn, configureClient, etc.
|
|
30
|
+
export * from "@pylonsync/react";
|
|
31
|
+
|
|
32
|
+
// React Native specific.
|
|
33
|
+
export { useNetworkStatus } from "./useNetworkStatus";
|
|
34
|
+
export type { NetworkStatus } from "./useNetworkStatus";
|
|
35
|
+
export {
|
|
36
|
+
AsyncStoragePersistence,
|
|
37
|
+
OfflineStore,
|
|
38
|
+
createAsyncStorageBridge,
|
|
39
|
+
type PersistenceAdapter,
|
|
40
|
+
} from "./storage";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize Pylon for a React Native app. Reads the persisted token /
|
|
44
|
+
* client_id from AsyncStorage, registers the bridge with both the sync
|
|
45
|
+
* engine and the React free helpers, then starts sync.
|
|
46
|
+
*
|
|
47
|
+
* Returns a Promise so RN apps can `await init(...)` before rendering —
|
|
48
|
+
* otherwise the first paint would render against an unauthenticated cache.
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* await init({ baseUrl: "https://api.example.com" });
|
|
52
|
+
* AppRegistry.registerComponent("App", () => RootApp);
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export async function init(
|
|
56
|
+
config?: Partial<SyncEngineConfig> & { baseUrl?: string },
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const storage = await createAsyncStorageBridge();
|
|
59
|
+
setReactStorage(storage);
|
|
60
|
+
reactInit({ ...config, storage });
|
|
61
|
+
}
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
|
+
import {
|
|
3
|
+
createWriteThroughStorage,
|
|
4
|
+
type Storage as PylonStorage,
|
|
5
|
+
} from "@pylonsync/sync";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Sync key-value adapter (AsyncStorage-backed)
|
|
9
|
+
//
|
|
10
|
+
// Pylon's sync engine uses a *synchronous* Storage interface for hot-path
|
|
11
|
+
// state (auth token, client_id) — async would force the engine to be
|
|
12
|
+
// async-all-the-way-down. AsyncStorage is async. The bridge: read seeds
|
|
13
|
+
// into memory once at startup, then write through to AsyncStorage in the
|
|
14
|
+
// background. Reads are immediate, writes are eventually-consistent.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hot-path keys we eagerly read into memory at boot. Anything outside this
|
|
19
|
+
* set is missed until the engine writes to it (which then immediately
|
|
20
|
+
* mirrors to AsyncStorage). Add to this list if a new sync-engine-internal
|
|
21
|
+
* key shows up.
|
|
22
|
+
*/
|
|
23
|
+
const HOT_KEYS = [
|
|
24
|
+
"pylon:client_id",
|
|
25
|
+
"pylon_token",
|
|
26
|
+
// Wildcards aren't supported here; namespace-aware pylon apps register
|
|
27
|
+
// keys via `init({ appName })` before the first request, which prefixes
|
|
28
|
+
// tokens as `pylon:<app>:token`. The seed loader scans for matches.
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a sync-engine-compatible Storage adapter on top of AsyncStorage.
|
|
33
|
+
* Reads return immediately from an in-memory cache seeded from AsyncStorage.
|
|
34
|
+
* Writes update the cache and fire-and-forget the AsyncStorage write.
|
|
35
|
+
*
|
|
36
|
+
* Call before `init()` so the SyncEngine constructor can read the cached
|
|
37
|
+
* client_id / token without a Promise hop.
|
|
38
|
+
*/
|
|
39
|
+
export async function createAsyncStorageBridge(): Promise<PylonStorage> {
|
|
40
|
+
// Pull in any existing pylon keys so a cold launch re-uses the same
|
|
41
|
+
// client_id (so the server's per-client diagnostics line up across
|
|
42
|
+
// sessions) and the same token (so the user stays signed in).
|
|
43
|
+
const allKeys = await AsyncStorage.getAllKeys();
|
|
44
|
+
const pylonKeys = allKeys.filter(
|
|
45
|
+
(k) => k.startsWith("pylon_") || k.startsWith("pylon:"),
|
|
46
|
+
);
|
|
47
|
+
const merged = [...new Set([...HOT_KEYS, ...pylonKeys])];
|
|
48
|
+
const seed: Record<string, string> = {};
|
|
49
|
+
if (merged.length > 0) {
|
|
50
|
+
const values = await Promise.all(merged.map((k) => AsyncStorage.getItem(k)));
|
|
51
|
+
for (let i = 0; i < merged.length; i++) {
|
|
52
|
+
const v = values[i];
|
|
53
|
+
if (v != null) seed[merged[i]] = v;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return createWriteThroughStorage(seed, (key, value) => {
|
|
57
|
+
if (value === null) {
|
|
58
|
+
void AsyncStorage.removeItem(key);
|
|
59
|
+
} else {
|
|
60
|
+
void AsyncStorage.setItem(key, value);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Optional higher-level offline-row store
|
|
67
|
+
//
|
|
68
|
+
// Kept for apps that want a separate manual cache (e.g. offline mutation
|
|
69
|
+
// queue persistence outside the sync engine's IndexedDB equivalent).
|
|
70
|
+
// SyncEngine handles the common case automatically once given a Storage.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export interface PersistenceAdapter {
|
|
74
|
+
get(key: string): Promise<string | null>;
|
|
75
|
+
set(key: string, value: string): Promise<void>;
|
|
76
|
+
remove(key: string): Promise<void>;
|
|
77
|
+
keys(): Promise<string[]>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class AsyncStoragePersistence implements PersistenceAdapter {
|
|
81
|
+
private prefix: string;
|
|
82
|
+
|
|
83
|
+
constructor(prefix = "pylon") {
|
|
84
|
+
this.prefix = prefix;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private prefixedKey(key: string): string {
|
|
88
|
+
return `${this.prefix}:${key}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async get(key: string): Promise<string | null> {
|
|
92
|
+
return AsyncStorage.getItem(this.prefixedKey(key));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async set(key: string, value: string): Promise<void> {
|
|
96
|
+
await AsyncStorage.setItem(this.prefixedKey(key), value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async remove(key: string): Promise<void> {
|
|
100
|
+
await AsyncStorage.removeItem(this.prefixedKey(key));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async keys(): Promise<string[]> {
|
|
104
|
+
const allKeys = await AsyncStorage.getAllKeys();
|
|
105
|
+
const prefixWithSep = `${this.prefix}:`;
|
|
106
|
+
return allKeys
|
|
107
|
+
.filter((k) => k.startsWith(prefixWithSep))
|
|
108
|
+
.map((k) => k.slice(prefixWithSep.length));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class OfflineStore {
|
|
113
|
+
private adapter: PersistenceAdapter;
|
|
114
|
+
|
|
115
|
+
constructor(adapter?: PersistenceAdapter) {
|
|
116
|
+
this.adapter = adapter ?? new AsyncStoragePersistence();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async saveEntities(
|
|
120
|
+
entity: string,
|
|
121
|
+
rows: Record<string, unknown>[],
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
await this.adapter.set(`entities:${entity}`, JSON.stringify(rows));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async loadEntities(
|
|
127
|
+
entity: string,
|
|
128
|
+
): Promise<Record<string, unknown>[]> {
|
|
129
|
+
const data = await this.adapter.get(`entities:${entity}`);
|
|
130
|
+
return data ? (JSON.parse(data) as Record<string, unknown>[]) : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async saveCursor(cursor: string): Promise<void> {
|
|
134
|
+
await this.adapter.set("sync:cursor", cursor);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async loadCursor(): Promise<string | null> {
|
|
138
|
+
return this.adapter.get("sync:cursor");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async clear(): Promise<void> {
|
|
142
|
+
const keys = await this.adapter.keys();
|
|
143
|
+
await Promise.all(keys.map((key) => this.adapter.remove(key)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import NetInfo, { type NetInfoState } from "@react-native-community/netinfo";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Network status hook
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export interface NetworkStatus {
|
|
9
|
+
/** Whether the device currently has internet connectivity. */
|
|
10
|
+
isOnline: boolean;
|
|
11
|
+
/** Connection type reported by NetInfo (e.g. "wifi", "cellular", "none"). */
|
|
12
|
+
connectionType: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe to network connectivity changes. Useful for toggling offline
|
|
17
|
+
* banners or pausing sync when the device goes offline.
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { isOnline, connectionType } = useNetworkStatus();
|
|
21
|
+
* if (!isOnline) return <OfflineBanner />;
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useNetworkStatus(): NetworkStatus {
|
|
25
|
+
const [isOnline, setIsOnline] = useState(true);
|
|
26
|
+
const [connectionType, setConnectionType] = useState<string>("unknown");
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handleChange = (state: NetInfoState) => {
|
|
30
|
+
setIsOnline(state.isConnected ?? false);
|
|
31
|
+
setConnectionType(state.type);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const unsubscribe = NetInfo.addEventListener(handleChange);
|
|
35
|
+
return () => {
|
|
36
|
+
unsubscribe();
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return { isOnline, connectionType };
|
|
41
|
+
}
|