@replanejs/react 0.8.20 → 0.9.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/README.md +60 -30
- package/dist/index.cjs +162 -151
- package/dist/index.d.cts +24 -29
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +24 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -153
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,17 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { Replane, Replane as Replane$1, ReplaneError, ReplaneErrorCode, getReplaneSnapshot } from "@replanejs/sdk";
|
|
6
6
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
7
7
|
|
|
8
8
|
//#region src/context.ts
|
|
9
9
|
const ReplaneContext = createContext(null);
|
|
10
10
|
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/version.ts
|
|
13
|
-
const VERSION = "0.8.20";
|
|
14
|
-
const DEFAULT_AGENT = `replane-js-react/${VERSION}`;
|
|
15
|
-
|
|
16
11
|
//#endregion
|
|
17
12
|
//#region src/useReplaneClient.ts
|
|
18
13
|
const suspenseCache = new Map();
|
|
@@ -20,10 +15,23 @@ function getCacheKey(options) {
|
|
|
20
15
|
return `${options.baseUrl}:${options.sdkKey}`;
|
|
21
16
|
}
|
|
22
17
|
/**
|
|
23
|
-
*
|
|
18
|
+
* Creates a Replane client and connects it.
|
|
19
|
+
*/
|
|
20
|
+
async function createAndConnectClient(options, connection) {
|
|
21
|
+
const client = new Replane$1({
|
|
22
|
+
logger: options.logger,
|
|
23
|
+
context: options.context,
|
|
24
|
+
defaults: options.defaults
|
|
25
|
+
});
|
|
26
|
+
if (!connection) return client;
|
|
27
|
+
await client.connect(connection);
|
|
28
|
+
return client;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hook to manage Replane client creation internally.
|
|
24
32
|
* Handles loading state and cleanup.
|
|
25
33
|
*/
|
|
26
|
-
function useReplaneClientInternal(options) {
|
|
34
|
+
function useReplaneClientInternal(options, originalConnection) {
|
|
27
35
|
const [state, setState] = useState({
|
|
28
36
|
status: "loading",
|
|
29
37
|
client: null,
|
|
@@ -31,16 +39,15 @@ function useReplaneClientInternal(options) {
|
|
|
31
39
|
});
|
|
32
40
|
const clientRef = useRef(null);
|
|
33
41
|
const optionsRef = useRef(options);
|
|
42
|
+
const connectionJson = JSON.stringify(originalConnection);
|
|
34
43
|
useEffect(() => {
|
|
44
|
+
const connection = JSON.parse(connectionJson);
|
|
35
45
|
let cancelled = false;
|
|
36
46
|
async function initClient() {
|
|
37
47
|
try {
|
|
38
|
-
const client = await
|
|
39
|
-
...optionsRef.current,
|
|
40
|
-
agent: optionsRef.current.agent ?? DEFAULT_AGENT
|
|
41
|
-
});
|
|
48
|
+
const client = await createAndConnectClient(optionsRef.current, connection);
|
|
42
49
|
if (cancelled) {
|
|
43
|
-
client.
|
|
50
|
+
client.disconnect();
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
clientRef.current = client;
|
|
@@ -63,29 +70,26 @@ function useReplaneClientInternal(options) {
|
|
|
63
70
|
return () => {
|
|
64
71
|
cancelled = true;
|
|
65
72
|
if (clientRef.current) {
|
|
66
|
-
clientRef.current.
|
|
73
|
+
clientRef.current.disconnect();
|
|
67
74
|
clientRef.current = null;
|
|
68
75
|
}
|
|
69
76
|
};
|
|
70
|
-
}, []);
|
|
77
|
+
}, [connectionJson]);
|
|
71
78
|
return state;
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* Hook for Suspense-based client creation.
|
|
75
82
|
* Throws a promise while loading, throws error on failure.
|
|
76
83
|
*/
|
|
77
|
-
function useReplaneClientSuspense(options) {
|
|
78
|
-
const cacheKey = getCacheKey(
|
|
84
|
+
function useReplaneClientSuspense(options, connection) {
|
|
85
|
+
const cacheKey = getCacheKey(connection);
|
|
79
86
|
const cached = suspenseCache.get(cacheKey);
|
|
80
87
|
if (cached) {
|
|
81
88
|
if (cached.error) throw cached.error;
|
|
82
89
|
if (cached.result) return cached.result;
|
|
83
90
|
throw cached.promise;
|
|
84
91
|
}
|
|
85
|
-
const promise =
|
|
86
|
-
...options,
|
|
87
|
-
agent: options.agent ?? DEFAULT_AGENT
|
|
88
|
-
}).then((client) => {
|
|
92
|
+
const promise = createAndConnectClient(options, connection).then((client) => {
|
|
89
93
|
const entry = suspenseCache.get(cacheKey);
|
|
90
94
|
if (entry) entry.result = client;
|
|
91
95
|
return client;
|
|
@@ -98,150 +102,59 @@ function useReplaneClientSuspense(options) {
|
|
|
98
102
|
throw promise;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
|
-
* Clear the suspense cache for a specific
|
|
105
|
+
* Clear the suspense cache for a specific connection configuration.
|
|
102
106
|
* Useful for testing or when you need to force re-initialization.
|
|
103
107
|
*/
|
|
104
|
-
function clearSuspenseCache(
|
|
105
|
-
if (
|
|
108
|
+
function clearSuspenseCache(connection) {
|
|
109
|
+
if (connection) suspenseCache.delete(getCacheKey(connection));
|
|
106
110
|
else suspenseCache.clear();
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
//#endregion
|
|
110
|
-
//#region src/hooks.ts
|
|
111
|
-
function useReplane() {
|
|
112
|
-
const context = useContext(ReplaneContext);
|
|
113
|
-
if (!context) throw new Error("useReplane must be used within a ReplaneProvider");
|
|
114
|
-
return context.client;
|
|
115
|
-
}
|
|
116
|
-
function useConfig(name, options) {
|
|
117
|
-
const client = useReplane();
|
|
118
|
-
const subscribe = useCallback((callback) => {
|
|
119
|
-
return client.subscribe(name, callback);
|
|
120
|
-
}, [client, name]);
|
|
121
|
-
const get = useCallback(() => {
|
|
122
|
-
return client.get(name, options);
|
|
123
|
-
}, [
|
|
124
|
-
client,
|
|
125
|
-
name,
|
|
126
|
-
options
|
|
127
|
-
]);
|
|
128
|
-
const value = useSyncExternalStore(subscribe, get, get);
|
|
129
|
-
return value;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Creates a typed version of useReplane hook.
|
|
133
|
-
*
|
|
134
|
-
* @example
|
|
135
|
-
* ```tsx
|
|
136
|
-
* interface AppConfigs {
|
|
137
|
-
* theme: { darkMode: boolean };
|
|
138
|
-
* features: { beta: boolean };
|
|
139
|
-
* }
|
|
140
|
-
*
|
|
141
|
-
* const useAppReplane = createReplaneHook<AppConfigs>();
|
|
142
|
-
*
|
|
143
|
-
* function MyComponent() {
|
|
144
|
-
* const replane = useAppReplane();
|
|
145
|
-
* // replane.get("theme") returns { darkMode: boolean }
|
|
146
|
-
* }
|
|
147
|
-
* ```
|
|
148
|
-
*/
|
|
149
|
-
function createReplaneHook() {
|
|
150
|
-
return function useTypedReplane() {
|
|
151
|
-
return useReplane();
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Creates a typed version of useConfig hook.
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```tsx
|
|
159
|
-
* interface AppConfigs {
|
|
160
|
-
* theme: { darkMode: boolean };
|
|
161
|
-
* features: { beta: boolean };
|
|
162
|
-
* }
|
|
163
|
-
*
|
|
164
|
-
* const useAppConfig = createConfigHook<AppConfigs>();
|
|
165
|
-
*
|
|
166
|
-
* function MyComponent() {
|
|
167
|
-
* const theme = useAppConfig("theme");
|
|
168
|
-
* // theme is typed as { darkMode: boolean }
|
|
169
|
-
* }
|
|
170
|
-
* ```
|
|
171
|
-
*/
|
|
172
|
-
function createConfigHook() {
|
|
173
|
-
return function useTypedConfig(name, options) {
|
|
174
|
-
return useConfig(String(name), options);
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Hook for creating stateful resources with cleanup support.
|
|
179
|
-
* Unlike useMemo, this guarantees cleanup when dependencies change or on unmount.
|
|
180
|
-
*
|
|
181
|
-
* @param factory - Function that creates the resource
|
|
182
|
-
* @param cleanup - Function that cleans up the resource
|
|
183
|
-
* @param deps - Dependencies array (resource is recreated when these change)
|
|
184
|
-
*/
|
|
185
|
-
function useStateful(factory, cleanup, deps) {
|
|
186
|
-
const valueRef = useRef(null);
|
|
187
|
-
const initializedRef = useRef(false);
|
|
188
|
-
if (!initializedRef.current) {
|
|
189
|
-
valueRef.current = factory();
|
|
190
|
-
initializedRef.current = true;
|
|
191
|
-
}
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (valueRef.current === null) valueRef.current = factory();
|
|
194
|
-
return () => {
|
|
195
|
-
if (valueRef.current !== null) {
|
|
196
|
-
cleanup(valueRef.current);
|
|
197
|
-
valueRef.current = null;
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
}, deps);
|
|
201
|
-
return valueRef.current;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
113
|
//#endregion
|
|
205
114
|
//#region src/types.ts
|
|
206
115
|
/**
|
|
207
116
|
* Type guard to check if props contain a pre-created client.
|
|
208
117
|
*/
|
|
209
118
|
function hasClient(props) {
|
|
210
|
-
return "client" in props && props.client
|
|
119
|
+
return "client" in props && !!props.client;
|
|
211
120
|
}
|
|
212
121
|
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/version.ts
|
|
124
|
+
const VERSION = "0.9.2";
|
|
125
|
+
const DEFAULT_AGENT = `replane-js-react/${VERSION}`;
|
|
126
|
+
|
|
213
127
|
//#endregion
|
|
214
128
|
//#region src/provider.tsx
|
|
215
129
|
/**
|
|
216
130
|
* Internal provider component for pre-created client.
|
|
217
131
|
*/
|
|
218
132
|
function ReplaneProviderWithClient({ client, children }) {
|
|
219
|
-
const value = useMemo(() => ({ client }), [client]);
|
|
133
|
+
const value = useMemo(() => ({ replane: client }), [client]);
|
|
220
134
|
return /* @__PURE__ */ jsx(ReplaneContext.Provider, {
|
|
221
135
|
value,
|
|
222
136
|
children
|
|
223
137
|
});
|
|
224
138
|
}
|
|
225
139
|
/**
|
|
226
|
-
* Internal provider component for
|
|
227
|
-
*
|
|
140
|
+
* Internal provider component for creating a Replane client asynchronously.
|
|
141
|
+
* Creates a Replane client synchronously and connects in background.
|
|
228
142
|
*/
|
|
229
|
-
function
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const value = useMemo(() => ({ client }), [client]);
|
|
143
|
+
function AsyncReplaneProvider({ children, connection,...options }) {
|
|
144
|
+
const replaneRef = useRef(void 0);
|
|
145
|
+
if (!replaneRef.current) replaneRef.current = new Replane$1(options);
|
|
146
|
+
const connectionJson = connection ? JSON.stringify(connection) : void 0;
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const parsedConnection = connectionJson ? JSON.parse(connectionJson) : void 0;
|
|
149
|
+
if (!parsedConnection) return;
|
|
150
|
+
replaneRef.current.connect(parsedConnection).catch((err) => {
|
|
151
|
+
(options.logger ?? console)?.error("Failed to connect Replane client", err);
|
|
152
|
+
});
|
|
153
|
+
return () => {
|
|
154
|
+
replaneRef.current.disconnect();
|
|
155
|
+
};
|
|
156
|
+
}, [connectionJson, options.logger]);
|
|
157
|
+
const value = useMemo(() => ({ replane: replaneRef.current }), []);
|
|
245
158
|
return /* @__PURE__ */ jsx(ReplaneContext.Provider, {
|
|
246
159
|
value,
|
|
247
160
|
children
|
|
@@ -251,11 +164,12 @@ function ReplaneProviderWithSnapshot({ options, snapshot, children }) {
|
|
|
251
164
|
* Internal provider component for options-based client creation (non-suspense).
|
|
252
165
|
* Throws errors during rendering so they can be caught by Error Boundaries.
|
|
253
166
|
*/
|
|
254
|
-
function
|
|
255
|
-
|
|
167
|
+
function LoaderReplaneProvider({ children, loader, connection,...options }) {
|
|
168
|
+
if (!connection) throw new Error("Connection is required when using Loader");
|
|
169
|
+
const state = useReplaneClientInternal(options, connection);
|
|
256
170
|
if (state.status === "loading") return /* @__PURE__ */ jsx(Fragment, { children: loader ?? null });
|
|
257
171
|
if (state.status === "error") throw state.error;
|
|
258
|
-
const value = {
|
|
172
|
+
const value = { replane: state.client };
|
|
259
173
|
return /* @__PURE__ */ jsx(ReplaneContext.Provider, {
|
|
260
174
|
value,
|
|
261
175
|
children
|
|
@@ -264,22 +178,24 @@ function ReplaneProviderWithOptions({ options, children, loader }) {
|
|
|
264
178
|
/**
|
|
265
179
|
* Internal provider component for options-based client creation with Suspense.
|
|
266
180
|
*/
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
const
|
|
181
|
+
function SuspenseReplaneProvider({ connection, children,...options }) {
|
|
182
|
+
if (!connection) throw new Error("Connection is required when using Suspense");
|
|
183
|
+
const client = useReplaneClientSuspense(options, connection);
|
|
184
|
+
const value = useMemo(() => ({ replane: client }), [client]);
|
|
270
185
|
return /* @__PURE__ */ jsx(ReplaneContext.Provider, {
|
|
271
186
|
value,
|
|
272
187
|
children
|
|
273
188
|
});
|
|
274
189
|
}
|
|
275
190
|
/**
|
|
276
|
-
* Provider component that makes a
|
|
191
|
+
* Provider component that makes a Replane client available to the component tree.
|
|
277
192
|
*
|
|
278
|
-
* Can be used in
|
|
193
|
+
* Can be used in several ways:
|
|
279
194
|
*
|
|
280
195
|
* 1. With a pre-created client:
|
|
281
196
|
* ```tsx
|
|
282
|
-
* const client =
|
|
197
|
+
* const client = new Replane({ defaults: { ... } });
|
|
198
|
+
* await client.connect({ baseUrl: '...', sdkKey: '...' });
|
|
283
199
|
* <ReplaneProvider client={client}>
|
|
284
200
|
* <App />
|
|
285
201
|
* </ReplaneProvider>
|
|
@@ -329,15 +245,92 @@ function ReplaneProviderWithSuspense({ options, children }) {
|
|
|
329
245
|
* allowing them to be caught by React Error Boundaries.
|
|
330
246
|
*/
|
|
331
247
|
function ReplaneProvider(props) {
|
|
248
|
+
const originalConnection = props.connection;
|
|
249
|
+
const connection = useMemo(() => originalConnection ? {
|
|
250
|
+
...originalConnection,
|
|
251
|
+
agent: originalConnection.agent ?? DEFAULT_AGENT
|
|
252
|
+
} : void 0, [originalConnection]);
|
|
332
253
|
if (hasClient(props)) return /* @__PURE__ */ jsx(ReplaneProviderWithClient, { ...props });
|
|
333
|
-
if (props.snapshot) return /* @__PURE__ */ jsx(
|
|
254
|
+
if (props.snapshot || !connection || props.async) return /* @__PURE__ */ jsx(AsyncReplaneProvider, { ...props });
|
|
255
|
+
if (props.suspense) return /* @__PURE__ */ jsx(SuspenseReplaneProvider, {
|
|
334
256
|
...props,
|
|
335
|
-
|
|
257
|
+
connection
|
|
336
258
|
});
|
|
337
|
-
|
|
338
|
-
|
|
259
|
+
return /* @__PURE__ */ jsx(LoaderReplaneProvider, {
|
|
260
|
+
...props,
|
|
261
|
+
connection
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/hooks.ts
|
|
267
|
+
function useReplane() {
|
|
268
|
+
const context = useContext(ReplaneContext);
|
|
269
|
+
if (!context) throw new Error("useReplane must be used within a ReplaneProvider");
|
|
270
|
+
return context.replane;
|
|
271
|
+
}
|
|
272
|
+
function useConfig(name, options) {
|
|
273
|
+
const client = useReplane();
|
|
274
|
+
const subscribe = useCallback((callback) => {
|
|
275
|
+
return client.subscribe(name, callback);
|
|
276
|
+
}, [client, name]);
|
|
277
|
+
const get = useCallback(() => {
|
|
278
|
+
return client.get(name, options);
|
|
279
|
+
}, [
|
|
280
|
+
client,
|
|
281
|
+
name,
|
|
282
|
+
options
|
|
283
|
+
]);
|
|
284
|
+
const value = useSyncExternalStore(subscribe, get, get);
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Creates a typed version of useReplane hook.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```tsx
|
|
292
|
+
* interface AppConfigs {
|
|
293
|
+
* theme: { darkMode: boolean };
|
|
294
|
+
* features: { beta: boolean };
|
|
295
|
+
* }
|
|
296
|
+
*
|
|
297
|
+
* const useAppReplane = createReplaneHook<AppConfigs>();
|
|
298
|
+
*
|
|
299
|
+
* function MyComponent() {
|
|
300
|
+
* const replane = useAppReplane();
|
|
301
|
+
* // replane.get("theme") returns { darkMode: boolean }
|
|
302
|
+
* }
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
function createReplaneHook() {
|
|
306
|
+
return function useTypedReplane() {
|
|
307
|
+
return useReplane();
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Creates a typed version of useConfig hook.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```tsx
|
|
315
|
+
* interface AppConfigs {
|
|
316
|
+
* theme: { darkMode: boolean };
|
|
317
|
+
* features: { beta: boolean };
|
|
318
|
+
* }
|
|
319
|
+
*
|
|
320
|
+
* const useAppConfig = createConfigHook<AppConfigs>();
|
|
321
|
+
*
|
|
322
|
+
* function MyComponent() {
|
|
323
|
+
* const theme = useAppConfig("theme");
|
|
324
|
+
* // theme is typed as { darkMode: boolean }
|
|
325
|
+
* }
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
function createConfigHook() {
|
|
329
|
+
return function useTypedConfig(name, options) {
|
|
330
|
+
return useConfig(String(name), options);
|
|
331
|
+
};
|
|
339
332
|
}
|
|
340
333
|
|
|
341
334
|
//#endregion
|
|
342
|
-
export { ReplaneProvider, clearSuspenseCache, createConfigHook, createReplaneHook, getReplaneSnapshot, useConfig, useReplane };
|
|
335
|
+
export { Replane, ReplaneError, ReplaneErrorCode, ReplaneProvider, clearSuspenseCache, createConfigHook, createReplaneHook, getReplaneSnapshot, useConfig, useReplane };
|
|
343
336
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["options: ReplaneClientOptions<T>","err: unknown","options?: ReplaneClientOptions<T>","name: string","options?: GetConfigOptions<T>","callback: () => void","name: K","options?: GetConfigOptions<TConfigs[K]>","factory: () => T","cleanup: (value: T) => void","deps: React.DependencyList","props: ReplaneProviderProps<T>","value: ReplaneContextValue<T>","props: ReplaneProviderProps<T>"],"sources":["../src/context.ts","../src/version.ts","../src/useReplaneClient.ts","../src/hooks.ts","../src/types.ts","../src/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext } from \"react\";\nimport type { ReplaneContextValue } from \"./types\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const ReplaneContext = createContext<ReplaneContextValue<any> | null>(null);\n","// Auto-generated - do not edit manually\nexport const VERSION = \"0.8.20\";\nexport const DEFAULT_AGENT = `replane-js-react/${VERSION}`;\n","\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { createReplaneClient } from \"@replanejs/sdk\";\nimport type { ReplaneClient, ReplaneClientOptions } from \"@replanejs/sdk\";\nimport { DEFAULT_AGENT } from \"./version\";\n\ntype ClientState<T extends object> =\n | { status: \"loading\"; client: null; error: null }\n | { status: \"ready\"; client: ReplaneClient<T>; error: null }\n | { status: \"error\"; client: null; error: Error };\n\n// Cache for suspense promise tracking\nconst suspenseCache = new Map<\n string,\n {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n promise: Promise<ReplaneClient<any>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result?: ReplaneClient<any>;\n error?: Error;\n }\n>();\n\nfunction getCacheKey<T extends object>(options: ReplaneClientOptions<T>): string {\n return `${options.baseUrl}:${options.sdkKey}`;\n}\n\ntype ErrorConstructor = new (message: string, options?: { cause?: unknown }) => Error;\n\n/**\n * Hook to manage ReplaneClient creation internally.\n * Handles loading state and cleanup.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientInternal<T extends object = any>(\n options: ReplaneClientOptions<T>\n): ClientState<T> {\n const [state, setState] = useState<ClientState<T>>({\n status: \"loading\",\n client: null,\n error: null,\n });\n const clientRef = useRef<ReplaneClient<T> | null>(null);\n const optionsRef = useRef(options);\n\n useEffect(() => {\n let cancelled = false;\n\n async function initClient() {\n try {\n const client = await createReplaneClient<T>({\n ...optionsRef.current,\n agent: optionsRef.current.agent ?? DEFAULT_AGENT,\n });\n if (cancelled) {\n client.close();\n return;\n }\n clientRef.current = client;\n setState({ status: \"ready\", client, error: null });\n } catch (err) {\n if (cancelled) return;\n const error =\n err instanceof Error ? err : new (Error as ErrorConstructor)(String(err), { cause: err });\n setState({ status: \"error\", client: null, error });\n }\n }\n\n initClient();\n\n return () => {\n cancelled = true;\n if (clientRef.current) {\n clientRef.current.close();\n clientRef.current = null;\n }\n };\n }, []);\n\n return state;\n}\n\n/**\n * Hook for Suspense-based client creation.\n * Throws a promise while loading, throws error on failure.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientSuspense<T extends object = any>(\n options: ReplaneClientOptions<T>\n): ReplaneClient<T> {\n const cacheKey = getCacheKey(options);\n const cached = suspenseCache.get(cacheKey);\n\n if (cached) {\n if (cached.error) {\n throw cached.error;\n }\n if (cached.result) {\n return cached.result as ReplaneClient<T>;\n }\n // Still loading, throw the promise\n throw cached.promise;\n }\n\n // First time - create the promise\n const promise = createReplaneClient<T>({\n ...options,\n agent: options.agent ?? DEFAULT_AGENT,\n })\n .then((client) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.result = client;\n }\n return client;\n })\n .catch((err: unknown) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.error = err instanceof Error ? err : new Error(String(err));\n }\n throw err;\n });\n\n suspenseCache.set(cacheKey, { promise });\n throw promise;\n}\n\n/**\n * Clear the suspense cache for a specific options configuration.\n * Useful for testing or when you need to force re-initialization.\n */\nexport function clearSuspenseCache<T extends object>(options?: ReplaneClientOptions<T>): void {\n if (options) {\n suspenseCache.delete(getCacheKey(options));\n } else {\n suspenseCache.clear();\n }\n}\n","\"use client\";\n\nimport { useCallback, useContext, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport { ReplaneContext } from \"./context\";\nimport type { UntypedReplaneConfig } from \"./types\";\nimport type { ReplaneClient, GetConfigOptions } from \"@replanejs/sdk\";\n\nexport function useReplane<T extends object = UntypedReplaneConfig>(): ReplaneClient<T> {\n const context = useContext(ReplaneContext);\n if (!context) {\n throw new Error(\"useReplane must be used within a ReplaneProvider\");\n }\n return context.client as ReplaneClient<T>;\n}\n\nexport function useConfig<T>(name: string, options?: GetConfigOptions<T>): T {\n const client = useReplane();\n\n const subscribe = useCallback(\n (callback: () => void) => {\n return client.subscribe(name, callback);\n },\n [client, name]\n );\n\n const get = useCallback(() => {\n return client.get(name, options) as T;\n }, [client, name, options]);\n\n const value = useSyncExternalStore(subscribe, get, get);\n\n return value;\n}\n\n/**\n * Creates a typed version of useReplane hook.\n *\n * @example\n * ```tsx\n * interface AppConfigs {\n * theme: { darkMode: boolean };\n * features: { beta: boolean };\n * }\n *\n * const useAppReplane = createReplaneHook<AppConfigs>();\n *\n * function MyComponent() {\n * const replane = useAppReplane();\n * // replane.get(\"theme\") returns { darkMode: boolean }\n * }\n * ```\n */\nexport function createReplaneHook<TConfigs extends object>() {\n return function useTypedReplane(): ReplaneClient<TConfigs> {\n return useReplane<TConfigs>();\n };\n}\n\n/**\n * Creates a typed version of useConfig hook.\n *\n * @example\n * ```tsx\n * interface AppConfigs {\n * theme: { darkMode: boolean };\n * features: { beta: boolean };\n * }\n *\n * const useAppConfig = createConfigHook<AppConfigs>();\n *\n * function MyComponent() {\n * const theme = useAppConfig(\"theme\");\n * // theme is typed as { darkMode: boolean }\n * }\n * ```\n */\nexport function createConfigHook<TConfigs extends object>() {\n return function useTypedConfig<K extends keyof TConfigs>(\n name: K,\n options?: GetConfigOptions<TConfigs[K]>\n ): TConfigs[K] {\n return useConfig<TConfigs[K]>(String(name), options);\n };\n}\n\n/**\n * Hook for creating stateful resources with cleanup support.\n * Unlike useMemo, this guarantees cleanup when dependencies change or on unmount.\n *\n * @param factory - Function that creates the resource\n * @param cleanup - Function that cleans up the resource\n * @param deps - Dependencies array (resource is recreated when these change)\n */\nexport function useStateful<T>(\n factory: () => T,\n cleanup: (value: T) => void,\n deps: React.DependencyList\n): T {\n const valueRef = useRef<T | null>(null);\n const initializedRef = useRef(false);\n\n // Create initial value synchronously on first render\n if (!initializedRef.current) {\n valueRef.current = factory();\n initializedRef.current = true;\n }\n\n useEffect(() => {\n // On mount or deps change, we may need to recreate\n // If this is not the initial mount, recreate the value\n if (valueRef.current === null) {\n valueRef.current = factory();\n }\n\n return () => {\n if (valueRef.current !== null) {\n cleanup(valueRef.current);\n valueRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n return valueRef.current as T;\n}\n","import type {\n ReplaneClient,\n ReplaneClientOptions,\n ReplaneSnapshot,\n} from \"@replanejs/sdk\";\nimport type { ReactNode } from \"react\";\n\nexport type UntypedReplaneConfig = Record<string, unknown>;\n\nexport interface ReplaneContextValue<T extends object = UntypedReplaneConfig> {\n client: ReplaneClient<T>;\n}\n\n/**\n * Props for ReplaneProvider when using a pre-created client.\n */\nexport interface ReplaneProviderWithClientProps<T extends object = UntypedReplaneConfig> {\n /** Pre-created ReplaneClient instance */\n client: ReplaneClient<T>;\n children: ReactNode;\n}\n\n/**\n * Props for ReplaneProvider when letting it manage the client internally.\n */\nexport interface ReplaneProviderWithOptionsProps<T extends object = UntypedReplaneConfig> {\n /** Options to create or restore the ReplaneClient */\n options: ReplaneClientOptions<T>;\n children: ReactNode;\n /**\n * Optional snapshot from server-side rendering.\n * When provided, the client will be restored from the snapshot synchronously\n * instead of fetching configs from the server.\n * The `options` will be used for live updates connection if provided.\n */\n snapshot?: ReplaneSnapshot<T>;\n /**\n * Optional loading component to show while the client is initializing.\n * If not provided and suspense is false/undefined, children will not render until ready.\n * Ignored when snapshot is provided (restoration is synchronous).\n */\n loader?: ReactNode;\n /**\n * If true, uses React Suspense for loading state.\n * The provider will throw a promise that Suspense can catch.\n * Ignored when snapshot is provided (restoration is synchronous).\n * @default false\n */\n suspense?: boolean;\n}\n\nexport type ReplaneProviderProps<T extends object = UntypedReplaneConfig> =\n | ReplaneProviderWithClientProps<T>\n | ReplaneProviderWithOptionsProps<T>;\n\n/**\n * Type guard to check if props contain a pre-created client.\n */\nexport function hasClient<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithClientProps<T> {\n return \"client\" in props && props.client !== undefined;\n}\n\n/**\n * Type guard to check if props contain options (with or without snapshot).\n */\nexport function hasOptions<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithOptionsProps<T> {\n return \"options\" in props && props.options !== undefined;\n}\n","\"use client\";\n\nimport { useMemo } from \"react\";\nimport { restoreReplaneClient } from \"@replanejs/sdk\";\nimport { ReplaneContext } from \"./context\";\nimport { useReplaneClientInternal, useReplaneClientSuspense } from \"./useReplaneClient\";\nimport { useStateful } from \"./hooks\";\nimport type {\n ReplaneProviderProps,\n ReplaneProviderWithClientProps,\n ReplaneProviderWithOptionsProps,\n ReplaneContextValue,\n} from \"./types\";\nimport { hasClient } from \"./types\";\nimport { DEFAULT_AGENT } from \"./version\";\n\n/**\n * Internal provider component for pre-created client.\n */\nfunction ReplaneProviderWithClient<T extends object>({\n client,\n children,\n}: ReplaneProviderWithClientProps<T>) {\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for restoring client from snapshot.\n * Uses restoreReplaneClient which is synchronous.\n */\nfunction ReplaneProviderWithSnapshot<T extends object>({\n options,\n snapshot,\n children,\n}: ReplaneProviderWithOptionsProps<T> & {\n snapshot: NonNullable<ReplaneProviderWithOptionsProps<T>[\"snapshot\"]>;\n}) {\n const client = useStateful(\n () =>\n restoreReplaneClient<T>({\n snapshot,\n connection: {\n baseUrl: options.baseUrl,\n sdkKey: options.sdkKey,\n fetchFn: options.fetchFn,\n requestTimeoutMs: options.requestTimeoutMs,\n retryDelayMs: options.retryDelayMs,\n inactivityTimeoutMs: options.inactivityTimeoutMs,\n logger: options.logger,\n agent: options.agent ?? DEFAULT_AGENT,\n },\n context: options.context,\n }),\n (c) => c.close(),\n [snapshot, options]\n );\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation (non-suspense).\n * Throws errors during rendering so they can be caught by Error Boundaries.\n */\nfunction ReplaneProviderWithOptions<T extends object>({\n options,\n children,\n loader,\n}: ReplaneProviderWithOptionsProps<T>) {\n const state = useReplaneClientInternal<T>(options);\n\n if (state.status === \"loading\") {\n return <>{loader ?? null}</>;\n }\n\n if (state.status === \"error\") {\n // Throw error during render so it can be caught by Error Boundary\n throw state.error;\n }\n\n const value: ReplaneContextValue<T> = { client: state.client };\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation with Suspense.\n */\nfunction ReplaneProviderWithSuspense<T extends object>({\n options,\n children,\n}: ReplaneProviderWithOptionsProps<T>) {\n const client = useReplaneClientSuspense<T>(options);\n const value = useMemo<ReplaneContextValue<T>>(() => ({ client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Provider component that makes a ReplaneClient available to the component tree.\n *\n * Can be used in three ways:\n *\n * 1. With a pre-created client:\n * ```tsx\n * const client = await createReplaneClient({ ... });\n * <ReplaneProvider client={client}>\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * 2. With options (client managed internally):\n * ```tsx\n * <ErrorBoundary fallback={<ErrorMessage />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * loader={<LoadingSpinner />}\n * >\n * <App />\n * </ReplaneProvider>\n * </ErrorBoundary>\n * ```\n *\n * 3. With Suspense:\n * ```tsx\n * <ErrorBoundary fallback={<ErrorMessage />}>\n * <Suspense fallback={<LoadingSpinner />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * suspense\n * >\n * <App />\n * </ReplaneProvider>\n * </Suspense>\n * </ErrorBoundary>\n * ```\n *\n * 4. With a snapshot (for SSR/hydration):\n * ```tsx\n * // On the server, get a snapshot from the client\n * const snapshot = serverClient.getSnapshot();\n *\n * // On the client, restore from the snapshot with live updates\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * snapshot={snapshot}\n * >\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * Errors during client initialization are thrown during rendering,\n * allowing them to be caught by React Error Boundaries.\n */\nexport function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>) {\n if (hasClient(props)) {\n return <ReplaneProviderWithClient {...props} />;\n }\n\n // Has options - check if snapshot is provided\n if (props.snapshot) {\n return <ReplaneProviderWithSnapshot {...props} snapshot={props.snapshot} />;\n }\n\n if (props.suspense) {\n return <ReplaneProviderWithSuspense {...props} />;\n }\n\n return <ReplaneProviderWithOptions {...props} />;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["options: ConnectOptions","options: ReplaneOptions<T>","connection: ConnectOptions","Replane","originalConnection: ConnectOptions","err: unknown","connection?: ConnectOptions","props: ReplaneProviderProps<T>","Replane","value: ReplaneContextValue<T>","props: ReplaneProviderProps<T>","name: string","options?: GetConfigOptions<T>","callback: () => void","name: K","options?: GetConfigOptions<TConfigs[K]>"],"sources":["../src/context.ts","../src/useReplaneClient.ts","../src/types.ts","../src/version.ts","../src/provider.tsx","../src/hooks.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext } from \"react\";\nimport type { ReplaneContextValue } from \"./types\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const ReplaneContext = createContext<ReplaneContextValue<any> | null>(null);\n","\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Replane, type ConnectOptions, type ReplaneOptions } from \"@replanejs/sdk\";\n\ntype ClientState<T extends object> =\n | { status: \"loading\"; client: null; error: null }\n | { status: \"ready\"; client: Replane<T>; error: null }\n | { status: \"error\"; client: null; error: Error };\n\n// Cache for suspense promise tracking\nconst suspenseCache = new Map<\n string,\n {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n promise: Promise<Replane<any>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result?: Replane<any>;\n error?: Error;\n }\n>();\n\nfunction getCacheKey(options: ConnectOptions): string {\n return `${options.baseUrl}:${options.sdkKey}`;\n}\n\n/**\n * Creates a Replane client and connects it.\n */\nasync function createAndConnectClient<T extends object>(\n options: ReplaneOptions<T>,\n connection: ConnectOptions\n): Promise<Replane<T>> {\n const client = new Replane<T>({\n logger: options.logger,\n context: options.context,\n defaults: options.defaults,\n });\n\n if (!connection) {\n return client;\n }\n\n await client.connect(connection);\n\n return client;\n}\n\ntype ErrorConstructor = new (message: string, options?: { cause?: unknown }) => Error;\n\n/**\n * Hook to manage Replane client creation internally.\n * Handles loading state and cleanup.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientInternal<T extends object = any>(\n options: ReplaneOptions<T>,\n originalConnection: ConnectOptions\n): ClientState<T> {\n const [state, setState] = useState<ClientState<T>>({\n status: \"loading\",\n client: null,\n error: null,\n });\n const clientRef = useRef<Replane<T> | null>(null);\n const optionsRef = useRef(options);\n\n const connectionJson = JSON.stringify(originalConnection);\n useEffect(() => {\n const connection = JSON.parse(connectionJson);\n let cancelled = false;\n\n async function initClient() {\n try {\n const client = await createAndConnectClient<T>(optionsRef.current, connection);\n if (cancelled) {\n client.disconnect();\n return;\n }\n clientRef.current = client;\n setState({ status: \"ready\", client, error: null });\n } catch (err) {\n if (cancelled) return;\n const error =\n err instanceof Error ? err : new (Error as ErrorConstructor)(String(err), { cause: err });\n setState({ status: \"error\", client: null, error });\n }\n }\n\n initClient();\n\n return () => {\n cancelled = true;\n if (clientRef.current) {\n clientRef.current.disconnect();\n clientRef.current = null;\n }\n };\n }, [connectionJson]);\n\n return state;\n}\n\n/**\n * Hook for Suspense-based client creation.\n * Throws a promise while loading, throws error on failure.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useReplaneClientSuspense<T extends object = any>(\n options: ReplaneOptions<T>,\n connection: ConnectOptions\n): Replane<T> {\n const cacheKey = getCacheKey(connection);\n const cached = suspenseCache.get(cacheKey);\n\n if (cached) {\n if (cached.error) {\n throw cached.error;\n }\n if (cached.result) {\n return cached.result as Replane<T>;\n }\n // Still loading, throw the promise\n throw cached.promise;\n }\n\n // First time - create the promise\n const promise = createAndConnectClient<T>(options, connection)\n .then((client) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.result = client;\n }\n return client;\n })\n .catch((err: unknown) => {\n const entry = suspenseCache.get(cacheKey);\n if (entry) {\n entry.error = err instanceof Error ? err : new Error(String(err));\n }\n throw err;\n });\n\n suspenseCache.set(cacheKey, { promise });\n throw promise;\n}\n\n/**\n * Clear the suspense cache for a specific connection configuration.\n * Useful for testing or when you need to force re-initialization.\n */\nexport function clearSuspenseCache(connection?: ConnectOptions): void {\n if (connection) {\n suspenseCache.delete(getCacheKey(connection));\n } else {\n suspenseCache.clear();\n }\n}\n","import type { Replane, ReplaneOptions, ConnectOptions } from \"@replanejs/sdk\";\nimport type { ReactNode } from \"react\";\n\nexport type UntypedReplaneConfig = Record<string, unknown>;\n\nexport interface ReplaneContextValue<T extends object = UntypedReplaneConfig> {\n replane: Replane<T>;\n}\n\n/**\n * Props for ReplaneProvider when using a pre-created client.\n */\nexport interface ReplaneProviderWithClientProps<T extends object = UntypedReplaneConfig> {\n /** Pre-created Replane client instance */\n client: Replane<T>;\n children: ReactNode;\n}\n\n/**\n * Props for ReplaneProvider when letting it manage the client internally.\n */\nexport interface ReplaneProviderWithOptionsProps<\n T extends object = UntypedReplaneConfig,\n> extends ReplaneOptions<T> {\n children: ReactNode;\n /**\n * Connection options for connecting to the Replane server.\n * Pass null to explicitly skip connection (client will use defaults/snapshot only).\n */\n connection: ConnectOptions | null;\n /**\n * Optional loading component to show while the client is initializing.\n * If not provided and suspense is false/undefined, children will not render until ready.\n * Ignored when snapshot is provided (restoration is synchronous).\n */\n loader?: ReactNode;\n /**\n * If true, uses React Suspense for loading state.\n * The provider will throw a promise that Suspense can catch.\n * Ignored when snapshot is provided (restoration is synchronous).\n * @default false\n */\n suspense?: boolean;\n /**\n * If true, the client will be connected asynchronously. Make sure to provide defaults or snapshot.\n * @default false\n */\n async?: boolean;\n}\n\nexport type ReplaneProviderProps<T extends object = UntypedReplaneConfig> =\n | ReplaneProviderWithClientProps<T>\n | ReplaneProviderWithOptionsProps<T>;\n\n/**\n * Type guard to check if props contain a pre-created client.\n */\nexport function hasClient<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithClientProps<T> {\n return \"client\" in props && !!props.client;\n}\n\n/**\n * Type guard to check if props contain options (with or without snapshot).\n */\nexport function hasOptions<T extends object>(\n props: ReplaneProviderProps<T>\n): props is ReplaneProviderWithOptionsProps<T> {\n return !hasClient(props);\n}\n","// Auto-generated - do not edit manually\nexport const VERSION = \"0.9.2\";\nexport const DEFAULT_AGENT = `replane-js-react/${VERSION}`;\n","\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { Replane, type ConnectOptions } from \"@replanejs/sdk\";\nimport { ReplaneContext } from \"./context\";\nimport { useReplaneClientInternal, useReplaneClientSuspense } from \"./useReplaneClient\";\nimport type {\n ReplaneProviderProps,\n ReplaneProviderWithClientProps,\n ReplaneProviderWithOptionsProps,\n ReplaneContextValue,\n} from \"./types\";\nimport { hasClient } from \"./types\";\nimport { DEFAULT_AGENT } from \"./version\";\n\n/**\n * Internal provider component for pre-created client.\n */\nfunction ReplaneProviderWithClient<T extends object>({\n client,\n children,\n}: ReplaneProviderWithClientProps<T>) {\n const value = useMemo<ReplaneContextValue<T>>(() => ({ replane: client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for creating a Replane client asynchronously.\n * Creates a Replane client synchronously and connects in background.\n */\nfunction AsyncReplaneProvider<T extends object>({\n children,\n connection,\n ...options\n}: ReplaneProviderWithOptionsProps<T>) {\n const replaneRef = useRef<Replane<T>>(undefined as unknown as Replane<T>);\n\n if (!replaneRef.current) {\n replaneRef.current = new Replane<T>(options);\n }\n\n const connectionJson = connection ? JSON.stringify(connection) : undefined;\n\n useEffect(() => {\n const parsedConnection = connectionJson ? JSON.parse(connectionJson) : undefined;\n if (!parsedConnection) {\n return;\n }\n\n replaneRef.current.connect(parsedConnection).catch((err) => {\n (options.logger ?? console)?.error(\"Failed to connect Replane client\", err);\n });\n\n return () => {\n replaneRef.current.disconnect();\n };\n }, [connectionJson, options.logger]);\n\n const value = useMemo<ReplaneContextValue<T>>(() => ({ replane: replaneRef.current }), []);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation (non-suspense).\n * Throws errors during rendering so they can be caught by Error Boundaries.\n */\nfunction LoaderReplaneProvider<T extends object>({\n children,\n loader,\n connection,\n ...options\n}: ReplaneProviderWithOptionsProps<T> & { connection: ConnectOptions }) {\n if (!connection) {\n throw new Error(\"Connection is required when using Loader\");\n }\n const state = useReplaneClientInternal<T>(options, connection);\n\n if (state.status === \"loading\") {\n return <>{loader ?? null}</>;\n }\n\n if (state.status === \"error\") {\n // Throw error during render so it can be caught by Error Boundary\n throw state.error;\n }\n\n const value: ReplaneContextValue<T> = { replane: state.client };\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Internal provider component for options-based client creation with Suspense.\n */\nfunction SuspenseReplaneProvider<T extends object>({\n connection,\n children,\n ...options\n}: ReplaneProviderWithOptionsProps<T> & { connection: ConnectOptions }) {\n if (!connection) {\n throw new Error(\"Connection is required when using Suspense\");\n }\n const client = useReplaneClientSuspense<T>(options, connection);\n const value = useMemo<ReplaneContextValue<T>>(() => ({ replane: client }), [client]);\n return <ReplaneContext.Provider value={value}>{children}</ReplaneContext.Provider>;\n}\n\n/**\n * Provider component that makes a Replane client available to the component tree.\n *\n * Can be used in several ways:\n *\n * 1. With a pre-created client:\n * ```tsx\n * const client = new Replane({ defaults: { ... } });\n * await client.connect({ baseUrl: '...', sdkKey: '...' });\n * <ReplaneProvider client={client}>\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * 2. With options (client managed internally):\n * ```tsx\n * <ErrorBoundary fallback={<ErrorMessage />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * loader={<LoadingSpinner />}\n * >\n * <App />\n * </ReplaneProvider>\n * </ErrorBoundary>\n * ```\n *\n * 3. With Suspense:\n * ```tsx\n * <ErrorBoundary fallback={<ErrorMessage />}>\n * <Suspense fallback={<LoadingSpinner />}>\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * suspense\n * >\n * <App />\n * </ReplaneProvider>\n * </Suspense>\n * </ErrorBoundary>\n * ```\n *\n * 4. With a snapshot (for SSR/hydration):\n * ```tsx\n * // On the server, get a snapshot from the client\n * const snapshot = serverClient.getSnapshot();\n *\n * // On the client, restore from the snapshot with live updates\n * <ReplaneProvider\n * options={{ baseUrl: '...', sdkKey: '...' }}\n * snapshot={snapshot}\n * >\n * <App />\n * </ReplaneProvider>\n * ```\n *\n * Errors during client initialization are thrown during rendering,\n * allowing them to be caught by React Error Boundaries.\n */\nexport function ReplaneProvider<T extends object>(props: ReplaneProviderProps<T>) {\n const originalConnection = (props as { connection?: ConnectOptions }).connection;\n const connection = useMemo(\n () =>\n originalConnection\n ? {\n ...originalConnection,\n agent: originalConnection.agent ?? DEFAULT_AGENT,\n }\n : undefined,\n [originalConnection]\n );\n\n if (hasClient(props)) {\n return <ReplaneProviderWithClient {...props} />;\n }\n\n if (props.snapshot || !connection || props.async) {\n return <AsyncReplaneProvider {...props} />;\n }\n\n if (props.suspense) {\n return <SuspenseReplaneProvider {...props} connection={connection} />;\n }\n\n return <LoaderReplaneProvider {...props} connection={connection} />;\n}\n","\"use client\";\n\nimport { useCallback, useContext, useSyncExternalStore } from \"react\";\nimport { ReplaneContext } from \"./context\";\nimport type { UntypedReplaneConfig } from \"./types\";\nimport type { Replane, GetConfigOptions } from \"@replanejs/sdk\";\n\nexport function useReplane<T extends object = UntypedReplaneConfig>(): Replane<T> {\n const context = useContext(ReplaneContext);\n if (!context) {\n throw new Error(\"useReplane must be used within a ReplaneProvider\");\n }\n return context.replane as Replane<T>;\n}\n\nexport function useConfig<T>(name: string, options?: GetConfigOptions<T>): T {\n const client = useReplane();\n\n const subscribe = useCallback(\n (callback: () => void) => {\n return client.subscribe(name, callback);\n },\n [client, name]\n );\n\n const get = useCallback(() => {\n return client.get(name, options) as T;\n }, [client, name, options]);\n\n const value = useSyncExternalStore(subscribe, get, get);\n\n return value;\n}\n\n/**\n * Creates a typed version of useReplane hook.\n *\n * @example\n * ```tsx\n * interface AppConfigs {\n * theme: { darkMode: boolean };\n * features: { beta: boolean };\n * }\n *\n * const useAppReplane = createReplaneHook<AppConfigs>();\n *\n * function MyComponent() {\n * const replane = useAppReplane();\n * // replane.get(\"theme\") returns { darkMode: boolean }\n * }\n * ```\n */\nexport function createReplaneHook<TConfigs extends object>() {\n return function useTypedReplane(): Replane<TConfigs> {\n return useReplane<TConfigs>();\n };\n}\n\n/**\n * Creates a typed version of useConfig hook.\n *\n * @example\n * ```tsx\n * interface AppConfigs {\n * theme: { darkMode: boolean };\n * features: { beta: boolean };\n * }\n *\n * const useAppConfig = createConfigHook<AppConfigs>();\n *\n * function MyComponent() {\n * const theme = useAppConfig(\"theme\");\n * // theme is typed as { darkMode: boolean }\n * }\n * ```\n */\nexport function createConfigHook<TConfigs extends object>() {\n return function useTypedConfig<K extends keyof TConfigs>(\n name: K,\n options?: GetConfigOptions<TConfigs[K]>\n ): TConfigs[K] {\n return useConfig<TConfigs[K]>(String(name), options);\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replanejs/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "React SDK for Replane - feature flags and remote configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"react": ">=18.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@replanejs/sdk": "^0.
|
|
43
|
+
"@replanejs/sdk": "^0.9.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@testing-library/jest-dom": "^6.9.1",
|