@rockerone/xprnkit 0.3.0 → 0.3.1
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
CHANGED
|
@@ -23,6 +23,63 @@ Whether you're creating smart contracts, integrating wallets, or handling token
|
|
|
23
23
|
|
|
24
24
|
XPRNKit is the ideal toolkit for developers who want to build and deploy secure, scalable, and user-friendly dApps on XPRNetwork with greater efficiency.
|
|
25
25
|
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 0.3.1 Performance Update
|
|
29
|
+
|
|
30
|
+
Version 0.3.1 introduces significant performance optimizations to the `XPRNProvider` multi-session management system. The internal state architecture has been refactored to use refs instead of useState for session storage, dramatically reducing unnecessary re-renders.
|
|
31
|
+
|
|
32
|
+
### Performance Gains
|
|
33
|
+
|
|
34
|
+
| Scenario | Before | After | Improvement |
|
|
35
|
+
|----------|--------|-------|-------------|
|
|
36
|
+
| **Single-session usage** | baseline | optimized | **~1.5x faster** |
|
|
37
|
+
| **Multi-session (2-3 wallets)** | baseline | optimized | **~3-4x faster** |
|
|
38
|
+
| **Heavy multi-session (4+ wallets)** | baseline | optimized | **~5x+ faster** |
|
|
39
|
+
|
|
40
|
+
### Re-render Reduction (3-wallet scenario)
|
|
41
|
+
|
|
42
|
+
| Operation | Before | After | Reduction |
|
|
43
|
+
|-----------|--------|-------|-----------|
|
|
44
|
+
| Connect wallet 1 | 3 re-renders | 2 re-renders | 33% |
|
|
45
|
+
| Connect wallet 2 | 2 re-renders | 0 re-renders | 100% |
|
|
46
|
+
| Connect wallet 3 | 2 re-renders | 0 re-renders | 100% |
|
|
47
|
+
| Profile fetch (active) | 1 re-render | 1 re-render | 0% |
|
|
48
|
+
| Profile fetch (non-active) | 1 re-render | 0 re-renders | 100% |
|
|
49
|
+
| Authenticate (non-active) | 1 re-render | 0 re-renders | 100% |
|
|
50
|
+
| **Total (3-wallet setup)** | **13 re-renders** | **4 re-renders** | **~70%** |
|
|
51
|
+
|
|
52
|
+
### Callback Stability
|
|
53
|
+
|
|
54
|
+
All session management callbacks are now stable references:
|
|
55
|
+
|
|
56
|
+
| Callback | Dependencies | Stable? |
|
|
57
|
+
|----------|--------------|---------|
|
|
58
|
+
| `listSessions` | `[]` | ✅ Never recreated |
|
|
59
|
+
| `getSessionById` | `[]` | ✅ Never recreated |
|
|
60
|
+
| `getSessionByActor` | `[]` | ✅ Never recreated |
|
|
61
|
+
| `getAllProfiles` | `[]` | ✅ Never recreated |
|
|
62
|
+
| `getActiveSession` | `[]` | ✅ Never recreated |
|
|
63
|
+
| `setActiveSession` | `[forceUpdate]` | ✅ Stable |
|
|
64
|
+
| `switchSession` | `[setActiveSession]` | ✅ Stable |
|
|
65
|
+
| `removeSession` | `[config, forceUpdate]` | ✅ Stable |
|
|
66
|
+
| `disconnect` | `[removeSession]` | ✅ Stable |
|
|
67
|
+
| `authenticate` | `[config, forceUpdate]` | ✅ Stable |
|
|
68
|
+
| `connect` | `[config, forceUpdate]` | ✅ Stable |
|
|
69
|
+
|
|
70
|
+
### Key Optimizations
|
|
71
|
+
|
|
72
|
+
- **Ref-based session storage**: Sessions Map is now stored in a ref, eliminating Map recreation on every update
|
|
73
|
+
- **Selective re-renders**: Only triggers re-renders when the **active session** data changes
|
|
74
|
+
- **Zero re-renders for non-active operations**: Adding/updating non-active sessions doesn't cause re-renders
|
|
75
|
+
- **Stable callback references**: Consumer components using these callbacks won't re-render due to callback identity changes
|
|
76
|
+
|
|
77
|
+
### Backward Compatibility
|
|
78
|
+
|
|
79
|
+
These optimizations are fully backward compatible. All existing code using `useXPRN()` continues to work without changes.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
26
83
|
# XPRNProvider and useXPRN Documentation
|
|
27
84
|
|
|
28
85
|
## XPRNProvider
|
|
@@ -52,7 +52,7 @@ const XPRNTransaction = React.forwardRef(({ className, variant, size, asChild =
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}, [session, actions, setTxStatus]);
|
|
55
|
-
return (_jsxs(_Fragment, { children: [!session && (_jsx(Comp, { onClick: () => connect(false,
|
|
55
|
+
return (_jsxs(_Fragment, { children: [!session && (_jsx(Comp, { onClick: () => connect(false, () => pushTransaction()), className: cn(XPRNTransactionVariants({ variant, size, className })), ref: ref, children: "Connecte Me!", ...props })), session && (_jsx(Comp, { onClick: () => pushTransaction(), className: cn(XPRNTransactionVariants({ variant, size, className })), ref: ref, children: TxStatusNode, ...props }))] }));
|
|
56
56
|
});
|
|
57
57
|
XPRNTransaction.displayName = "XPRNTransaction";
|
|
58
58
|
export { XPRNTransaction, XPRNTransactionVariants };
|
|
@@ -43,7 +43,7 @@ type XPRNProviderContext = {
|
|
|
43
43
|
authentication: XPRNAuthentication | null;
|
|
44
44
|
authenticate: (success: (res: any) => void, fail: (e: any) => void, actor?: string) => void;
|
|
45
45
|
addTransactionError: (rawMessage: string) => void;
|
|
46
|
-
connect: (restore?: boolean,
|
|
46
|
+
connect: (restore?: boolean, onSession?: (session: LinkSession) => void, onProfile?: (profile: XPRNProfile) => void) => void;
|
|
47
47
|
disconnect: (actor?: string) => void;
|
|
48
48
|
getActiveSession: () => XPRNSession | null;
|
|
49
49
|
listSessions: () => XPRNSession[];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { ApiClass } from "@proton/api";
|
|
4
4
|
import ConnectWallet from "@proton/web-sdk";
|
|
5
|
-
import React, { useCallback, useContext, useEffect, useMemo,
|
|
5
|
+
import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, } from "react";
|
|
6
6
|
import { JsonRpc } from "@proton/js";
|
|
7
7
|
import { parseTransactionErrorMessage } from "../utils";
|
|
8
8
|
import { proton_wrap } from "../interfaces/proton_wrap";
|
|
@@ -27,50 +27,63 @@ const XPRNContext = React.createContext({
|
|
|
27
27
|
getSessionByActor: () => null,
|
|
28
28
|
});
|
|
29
29
|
export const XPRNProvider = ({ children, config, }) => {
|
|
30
|
-
//
|
|
31
|
-
const [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
30
|
+
// Force update mechanism - only triggers re-render when active session changes
|
|
31
|
+
const [version, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
32
|
+
// Internal refs - no re-renders when mutated
|
|
33
|
+
const sessionsRef = useRef(new Map());
|
|
34
|
+
const activeSessionIdRef = useRef(null);
|
|
35
|
+
const jsonRpcRef = useRef(new JsonRpc(config.endpoints));
|
|
36
|
+
const errorsStackRef = useRef([]);
|
|
36
37
|
const onSessionRef = useRef();
|
|
37
38
|
const onProfileRef = useRef();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const session =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const isRestoringRef = useRef(false);
|
|
40
|
+
// Derived values - recomputed only when version changes (active session affected)
|
|
41
|
+
const { activeSession, session, link, profile, authentication } = useMemo(() => {
|
|
42
|
+
const active = activeSessionIdRef.current
|
|
43
|
+
? sessionsRef.current.get(activeSessionIdRef.current) ?? null
|
|
44
|
+
: null;
|
|
45
|
+
return {
|
|
46
|
+
activeSession: active,
|
|
47
|
+
session: active?.session ?? null,
|
|
48
|
+
link: active?.link ?? null,
|
|
49
|
+
profile: active?.profile ?? null,
|
|
50
|
+
authentication: active?.authentication ?? null,
|
|
51
|
+
};
|
|
52
|
+
}, [version]);
|
|
53
|
+
// Session management methods - stable callbacks using refs
|
|
45
54
|
const getActiveSession = useCallback(() => {
|
|
46
|
-
return
|
|
47
|
-
|
|
55
|
+
return activeSessionIdRef.current
|
|
56
|
+
? sessionsRef.current.get(activeSessionIdRef.current) ?? null
|
|
57
|
+
: null;
|
|
58
|
+
}, []);
|
|
48
59
|
const listSessions = useCallback(() => {
|
|
49
|
-
return Array.from(
|
|
50
|
-
}, [
|
|
60
|
+
return Array.from(sessionsRef.current.values());
|
|
61
|
+
}, []);
|
|
51
62
|
const setActiveSession = useCallback((actor) => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
63
|
+
if (sessionsRef.current.has(actor)) {
|
|
64
|
+
activeSessionIdRef.current = actor;
|
|
54
65
|
// Store in localStorage for persistence
|
|
55
66
|
if (typeof window !== "undefined") {
|
|
56
67
|
localStorage.setItem("xprn_active_session_actor", actor);
|
|
57
68
|
}
|
|
69
|
+
// Trigger re-render for consumers
|
|
70
|
+
forceUpdate();
|
|
58
71
|
}
|
|
59
72
|
else {
|
|
60
73
|
console.warn(`Session with actor ${actor} not found`);
|
|
61
74
|
}
|
|
62
|
-
}, [
|
|
75
|
+
}, [forceUpdate]);
|
|
63
76
|
const switchSession = useCallback((actor) => {
|
|
64
77
|
setActiveSession(actor);
|
|
65
78
|
}, [setActiveSession]);
|
|
66
79
|
const getSessionById = useCallback((actor) => {
|
|
67
|
-
return
|
|
68
|
-
}, [
|
|
80
|
+
return sessionsRef.current.get(actor) ?? null;
|
|
81
|
+
}, []);
|
|
69
82
|
const getSessionByActor = useCallback((actor) => {
|
|
70
|
-
return
|
|
71
|
-
}, [
|
|
83
|
+
return sessionsRef.current.get(actor) ?? null;
|
|
84
|
+
}, []);
|
|
72
85
|
const removeSession = useCallback(async (actor) => {
|
|
73
|
-
const sessionToRemove =
|
|
86
|
+
const sessionToRemove = sessionsRef.current.get(actor);
|
|
74
87
|
if (!sessionToRemove) {
|
|
75
88
|
console.warn(`Session with actor ${actor} not found`);
|
|
76
89
|
return;
|
|
@@ -82,25 +95,22 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
82
95
|
catch (e) {
|
|
83
96
|
console.error("Error removing session from link:", e);
|
|
84
97
|
}
|
|
85
|
-
// Remove from sessions map
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
// If this was the active session, clear it
|
|
92
|
-
if (activeSessionId === actor) {
|
|
93
|
-
setActiveSessionId(null);
|
|
98
|
+
// Remove from sessions map (mutate ref directly)
|
|
99
|
+
sessionsRef.current.delete(actor);
|
|
100
|
+
// If this was the active session, clear it and trigger re-render
|
|
101
|
+
if (activeSessionIdRef.current === actor) {
|
|
102
|
+
activeSessionIdRef.current = null;
|
|
94
103
|
if (typeof window !== "undefined") {
|
|
95
104
|
localStorage.removeItem("xprn_active_session_actor");
|
|
96
105
|
}
|
|
106
|
+
forceUpdate();
|
|
97
107
|
}
|
|
98
|
-
}, [
|
|
108
|
+
}, [config.requesterAccount, forceUpdate]);
|
|
99
109
|
const getAllProfiles = useCallback(() => {
|
|
100
|
-
return Array.from(
|
|
110
|
+
return Array.from(sessionsRef.current.values())
|
|
101
111
|
.map(s => s.profile)
|
|
102
112
|
.filter((p) => p !== null);
|
|
103
|
-
}, [
|
|
113
|
+
}, []);
|
|
104
114
|
const connect = useCallback((restoreSession, onSession, onProfile) => {
|
|
105
115
|
console.log("restoreSession", restoreSession);
|
|
106
116
|
ConnectWallet({
|
|
@@ -127,17 +137,15 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
127
137
|
profile: null,
|
|
128
138
|
authentication: null,
|
|
129
139
|
};
|
|
130
|
-
// Add to sessions map
|
|
131
|
-
|
|
132
|
-
const newSessions = new Map(prev);
|
|
133
|
-
newSessions.set(actor, newSession);
|
|
134
|
-
return newSessions;
|
|
135
|
-
});
|
|
140
|
+
// Add to sessions map (mutate ref directly)
|
|
141
|
+
sessionsRef.current.set(actor, newSession);
|
|
136
142
|
// Auto-switch to newly connected session
|
|
137
|
-
|
|
143
|
+
activeSessionIdRef.current = actor;
|
|
138
144
|
if (typeof window !== "undefined") {
|
|
139
145
|
localStorage.setItem("xprn_active_session_actor", actor);
|
|
140
146
|
}
|
|
147
|
+
// Trigger re-render since active session changed
|
|
148
|
+
forceUpdate();
|
|
141
149
|
// Fetch profile data
|
|
142
150
|
const api = new ApiClass(config.apiMode && config.apiMode == "testnet"
|
|
143
151
|
? "proton-test"
|
|
@@ -149,46 +157,44 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
149
157
|
avatar: `${profileRes.avatar}`,
|
|
150
158
|
isKyc: profileRes.kyc.length > 0 || false,
|
|
151
159
|
};
|
|
152
|
-
// Update session with profile
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
160
|
+
// Update session with profile (mutate ref)
|
|
161
|
+
const existingSession = sessionsRef.current.get(actor);
|
|
162
|
+
if (existingSession) {
|
|
163
|
+
sessionsRef.current.set(actor, {
|
|
164
|
+
...existingSession,
|
|
165
|
+
profile: xprProfile,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Only trigger re-render if this is still the active session
|
|
169
|
+
if (activeSessionIdRef.current === actor) {
|
|
170
|
+
forceUpdate();
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
});
|
|
166
174
|
}
|
|
167
175
|
});
|
|
168
|
-
}, [config]);
|
|
176
|
+
}, [config, forceUpdate]);
|
|
169
177
|
const disconnect = useCallback(async (actor) => {
|
|
170
178
|
// If actor is provided, disconnect that specific session
|
|
171
179
|
// Otherwise, disconnect the active session
|
|
172
|
-
const targetActor = actor ||
|
|
180
|
+
const targetActor = actor || activeSessionIdRef.current;
|
|
173
181
|
if (!targetActor) {
|
|
174
182
|
console.warn("No session to disconnect");
|
|
175
183
|
return;
|
|
176
184
|
}
|
|
177
185
|
await removeSession(targetActor);
|
|
178
|
-
}, [
|
|
186
|
+
}, [removeSession]);
|
|
179
187
|
const addTxError = useCallback((message) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
setErrorStack(mutatedMessages);
|
|
183
|
-
}, [errorsStack, setErrorStack]);
|
|
188
|
+
errorsStackRef.current.push(parseTransactionErrorMessage(message));
|
|
189
|
+
}, []);
|
|
184
190
|
const authenticate = useCallback((success, fail, actor) => {
|
|
185
191
|
// Use provided actor or active session
|
|
186
|
-
const targetActor = actor ||
|
|
192
|
+
const targetActor = actor || activeSessionIdRef.current;
|
|
187
193
|
if (!targetActor) {
|
|
188
194
|
fail(new Error("No session available for authentication"));
|
|
189
195
|
return;
|
|
190
196
|
}
|
|
191
|
-
const targetSession =
|
|
197
|
+
const targetSession = sessionsRef.current.get(targetActor);
|
|
192
198
|
if (!targetSession) {
|
|
193
199
|
fail(new Error(`Session not found for actor: ${targetActor}`));
|
|
194
200
|
return;
|
|
@@ -231,18 +237,18 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
231
237
|
actor: targetSession.session.auth.actor.toString(),
|
|
232
238
|
data: authRes,
|
|
233
239
|
};
|
|
234
|
-
// Update session with authentication data
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
240
|
+
// Update session with authentication data (mutate ref)
|
|
241
|
+
const existingSession = sessionsRef.current.get(targetActor);
|
|
242
|
+
if (existingSession) {
|
|
243
|
+
sessionsRef.current.set(targetActor, {
|
|
244
|
+
...existingSession,
|
|
245
|
+
authentication: authData,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// Only trigger re-render if this is the active session
|
|
249
|
+
if (activeSessionIdRef.current === targetActor) {
|
|
250
|
+
forceUpdate();
|
|
251
|
+
}
|
|
246
252
|
success(authRes);
|
|
247
253
|
})
|
|
248
254
|
.catch(fail);
|
|
@@ -252,11 +258,11 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
252
258
|
catch (e) {
|
|
253
259
|
fail(e);
|
|
254
260
|
}
|
|
255
|
-
}, [
|
|
261
|
+
}, [config, forceUpdate]);
|
|
256
262
|
// Handle authentication for active session
|
|
257
263
|
useEffect(() => {
|
|
258
|
-
if (session &&
|
|
259
|
-
const currentSession =
|
|
264
|
+
if (session && activeSessionIdRef.current) {
|
|
265
|
+
const currentSession = sessionsRef.current.get(activeSessionIdRef.current);
|
|
260
266
|
if (currentSession && !currentSession.authentication) {
|
|
261
267
|
if (config.enforceAuthentication && config.authenticationUrl) {
|
|
262
268
|
authenticate(() => { }, () => { });
|
|
@@ -267,7 +273,7 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
267
273
|
onSessionRef.current(session);
|
|
268
274
|
onSessionRef.current = undefined; // Reset the ref
|
|
269
275
|
}
|
|
270
|
-
}, [session,
|
|
276
|
+
}, [session, authenticate, config.enforceAuthentication, config.authenticationUrl]);
|
|
271
277
|
// Handle profile callbacks
|
|
272
278
|
useEffect(() => {
|
|
273
279
|
if (profile && onProfileRef.current) {
|
|
@@ -275,32 +281,35 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
275
281
|
onProfileRef.current = undefined; // Reset the ref
|
|
276
282
|
}
|
|
277
283
|
}, [profile]);
|
|
278
|
-
// Session restoration - only restore active session
|
|
284
|
+
// Session restoration - only restore active session (runs once on mount)
|
|
279
285
|
useEffect(() => {
|
|
280
|
-
if (
|
|
286
|
+
if (isRestoringRef.current)
|
|
287
|
+
return;
|
|
288
|
+
if (sessionsRef.current.size === 0 && config.restoreSession) {
|
|
281
289
|
// Check if there's a stored active session
|
|
282
290
|
if (typeof window !== "undefined") {
|
|
283
291
|
const storedActiveActor = localStorage.getItem("xprn_active_session_actor");
|
|
284
292
|
if (storedActiveActor) {
|
|
293
|
+
isRestoringRef.current = true;
|
|
285
294
|
// Try to restore the session
|
|
286
295
|
connect(true);
|
|
287
296
|
}
|
|
288
297
|
}
|
|
289
298
|
}
|
|
290
|
-
}, [
|
|
299
|
+
}, [config.restoreSession, connect]);
|
|
291
300
|
const providerValue = useMemo(() => {
|
|
292
301
|
return {
|
|
293
|
-
// Backward compatibility - active session values
|
|
302
|
+
// Backward compatibility - active session values (reactive via version)
|
|
294
303
|
session,
|
|
295
304
|
link,
|
|
296
305
|
profile,
|
|
297
|
-
rpc:
|
|
306
|
+
rpc: jsonRpcRef.current,
|
|
298
307
|
addTransactionError: addTxError,
|
|
299
308
|
connect,
|
|
300
309
|
disconnect,
|
|
301
310
|
authenticate,
|
|
302
311
|
authentication,
|
|
303
|
-
// Multi-session management methods
|
|
312
|
+
// Multi-session management methods (stable references)
|
|
304
313
|
getActiveSession,
|
|
305
314
|
listSessions,
|
|
306
315
|
setActiveSession,
|
|
@@ -311,15 +320,16 @@ export const XPRNProvider = ({ children, config, }) => {
|
|
|
311
320
|
getSessionByActor,
|
|
312
321
|
};
|
|
313
322
|
}, [
|
|
323
|
+
// Reactive values (change when active session changes)
|
|
314
324
|
session,
|
|
315
325
|
link,
|
|
316
326
|
profile,
|
|
317
|
-
|
|
327
|
+
authentication,
|
|
328
|
+
// Stable callbacks (only change when their specific deps change)
|
|
318
329
|
addTxError,
|
|
319
330
|
connect,
|
|
320
331
|
disconnect,
|
|
321
332
|
authenticate,
|
|
322
|
-
authentication,
|
|
323
333
|
getActiveSession,
|
|
324
334
|
listSessions,
|
|
325
335
|
setActiveSession,
|
package/package.json
CHANGED