@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.
Files changed (60) hide show
  1. package/lib/commonjs/clipboard/clipboard-impl.js +86 -10
  2. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  3. package/lib/commonjs/index.js +141 -59
  4. package/lib/commonjs/storage/devToolsStorageKeys.js +8 -1
  5. package/lib/commonjs/stores/BaseEventStore.js +257 -0
  6. package/lib/commonjs/stores/index.js +12 -0
  7. package/lib/commonjs/utils/index.js +64 -1
  8. package/lib/commonjs/utils/safeExpoRouter.js +141 -0
  9. package/lib/commonjs/utils/subscribable.js +113 -0
  10. package/lib/commonjs/utils/subscriberCountNotifier.js +72 -0
  11. package/lib/module/clipboard/clipboard-impl.js +85 -10
  12. package/lib/module/hooks/safe-area-impl.js +1 -1
  13. package/lib/module/index.js +10 -1
  14. package/lib/module/storage/devToolsStorageKeys.js +8 -1
  15. package/lib/module/stores/BaseEventStore.js +253 -0
  16. package/lib/module/stores/index.js +7 -0
  17. package/lib/module/utils/index.js +4 -1
  18. package/lib/module/utils/safeExpoRouter.js +132 -0
  19. package/lib/module/utils/subscribable.js +108 -0
  20. package/lib/module/utils/subscriberCountNotifier.js +66 -0
  21. package/lib/typescript/commonjs/clipboard/clipboard-impl.d.ts +15 -8
  22. package/lib/typescript/commonjs/clipboard/clipboard-impl.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  24. package/lib/typescript/commonjs/index.d.ts +2 -1
  25. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +2 -0
  27. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/stores/BaseEventStore.d.ts +145 -0
  29. package/lib/typescript/commonjs/stores/BaseEventStore.d.ts.map +1 -0
  30. package/lib/typescript/commonjs/stores/index.d.ts +5 -0
  31. package/lib/typescript/commonjs/stores/index.d.ts.map +1 -0
  32. package/lib/typescript/commonjs/utils/index.d.ts +3 -0
  33. package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts +17 -0
  35. package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts.map +1 -0
  36. package/lib/typescript/commonjs/utils/subscribable.d.ts +78 -0
  37. package/lib/typescript/commonjs/utils/subscribable.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/utils/subscriberCountNotifier.d.ts +47 -0
  39. package/lib/typescript/commonjs/utils/subscriberCountNotifier.d.ts.map +1 -0
  40. package/lib/typescript/module/clipboard/clipboard-impl.d.ts +15 -8
  41. package/lib/typescript/module/clipboard/clipboard-impl.d.ts.map +1 -1
  42. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  43. package/lib/typescript/module/index.d.ts +2 -1
  44. package/lib/typescript/module/index.d.ts.map +1 -1
  45. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +2 -0
  46. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  47. package/lib/typescript/module/stores/BaseEventStore.d.ts +145 -0
  48. package/lib/typescript/module/stores/BaseEventStore.d.ts.map +1 -0
  49. package/lib/typescript/module/stores/index.d.ts +5 -0
  50. package/lib/typescript/module/stores/index.d.ts.map +1 -0
  51. package/lib/typescript/module/utils/index.d.ts +3 -0
  52. package/lib/typescript/module/utils/index.d.ts.map +1 -1
  53. package/lib/typescript/module/utils/safeExpoRouter.d.ts +17 -0
  54. package/lib/typescript/module/utils/safeExpoRouter.d.ts.map +1 -0
  55. package/lib/typescript/module/utils/subscribable.d.ts +78 -0
  56. package/lib/typescript/module/utils/subscribable.d.ts.map +1 -0
  57. package/lib/typescript/module/utils/subscriberCountNotifier.d.ts +47 -0
  58. package/lib/typescript/module/utils/subscriberCountNotifier.d.ts.map +1 -0
  59. package/package.json +4 -7
  60. package/scripts/detect-clipboard.js +78 -126
@@ -1,20 +1,95 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Auto-generated clipboard implementation
5
- * Detected: none
6
- * Generated at: 2026-01-29T01:02:34.738Z
4
+ * Runtime clipboard implementation
7
5
  *
8
- * DO NOT EDIT - This file is generated by scripts/detect-clipboard.js
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
- * No clipboard library found. Install one of:
11
- * - expo-clipboard (for Expo projects)
12
- * - @react-native-clipboard/clipboard (for RN CLI projects)
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
- export const clipboardType = null;
16
- export const isClipboardAvailable = () => false;
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
- console.error("[RnBetterDevTools] Copy failed: No clipboard library found.\n" + `Attempted to copy: ${text.substring(0, 50)}${text.length > 50 ? "..." : ""}\n` + "Install expo-clipboard or @react-native-clipboard/clipboard, or provide a custom onCopy function.");
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
  };
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-01-29T01:02:34.764Z
6
+ * Generated at: 2026-02-10T20:29:38.101Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *
@@ -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 } from "./utils/index.js";
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
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Shared store utilities and base classes
5
+ */
6
+
7
+ export { BaseEventStore } from "./BaseEventStore.js";
@@ -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
+ }