@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 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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }
7
+