@buoy-gg/shared-ui 2.1.2 → 2.1.4-beta.3
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/lib/commonjs/clipboard/clipboard-impl.js +86 -10
- package/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/index.js +141 -59
- package/lib/commonjs/storage/devToolsStorageKeys.js +8 -1
- package/lib/commonjs/stores/BaseEventStore.js +257 -0
- package/lib/commonjs/stores/index.js +12 -0
- package/lib/commonjs/utils/index.js +64 -1
- package/lib/commonjs/utils/safeExpoRouter.js +141 -0
- package/lib/commonjs/utils/subscribable.js +113 -0
- package/lib/commonjs/utils/subscriberCountNotifier.js +72 -0
- package/lib/module/clipboard/clipboard-impl.js +85 -10
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/index.js +10 -1
- package/lib/module/storage/devToolsStorageKeys.js +8 -1
- package/lib/module/stores/BaseEventStore.js +253 -0
- package/lib/module/stores/index.js +7 -0
- package/lib/module/utils/index.js +4 -1
- package/lib/module/utils/safeExpoRouter.js +132 -0
- package/lib/module/utils/subscribable.js +108 -0
- package/lib/module/utils/subscriberCountNotifier.js +66 -0
- package/lib/typescript/commonjs/clipboard/clipboard-impl.d.ts +15 -8
- package/lib/typescript/commonjs/clipboard/clipboard-impl.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +2 -0
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/stores/BaseEventStore.d.ts +145 -0
- package/lib/typescript/commonjs/stores/BaseEventStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/stores/index.d.ts +5 -0
- package/lib/typescript/commonjs/stores/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/index.d.ts +3 -0
- package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts +17 -0
- package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/subscribable.d.ts +78 -0
- package/lib/typescript/commonjs/utils/subscribable.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/subscriberCountNotifier.d.ts +47 -0
- package/lib/typescript/commonjs/utils/subscriberCountNotifier.d.ts.map +1 -0
- package/lib/typescript/module/clipboard/clipboard-impl.d.ts +15 -8
- package/lib/typescript/module/clipboard/clipboard-impl.d.ts.map +1 -1
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +2 -0
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/module/stores/BaseEventStore.d.ts +145 -0
- package/lib/typescript/module/stores/BaseEventStore.d.ts.map +1 -0
- package/lib/typescript/module/stores/index.d.ts +5 -0
- package/lib/typescript/module/stores/index.d.ts.map +1 -0
- package/lib/typescript/module/utils/index.d.ts +3 -0
- package/lib/typescript/module/utils/index.d.ts.map +1 -1
- package/lib/typescript/module/utils/safeExpoRouter.d.ts +17 -0
- package/lib/typescript/module/utils/safeExpoRouter.d.ts.map +1 -0
- package/lib/typescript/module/utils/subscribable.d.ts +78 -0
- package/lib/typescript/module/utils/subscribable.d.ts.map +1 -0
- package/lib/typescript/module/utils/subscriberCountNotifier.d.ts +47 -0
- package/lib/typescript/module/utils/subscriberCountNotifier.d.ts.map +1 -0
- package/package.json +4 -7
- package/scripts/detect-clipboard.js +78 -126
|
@@ -1,20 +1,95 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Detected: none
|
|
6
|
-
* Generated at: 2026-01-29T01:02:34.738Z
|
|
4
|
+
* Runtime clipboard implementation
|
|
7
5
|
*
|
|
8
|
-
*
|
|
6
|
+
* Uses Metro's allowOptionalDependencies with top-level try-catch
|
|
7
|
+
* so Metro marks these requires as optional (skipped if not installed).
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* We grab module references eagerly (for Metro), but detect which one
|
|
10
|
+
* actually works lazily on first use — by trying to call it. This avoids
|
|
11
|
+
* NativeModules checks which don't work with TurboModules/new architecture.
|
|
12
|
+
*
|
|
13
|
+
* Consumers must set `transformer.allowOptionalDependencies = true`
|
|
14
|
+
* in their metro.config.js for this to work.
|
|
15
|
+
*
|
|
16
|
+
* Fallback chain:
|
|
17
|
+
* 1. expo-clipboard
|
|
18
|
+
* 2. @react-native-clipboard/clipboard
|
|
19
|
+
* 3. Graceful failure
|
|
13
20
|
*/
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
// Grab module references at load time (top-level try-catch for Metro)
|
|
23
|
+
// Always require both — we decide which actually works at call time
|
|
24
|
+
let _expoClipboard = null;
|
|
25
|
+
try {
|
|
26
|
+
_expoClipboard = require("expo-clipboard");
|
|
27
|
+
} catch {}
|
|
28
|
+
let _rnClipboard = null;
|
|
29
|
+
try {
|
|
30
|
+
const mod = require("@react-native-clipboard/clipboard");
|
|
31
|
+
_rnClipboard = mod.default || mod;
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
// Lazy detection: resolved on first clipboardFunction() call
|
|
35
|
+
let _detectedType = null;
|
|
36
|
+
let _clipboardFn = null;
|
|
37
|
+
let _detected = false;
|
|
38
|
+
async function detect(text) {
|
|
39
|
+
// 1. Try expo-clipboard by actually calling it
|
|
40
|
+
if (_expoClipboard && typeof _expoClipboard.setStringAsync === "function") {
|
|
41
|
+
try {
|
|
42
|
+
await _expoClipboard.setStringAsync(text);
|
|
43
|
+
_detectedType = "expo";
|
|
44
|
+
_clipboardFn = async t => {
|
|
45
|
+
await _expoClipboard.setStringAsync(t);
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
_detected = true;
|
|
49
|
+
return true;
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Try @react-native-clipboard/clipboard
|
|
54
|
+
if (_rnClipboard && typeof _rnClipboard.setString === "function") {
|
|
55
|
+
try {
|
|
56
|
+
_rnClipboard.setString(text);
|
|
57
|
+
_detectedType = "react-native";
|
|
58
|
+
_clipboardFn = async t => {
|
|
59
|
+
_rnClipboard.setString(t);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
_detected = true;
|
|
63
|
+
return true;
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
_detected = true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
export const clipboardType = () => {
|
|
70
|
+
return _detectedType;
|
|
71
|
+
};
|
|
72
|
+
export const isClipboardAvailable = () => {
|
|
73
|
+
// Before first use, optimistically return true if we have a module ref
|
|
74
|
+
if (!_detected) return _expoClipboard != null || _rnClipboard != null;
|
|
75
|
+
return _clipboardFn != null;
|
|
76
|
+
};
|
|
17
77
|
export const clipboardFunction = async text => {
|
|
18
|
-
|
|
78
|
+
// If already detected, use the cached function
|
|
79
|
+
if (_detected && _clipboardFn) {
|
|
80
|
+
try {
|
|
81
|
+
return await _clipboardFn(text);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("[buoy] Clipboard copy failed:", error);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// First call: detect which clipboard works by actually trying it
|
|
89
|
+
if (!_detected) {
|
|
90
|
+
const result = await detect(text);
|
|
91
|
+
if (result) return true;
|
|
92
|
+
}
|
|
93
|
+
console.warn("[buoy] No clipboard library available. Install expo-clipboard or @react-native-clipboard/clipboard.");
|
|
19
94
|
return false;
|
|
20
95
|
};
|
package/lib/module/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// UI exports
|
|
4
4
|
export * from "./ui/index.js";
|
|
5
5
|
|
|
6
|
+
// Store base classes
|
|
7
|
+
export * from "./stores/index.js";
|
|
8
|
+
|
|
6
9
|
// Utils exports - selectively export to avoid conflicts
|
|
7
10
|
export {
|
|
8
11
|
// Display utilities
|
|
@@ -18,7 +21,13 @@ getValueType, isPrimitive, isJsonSerializable, isValidJson, getConstructorName,
|
|
|
18
21
|
// Value formatting utilities
|
|
19
22
|
parseValue, formatValue, getTypeColor, truncateText, flattenObject, formatPath,
|
|
20
23
|
// Optional module loading utilities
|
|
21
|
-
loadOptionalModule, getCachedOptionalModule
|
|
24
|
+
loadOptionalModule, getCachedOptionalModule,
|
|
25
|
+
// Subscribable base class for self-managing listeners
|
|
26
|
+
Subscribable,
|
|
27
|
+
// Subscriber count notifier for cross-package notifications
|
|
28
|
+
subscriberCountNotifier, subscribeToSubscriberCountChanges, notifySubscriberCountChange,
|
|
29
|
+
// Safe expo-router wrappers (falls back to no-ops on RN CLI)
|
|
30
|
+
useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, isExpoRouterAvailable } from "./utils/index.js";
|
|
22
31
|
|
|
23
32
|
// Also export formatting utils
|
|
24
33
|
export * from "./utils/formatting/index.js";
|
|
@@ -164,6 +164,8 @@ export const devToolsStorageKeys = {
|
|
|
164
164
|
modal: () => `${devToolsStorageKeys.events.root()}_modal`,
|
|
165
165
|
/** Selected badge/source filters */
|
|
166
166
|
enabledSources: () => `${devToolsStorageKeys.events.root()}_enabled_sources`,
|
|
167
|
+
/** Whether event capturing is active */
|
|
168
|
+
isCapturing: () => `${devToolsStorageKeys.events.root()}_is_capturing`,
|
|
167
169
|
/** Copy/export settings */
|
|
168
170
|
copySettings: () => `${devToolsStorageKeys.events.root()}_copy_settings`
|
|
169
171
|
}
|
|
@@ -188,11 +190,16 @@ const LEGACY_DEV_TOOL_PATTERNS = ["@devtools", "@dev_tools_", "@modal_state_",
|
|
|
188
190
|
export function isDevToolsStorageKey(key) {
|
|
189
191
|
if (!key) return false;
|
|
190
192
|
|
|
191
|
-
// Check if it starts with our base prefix
|
|
193
|
+
// Check if it starts with our base prefix (@react_buoy)
|
|
192
194
|
if (key.startsWith(devToolsStorageKeys.base)) {
|
|
193
195
|
return true;
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// Check for buoy- prefixed keys (modal persistence, license, etc.)
|
|
199
|
+
if (key.startsWith("buoy-")) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
196
203
|
// Check for legacy dev tool keys that need cleanup
|
|
197
204
|
for (const pattern of LEGACY_DEV_TOOL_PATTERNS) {
|
|
198
205
|
if (key.startsWith(pattern)) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BaseEventStore - Abstract base class for event aggregation stores
|
|
5
|
+
*
|
|
6
|
+
* Handles dual subscription patterns (array listeners + individual event callbacks)
|
|
7
|
+
* and automatically manages start/stop lifecycle based on subscriber count.
|
|
8
|
+
*
|
|
9
|
+
* Follows TanStack Query's Subscribable pattern with extensions for:
|
|
10
|
+
* - Array subscriptions (get full event list on each update)
|
|
11
|
+
* - Individual event subscriptions (get each event as it occurs)
|
|
12
|
+
* - Auto-start capturing when first subscriber joins
|
|
13
|
+
* - Auto-stop capturing when last subscriber leaves
|
|
14
|
+
* - Subscriber count notifications for debugging UI
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* class StorageEventStore extends BaseEventStore<StorageEvent> {
|
|
19
|
+
* protected storeName = 'storage';
|
|
20
|
+
*
|
|
21
|
+
* protected startCapturing(): void {
|
|
22
|
+
* // Start listening to storage changes
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* protected stopCapturing(): void {
|
|
26
|
+
* // Stop listening to storage changes
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* isCapturing(): boolean {
|
|
30
|
+
* return this.unsubscribe !== null;
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { Subscribable } from "../utils/subscribable.js";
|
|
37
|
+
import { notifySubscriberCountChange } from "../utils/subscriberCountNotifier.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Callback type for receiving full events array
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Callback type for receiving individual events
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration options for BaseEventStore
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Abstract base class for event stores.
|
|
53
|
+
*
|
|
54
|
+
* Subclasses must implement:
|
|
55
|
+
* - `startCapturing()` - Begin listening to the underlying event source
|
|
56
|
+
* - `stopCapturing()` - Stop listening to the underlying event source
|
|
57
|
+
* - `isCapturing()` - Check if currently capturing events
|
|
58
|
+
*/
|
|
59
|
+
export class BaseEventStore extends Subscribable {
|
|
60
|
+
events = [];
|
|
61
|
+
arrayListeners = new Set();
|
|
62
|
+
constructor(options) {
|
|
63
|
+
super();
|
|
64
|
+
this.maxEvents = options.maxEvents ?? 500;
|
|
65
|
+
this.storeName = options.storeName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ===========================================================================
|
|
69
|
+
// ABSTRACT METHODS - Must be implemented by subclasses
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start capturing events from the underlying source.
|
|
74
|
+
* Called automatically when first subscriber joins.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stop capturing events from the underlying source.
|
|
79
|
+
* Called automatically when last subscriber leaves.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if the store is actively capturing events.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
// ===========================================================================
|
|
87
|
+
// LIFECYCLE HOOKS - Called by Subscribable base class
|
|
88
|
+
// ===========================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Called when first subscriber joins (via onEvent).
|
|
92
|
+
* Starts capturing if no one was subscribed.
|
|
93
|
+
*/
|
|
94
|
+
onSubscribe() {
|
|
95
|
+
if (this.getTotalSubscriberCount() === 1) {
|
|
96
|
+
this.startCapturing();
|
|
97
|
+
}
|
|
98
|
+
notifySubscriberCountChange(this.storeName);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Called when a subscriber leaves (via onEvent unsubscribe).
|
|
103
|
+
* Stops capturing if no one is subscribed anymore.
|
|
104
|
+
*/
|
|
105
|
+
onUnsubscribe() {
|
|
106
|
+
if (this.getTotalSubscriberCount() === 0) {
|
|
107
|
+
this.stopCapturing();
|
|
108
|
+
}
|
|
109
|
+
notifySubscriberCountChange(this.storeName);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ===========================================================================
|
|
113
|
+
// SUBSCRIPTION METHODS
|
|
114
|
+
// ===========================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get total count of all subscribers (both individual and array listeners)
|
|
118
|
+
*/
|
|
119
|
+
getTotalSubscriberCount() {
|
|
120
|
+
return this.listeners.size + this.arrayListeners.size;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Subscribe to the full events array.
|
|
125
|
+
* Automatically starts capturing when first subscriber joins.
|
|
126
|
+
*
|
|
127
|
+
* @param listener - Callback that receives the full events array on each update
|
|
128
|
+
* @returns Unsubscribe function
|
|
129
|
+
*/
|
|
130
|
+
subscribeToEvents(listener) {
|
|
131
|
+
const wasEmpty = this.getTotalSubscriberCount() === 0;
|
|
132
|
+
this.arrayListeners.add(listener);
|
|
133
|
+
|
|
134
|
+
// Start capturing if this is the first subscriber
|
|
135
|
+
if (wasEmpty) {
|
|
136
|
+
this.startCapturing();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Notify subscriber count change
|
|
140
|
+
notifySubscriberCountChange(this.storeName);
|
|
141
|
+
|
|
142
|
+
// Immediately call with current events
|
|
143
|
+
listener(this.getEvents());
|
|
144
|
+
|
|
145
|
+
// Return unsubscribe function
|
|
146
|
+
return () => {
|
|
147
|
+
this.arrayListeners.delete(listener);
|
|
148
|
+
|
|
149
|
+
// Stop capturing if no one is subscribed anymore
|
|
150
|
+
if (this.getTotalSubscriberCount() === 0) {
|
|
151
|
+
this.stopCapturing();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Notify subscriber count change
|
|
155
|
+
notifySubscriberCountChange(this.storeName);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Subscribe to individual events as they occur.
|
|
161
|
+
* Automatically starts capturing when first subscriber joins.
|
|
162
|
+
*
|
|
163
|
+
* @param callback - Callback that receives each event as it occurs
|
|
164
|
+
* @returns Unsubscribe function
|
|
165
|
+
*/
|
|
166
|
+
onEvent(callback) {
|
|
167
|
+
// Use Subscribable's subscribe method (triggers onSubscribe hook)
|
|
168
|
+
return this.subscribe(callback);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ===========================================================================
|
|
172
|
+
// EVENT MANAGEMENT
|
|
173
|
+
// ===========================================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Add an event to the store.
|
|
177
|
+
* Call this from subclasses when a new event is received.
|
|
178
|
+
*
|
|
179
|
+
* @param event - The event to add
|
|
180
|
+
*/
|
|
181
|
+
addEvent(event) {
|
|
182
|
+
// Add to beginning (newest first) and limit to maxEvents
|
|
183
|
+
this.events = [event, ...this.events].slice(0, this.maxEvents);
|
|
184
|
+
|
|
185
|
+
// Notify individual event callbacks (via Subscribable)
|
|
186
|
+
this.notify(event);
|
|
187
|
+
|
|
188
|
+
// Notify array listeners
|
|
189
|
+
this.notifyArrayListeners();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Notify all array listeners of changes
|
|
194
|
+
*/
|
|
195
|
+
notifyArrayListeners() {
|
|
196
|
+
const events = this.getEvents();
|
|
197
|
+
this.arrayListeners.forEach(listener => {
|
|
198
|
+
try {
|
|
199
|
+
listener(events);
|
|
200
|
+
} catch {
|
|
201
|
+
// Ignore listener errors
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get all events (newest first)
|
|
208
|
+
*/
|
|
209
|
+
getEvents() {
|
|
210
|
+
return this.events;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get event count
|
|
215
|
+
*/
|
|
216
|
+
getEventCount() {
|
|
217
|
+
return this.events.length;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Clear all events
|
|
222
|
+
*/
|
|
223
|
+
clearEvents() {
|
|
224
|
+
this.events = [];
|
|
225
|
+
this.notifyArrayListeners();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Set maximum number of events to keep
|
|
230
|
+
*/
|
|
231
|
+
setMaxEvents(max) {
|
|
232
|
+
this.maxEvents = max;
|
|
233
|
+
if (this.events.length > max) {
|
|
234
|
+
this.events = this.events.slice(0, max);
|
|
235
|
+
this.notifyArrayListeners();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ===========================================================================
|
|
240
|
+
// DEBUGGING
|
|
241
|
+
// ===========================================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get current subscriber counts for debugging
|
|
245
|
+
*/
|
|
246
|
+
getSubscriberCounts() {
|
|
247
|
+
return {
|
|
248
|
+
eventCallbacks: this.listeners.size,
|
|
249
|
+
arrayListeners: this.arrayListeners.size,
|
|
250
|
+
total: this.getTotalSubscriberCount()
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -6,4 +6,7 @@ export { persistentStorage, isUsingPersistentStorage, getStorageBackendType } fr
|
|
|
6
6
|
export { safeStringify } from "./safeStringify.js";
|
|
7
7
|
export { getValueType, isPrimitive, isJsonSerializable, isValidJson, getConstructorName, isEmpty, getValueSize } from "./typeHelpers.js";
|
|
8
8
|
export { parseValue, formatValue, getTypeColor, truncateText, flattenObject, formatPath } from "./valueFormatting.js";
|
|
9
|
-
export { loadOptionalModule, getCachedOptionalModule } from "./loadOptionalModule.js";
|
|
9
|
+
export { loadOptionalModule, getCachedOptionalModule } from "./loadOptionalModule.js";
|
|
10
|
+
export { Subscribable } from "./subscribable.js";
|
|
11
|
+
export { subscriberCountNotifier, subscribeToSubscriberCountChanges, notifySubscriberCountChange } from "./subscriberCountNotifier.js";
|
|
12
|
+
export { useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, isExpoRouterAvailable } from "./safeExpoRouter.js";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe wrapper for expo-router
|
|
5
|
+
*
|
|
6
|
+
* Provides optional imports for expo-router hooks and utilities.
|
|
7
|
+
* Falls back to no-op implementations when expo-router is not installed.
|
|
8
|
+
*
|
|
9
|
+
* On RN CLI (without Expo native modules), expo-router's JS is bundled but
|
|
10
|
+
* its hooks crash at runtime because the ExpoLinking native module is missing.
|
|
11
|
+
* We check for the native module BEFORE attempting to use any expo-router hooks.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { NativeModules } from "react-native";
|
|
15
|
+
let expoRouter = null;
|
|
16
|
+
let isAvailable = false;
|
|
17
|
+
let checkedAvailability = false;
|
|
18
|
+
function checkExpoRouterAvailability() {
|
|
19
|
+
if (checkedAvailability) return isAvailable;
|
|
20
|
+
try {
|
|
21
|
+
// expo-router depends on ExpoLinking native module.
|
|
22
|
+
// On RN CLI the JS is bundled but the native module isn't registered,
|
|
23
|
+
// so require() succeeds but hooks crash at runtime. Check native side first.
|
|
24
|
+
if (!NativeModules.ExpoLinking) {
|
|
25
|
+
checkedAvailability = true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
expoRouter = require("expo-router");
|
|
29
|
+
isAvailable = expoRouter != null;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
isAvailable = false;
|
|
32
|
+
expoRouter = null;
|
|
33
|
+
}
|
|
34
|
+
checkedAvailability = true;
|
|
35
|
+
return isAvailable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// No-op implementations when expo-router is not available
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
function noOpUseRouter() {
|
|
43
|
+
return {
|
|
44
|
+
push: () => console.warn("[buoy] expo-router not installed: push() unavailable"),
|
|
45
|
+
replace: () => console.warn("[buoy] expo-router not installed: replace() unavailable"),
|
|
46
|
+
back: () => console.warn("[buoy] expo-router not installed: back() unavailable"),
|
|
47
|
+
canGoBack: () => false,
|
|
48
|
+
setParams: () => console.warn("[buoy] expo-router not installed: setParams() unavailable"),
|
|
49
|
+
navigate: () => console.warn("[buoy] expo-router not installed: navigate() unavailable")
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function noOpUsePathname() {
|
|
53
|
+
return "/";
|
|
54
|
+
}
|
|
55
|
+
function noOpUseSegments() {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
function noOpUseGlobalSearchParams() {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Safe hook exports
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export function useSafeRouter() {
|
|
67
|
+
if (!checkExpoRouterAvailability()) {
|
|
68
|
+
return noOpUseRouter();
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return expoRouter.useRouter();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn("[buoy] Failed to use expo-router.useRouter:", error);
|
|
74
|
+
return noOpUseRouter();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function useSafePathname() {
|
|
78
|
+
if (!checkExpoRouterAvailability()) {
|
|
79
|
+
return noOpUsePathname();
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return expoRouter.usePathname();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn("[buoy] Failed to use expo-router.usePathname:", error);
|
|
85
|
+
return noOpUsePathname();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function useSafeSegments() {
|
|
89
|
+
if (!checkExpoRouterAvailability()) {
|
|
90
|
+
return noOpUseSegments();
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return expoRouter.useSegments();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn("[buoy] Failed to use expo-router.useSegments:", error);
|
|
96
|
+
return noOpUseSegments();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function useSafeGlobalSearchParams() {
|
|
100
|
+
if (!checkExpoRouterAvailability()) {
|
|
101
|
+
return noOpUseGlobalSearchParams();
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return expoRouter.useGlobalSearchParams();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn("[buoy] Failed to use expo-router.useGlobalSearchParams:", error);
|
|
107
|
+
return noOpUseGlobalSearchParams();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Router instance getter (for imperative navigation)
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
export function getSafeRouter() {
|
|
116
|
+
if (!checkExpoRouterAvailability()) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return expoRouter.router || null;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Availability check
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
export function isExpoRouterAvailable() {
|
|
131
|
+
return checkExpoRouterAvailability();
|
|
132
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subscribable - Base class for pub/sub pattern with lifecycle hooks
|
|
5
|
+
*
|
|
6
|
+
* Ported from TanStack Query's subscription architecture.
|
|
7
|
+
* Provides a foundation for self-managing listeners that automatically
|
|
8
|
+
* start when the first subscriber joins and stop when the last leaves.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* class NetworkEventStore extends Subscribable<NetworkEventListener> {
|
|
13
|
+
* protected onSubscribe(): void {
|
|
14
|
+
* if (this.listeners.size === 1) {
|
|
15
|
+
* // First subscriber - start listening
|
|
16
|
+
* this.startNetworkListener();
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* protected onUnsubscribe(): void {
|
|
21
|
+
* if (this.listeners.size === 0) {
|
|
22
|
+
* // Last subscriber left - stop listening
|
|
23
|
+
* this.stopNetworkListener();
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
|
|
32
|
+
export class Subscribable {
|
|
33
|
+
listeners = new Set();
|
|
34
|
+
constructor() {
|
|
35
|
+
this.subscribe = this.subscribe.bind(this);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to updates.
|
|
40
|
+
* @param listener - Callback function to receive updates
|
|
41
|
+
* @returns Unsubscribe function
|
|
42
|
+
*/
|
|
43
|
+
subscribe(listener) {
|
|
44
|
+
this.listeners.add(listener);
|
|
45
|
+
this.onSubscribe();
|
|
46
|
+
return () => {
|
|
47
|
+
this.listeners.delete(listener);
|
|
48
|
+
this.onUnsubscribe();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if there are any active listeners.
|
|
54
|
+
*/
|
|
55
|
+
hasListeners() {
|
|
56
|
+
return this.listeners.size > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the current number of listeners.
|
|
61
|
+
*/
|
|
62
|
+
getListenerCount() {
|
|
63
|
+
return this.listeners.size;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Notify all listeners with given arguments.
|
|
68
|
+
*/
|
|
69
|
+
notify(...args) {
|
|
70
|
+
this.listeners.forEach(listener => {
|
|
71
|
+
listener(...args);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Called when a listener is added.
|
|
77
|
+
* Override to start resources when first subscriber joins.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* protected onSubscribe(): void {
|
|
82
|
+
* if (this.listeners.size === 1) {
|
|
83
|
+
* this.startCapturing();
|
|
84
|
+
* }
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
onSubscribe() {
|
|
89
|
+
// Override in subclasses
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Called when a listener is removed.
|
|
94
|
+
* Override to stop resources when last subscriber leaves.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* protected onUnsubscribe(): void {
|
|
99
|
+
* if (this.listeners.size === 0) {
|
|
100
|
+
* this.stopCapturing();
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
onUnsubscribe() {
|
|
106
|
+
// Override in subclasses
|
|
107
|
+
}
|
|
108
|
+
}
|