@wayq/beekon-rn 0.0.1 → 0.0.5
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/BeekonRn.podspec +4 -4
- package/README.md +94 -48
- package/android/build.gradle +11 -6
- package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +428 -0
- package/android/src/main/java/{com → in}/wayq/beekonrn/BeekonRnPackage.kt +1 -1
- package/ios/BeekonRn.h +5 -1
- package/ios/BeekonRn.mm +90 -34
- package/ios/BeekonRn.swift +396 -116
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/BeekonKit +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Info.plist +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.abi.json +8636 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftinterface +236 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/PrivacyInfo.xcprivacy +1 -1
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/BeekonKit +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Info.plist +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.abi.json +8636 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +236 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +8636 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +236 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/PrivacyInfo.xcprivacy +1 -1
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +2 -14
- package/lib/module/NativeBeekonRn.js +22 -7
- package/lib/module/NativeBeekonRn.js.map +1 -1
- package/lib/module/beekon.js +209 -60
- package/lib/module/beekon.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/internal/mappers.js +145 -23
- package/lib/module/internal/mappers.js.map +1 -1
- package/lib/module/types/enums.js +2 -0
- package/lib/module/types/{preset.js.map → enums.js.map} +1 -1
- package/lib/module/types/error.js +25 -0
- package/lib/module/types/error.js.map +1 -0
- package/lib/module/types/geofence.js +2 -0
- package/lib/module/types/{position.js.map → geofence.js.map} +1 -1
- package/lib/module/types/location.js +4 -0
- package/lib/module/types/location.js.map +1 -0
- package/lib/module/types/sync.js +2 -0
- package/lib/module/types/sync.js.map +1 -0
- package/lib/typescript/src/NativeBeekonRn.d.ts +113 -35
- package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
- package/lib/typescript/src/beekon.d.ts +84 -49
- package/lib/typescript/src/beekon.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +7 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/internal/mappers.d.ts +16 -3
- package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
- package/lib/typescript/src/types/config.d.ts +53 -31
- package/lib/typescript/src/types/config.d.ts.map +1 -1
- package/lib/typescript/src/types/enums.d.ts +48 -0
- package/lib/typescript/src/types/enums.d.ts.map +1 -0
- package/lib/typescript/src/types/error.d.ts +20 -0
- package/lib/typescript/src/types/error.d.ts.map +1 -0
- package/lib/typescript/src/types/geofence.d.ts +36 -0
- package/lib/typescript/src/types/geofence.d.ts.map +1 -0
- package/lib/typescript/src/types/location.d.ts +40 -0
- package/lib/typescript/src/types/location.d.ts.map +1 -0
- package/lib/typescript/src/types/state.d.ts +18 -9
- package/lib/typescript/src/types/state.d.ts.map +1 -1
- package/lib/typescript/src/types/sync.d.ts +27 -0
- package/lib/typescript/src/types/sync.d.ts.map +1 -0
- package/package.json +5 -6
- package/scripts/fetch-beekonkit.sh +5 -2
- package/src/NativeBeekonRn.ts +120 -34
- package/src/beekon.ts +235 -63
- package/src/index.tsx +23 -4
- package/src/internal/mappers.ts +213 -22
- package/src/types/config.ts +54 -31
- package/src/types/enums.ts +64 -0
- package/src/types/error.ts +25 -0
- package/src/types/geofence.ts +37 -0
- package/src/types/location.ts +45 -0
- package/src/types/state.ts +23 -7
- package/src/types/sync.ts +23 -0
- package/android/src/main/java/com/wayq/beekonrn/BeekonRnModule.kt +0 -233
- package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeDirectory +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeRequirements +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeRequirements-1 +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeResources +0 -233
- package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeSignature +0 -0
- package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/_CodeSignature/CodeResources +0 -113
- package/lib/module/types/position.js +0 -2
- package/lib/module/types/preset.js +0 -2
- package/lib/typescript/src/types/position.d.ts +0 -24
- package/lib/typescript/src/types/position.d.ts.map +0 -1
- package/lib/typescript/src/types/preset.d.ts +0 -12
- package/lib/typescript/src/types/preset.d.ts.map +0 -1
- package/src/types/position.ts +0 -23
- package/src/types/preset.ts +0 -11
package/BeekonRn.podspec
CHANGED
|
@@ -10,10 +10,10 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.license = package["license"]
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
s.platforms = { :ios => "
|
|
16
|
-
s.source = { :git => "https://github.com/wayqteam/beekon.git", :tag => "v#{s.version}" }
|
|
13
|
+
# BeekonKit targets iOS 17.0 (uses CLLocationUpdate.liveUpdates). The
|
|
14
|
+
# consuming app must also target iOS 17.0 or higher.
|
|
15
|
+
s.platforms = { :ios => "17.0" }
|
|
16
|
+
s.source = { :git => "https://github.com/wayqteam/beekon-rn.git", :tag => "v#{s.version}" }
|
|
17
17
|
|
|
18
18
|
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
19
19
|
# Headers are only for the auto-generated `BeekonRn-Swift.h` interop — keep
|
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
React Native binding for the [Beekon](https://github.com/wayqteam/beekon) location SDK.
|
|
4
4
|
|
|
5
|
-
A thin pass-through over the native Android (`io.github.wayqteam:beekon`) and iOS (`BeekonKit`) libraries. The public TypeScript surface mirrors the Kotlin / Swift
|
|
5
|
+
A thin pass-through over the native Android (`io.github.wayqteam:beekon`) and iOS (`BeekonKit`) libraries at version **0.0.5**. The public TypeScript surface mirrors the Kotlin / Swift APIs in shape: a 12-method `Beekon` singleton, four callback streams (`state`, `locations`, `geofenceEvents`, `syncStatus`), geofencing, and optional server sync.
|
|
6
6
|
|
|
7
|
-
Built on the React Native New Architecture (TurboModules + Codegen). The binding
|
|
7
|
+
Built on the React Native New Architecture (TurboModules + Codegen). The binding contains no location logic — all tracking, persistence, geofencing, and OS integration live natively. **Writes never cross into JavaScript**: in the background the JS engine isn't guaranteed to be alive, so the native libraries own the persistence path end-to-end.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
There is **no `initialize()`** — the native SDKs auto-initialize. `start()` / `stop()` **never throw**: subscribe to `onState` to learn whether tracking is active and why it stopped.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
13
|
| Area | Floor |
|
|
14
14
|
|---|---|
|
|
15
15
|
| React Native | 0.76+ (target 0.85) |
|
|
16
|
-
| iOS |
|
|
17
|
-
| Android | API
|
|
16
|
+
| iOS | 17.0 |
|
|
17
|
+
| Android | **API 26** (required by the native AAR) |
|
|
18
18
|
| New Architecture | required (Old Arch is unsupported) |
|
|
19
19
|
|
|
20
20
|
## Install
|
|
@@ -25,19 +25,17 @@ npm install @wayq/beekon-rn
|
|
|
25
25
|
yarn add @wayq/beekon-rn
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
iOS:
|
|
28
|
+
iOS — the package fetches and bundles `BeekonKit.xcframework` (SHA256-verified) during `prepare`, so just install pods:
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
31
|
cd ios && pod install
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Android: Maven Central is auto-included by the autolinker. The native AAR (`io.github.wayqteam:beekon`) is pulled transitively.
|
|
34
|
+
Android — Maven Central is auto-included by the autolinker; the native AAR (`io.github.wayqteam:beekon`) is pulled transitively.
|
|
37
35
|
|
|
38
36
|
## Permissions
|
|
39
37
|
|
|
40
|
-
The library does NOT request permissions
|
|
38
|
+
The library does NOT request permissions — your app must (e.g. via [`react-native-permissions`](https://github.com/zoontek/react-native-permissions) or `PermissionsAndroid`). Foreground location works without background location.
|
|
41
39
|
|
|
42
40
|
**Android** (`AndroidManifest.xml`):
|
|
43
41
|
|
|
@@ -47,75 +45,123 @@ The library does NOT request permissions itself — your app must. Use a library
|
|
|
47
45
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
48
46
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
|
49
47
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
48
|
+
<!-- Only when BeekonConfig.detectActivity is enabled -->
|
|
49
|
+
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
**iOS** (`Info.plist`):
|
|
53
53
|
|
|
54
54
|
```xml
|
|
55
55
|
<key>NSLocationWhenInUseUsageDescription</key>
|
|
56
|
-
<string
|
|
56
|
+
<string>…</string>
|
|
57
57
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
58
|
-
<string
|
|
58
|
+
<string>…</string>
|
|
59
|
+
<!-- Only when BeekonConfig.detectActivity is enabled -->
|
|
60
|
+
<key>NSMotionUsageDescription</key>
|
|
61
|
+
<string>…</string>
|
|
59
62
|
<key>UIBackgroundModes</key>
|
|
60
|
-
<array
|
|
63
|
+
<array>
|
|
64
|
+
<string>location</string>
|
|
65
|
+
<string>fetch</string>
|
|
66
|
+
</array>
|
|
67
|
+
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
68
|
+
<array>
|
|
69
|
+
<string>in.wayq.beekon.sync</string>
|
|
70
|
+
</array>
|
|
61
71
|
```
|
|
62
72
|
|
|
73
|
+
## Background-task registration (iOS)
|
|
74
|
+
|
|
75
|
+
For background sync scheduling and cold-launch resume, register Beekon's background task **synchronously during app launch** (it must run before `didFinishLaunchingWithOptions` returns). In your `AppDelegate.swift`:
|
|
76
|
+
|
|
77
|
+
```swift
|
|
78
|
+
import BeekonRn
|
|
79
|
+
|
|
80
|
+
func application(_ application: UIApplication,
|
|
81
|
+
didFinishLaunchingWithOptions launchOptions: …) -> Bool {
|
|
82
|
+
BeekonRnImpl.registerBackgroundTasks()
|
|
83
|
+
// … React Native setup …
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Foreground tracking and the JS streams work without this; only background sync / relaunch need it.
|
|
89
|
+
|
|
90
|
+
## Cold-launch resume (Android)
|
|
91
|
+
|
|
92
|
+
To resume tracking after the OS killed the process, call `in.wayq.beekon.Beekon.start()` from your `Application.onCreate` (native — the JS engine may not be running on a background relaunch). From JS, `Beekon.resumeIfNeeded()` covers the foreground case.
|
|
93
|
+
|
|
63
94
|
## Usage
|
|
64
95
|
|
|
65
96
|
```ts
|
|
66
|
-
import { Beekon, type BeekonState, type
|
|
97
|
+
import { Beekon, type BeekonState, type Location } from '@wayq/beekon-rn';
|
|
67
98
|
|
|
68
|
-
// Subscribe — returns an unsubscribe function.
|
|
99
|
+
// Subscribe — each returns an unsubscribe function. `onState` / `onSyncStatus`
|
|
100
|
+
// replay the latest value to new subscribers; `onLocation` / `onGeofenceEvent`
|
|
101
|
+
// are broadcast (no replay).
|
|
69
102
|
const offState = Beekon.onState((s: BeekonState) => console.log('state', s));
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
// Initialize once at startup.
|
|
73
|
-
await Beekon.init();
|
|
103
|
+
const offLoc = Beekon.onLocation((l: Location) => console.log('location', l));
|
|
74
104
|
|
|
75
|
-
// Configure (
|
|
105
|
+
// Configure (optional; defaults below). Pass 0 to disable a gate.
|
|
76
106
|
await Beekon.configure({
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
minTimeBetweenLocationsSeconds: 30, // default 30
|
|
108
|
+
minDistanceBetweenLocationsMeters: 100, // default 100
|
|
109
|
+
accuracyMode: 'balanced', // 'high' | 'balanced' | 'low'
|
|
110
|
+
whenStationary: 'pause', // 'keepTracking' | 'pause' | 'pauseWithCheckIns'
|
|
111
|
+
detectActivity: false,
|
|
112
|
+
// Optional server upload:
|
|
113
|
+
sync: {
|
|
114
|
+
url: 'https://example.com/ingest',
|
|
115
|
+
headers: { Authorization: 'Bearer …' },
|
|
85
116
|
},
|
|
117
|
+
// Android-only foreground-service notification:
|
|
118
|
+
notification: { title: 'Tracking', text: 'Recording your route' },
|
|
86
119
|
});
|
|
87
120
|
|
|
88
|
-
await Beekon.start();
|
|
89
|
-
//
|
|
121
|
+
await Beekon.start(); // never throws — observe onState
|
|
122
|
+
// … later
|
|
90
123
|
await Beekon.stop();
|
|
91
124
|
|
|
92
|
-
//
|
|
93
|
-
const
|
|
125
|
+
// History (local store; source of truth even when JS was asleep).
|
|
126
|
+
const fixes = await Beekon.getLocations(new Date(Date.now() - 3600_000), new Date());
|
|
94
127
|
|
|
95
128
|
// Cleanup.
|
|
96
129
|
offState();
|
|
97
|
-
|
|
130
|
+
offLoc();
|
|
98
131
|
```
|
|
99
132
|
|
|
100
133
|
## API
|
|
101
134
|
|
|
102
135
|
| Method | Description |
|
|
103
136
|
|---|---|
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
| `configure(config)` | Set config. Optional, idempotent. Does not throw. |
|
|
138
|
+
| `start()` | Begin tracking. Never throws — observe `onState`. |
|
|
139
|
+
| `stop()` | Stop tracking. Idempotent. Never throws. |
|
|
140
|
+
| `resumeIfNeeded()` | Resume a session active before the app was terminated. |
|
|
141
|
+
| `getLocations(from, to)` | Read persisted fixes in `[from, to]`. Throws `'storage'`. |
|
|
142
|
+
| `deleteLocations(before?)` | Delete fixes at/before `before` (all when omitted). Throws `'storage'`. |
|
|
143
|
+
| `pendingUploadCount()` | Count of fixes not yet uploaded. Throws `'storage'`. |
|
|
144
|
+
| `sync()` | Request an immediate upload (no-op if sync unconfigured). |
|
|
145
|
+
| `setExtras(extras)` | Custom key/value fields sent with every upload. |
|
|
146
|
+
| `addGeofences(geofences)` | Register geofences. Throws `'invalidGeofence'`. |
|
|
147
|
+
| `removeGeofences(ids)` | Unregister geofences by id. |
|
|
148
|
+
| `listGeofences()` | The currently registered geofences. |
|
|
149
|
+
| `onState(cb)` | Subscribe to state. Replay-1. Returns unsubscribe fn. |
|
|
150
|
+
| `onLocation(cb)` | Subscribe to gated fixes. No replay. |
|
|
151
|
+
| `onGeofenceEvent(cb)` | Subscribe to geofence enter/exit. No replay. |
|
|
152
|
+
| `onSyncStatus(cb)` | Subscribe to upload health. Replay-1. |
|
|
153
|
+
|
|
154
|
+
`BeekonState` is a discriminated union on `kind`: `'idle' | 'tracking' | 'stopped'`. The `'stopped'` variant carries `reason: 'user' | 'permissionDenied' | 'locationServicesDisabled' | 'locationUnavailable' | 'system'`.
|
|
155
|
+
|
|
156
|
+
`SyncStatus` is a union on `kind`: `'idle' | 'pending' | 'failed'`; `'failed'` carries `reason: 'auth' | 'rejected'`.
|
|
157
|
+
|
|
158
|
+
`Location` carries `id`, `latitude`, `longitude`, `timestamp`, the nullable `accuracy` / `speed` / `bearing` / `altitude`, plus `quality`, `trigger`, `motion`, `activity` (`null` unless `detectActivity`), and `isMock`. Optional numeric fields are `null` when the OS did not report a value — never `0`.
|
|
159
|
+
|
|
160
|
+
The **only** thrown errors are `BeekonError` instances with `kind: 'storage' | 'invalidGeofence'`. Permission / services / lifecycle problems are not thrown — they surface on `onState` as `stopped(reason)`.
|
|
161
|
+
|
|
162
|
+
## Storage, retention, and sync
|
|
163
|
+
|
|
164
|
+
The native SDKs persist every gated fix locally (Room on Android, GRDB on iOS) — JS is a passive reader. Retention: **TTL 7 days OR the most recent 100 K rows**, whichever is smaller; auto-pruned on each write batch. With `sync` configured, accepted rows are deleted locally after upload, so `getLocations` / `pendingUploadCount` return only un-uploaded fixes.
|
|
119
165
|
|
|
120
166
|
## License
|
|
121
167
|
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
buildscript {
|
|
2
2
|
ext.BeekonRn = [
|
|
3
|
-
kotlinVersion: "2.
|
|
4
|
-
|
|
3
|
+
kotlinVersion: "2.1.20",
|
|
4
|
+
// minSdk 26 is required by the native Beekon AAR (io.github.wayqteam:beekon).
|
|
5
|
+
minSdkVersion: 26,
|
|
5
6
|
compileSdkVersion: 36,
|
|
6
7
|
targetSdkVersion: 36
|
|
7
8
|
]
|
|
@@ -33,7 +34,7 @@ apply plugin: "kotlin-android"
|
|
|
33
34
|
apply plugin: "com.facebook.react"
|
|
34
35
|
|
|
35
36
|
android {
|
|
36
|
-
namespace "
|
|
37
|
+
namespace "in.wayq.beekonrn"
|
|
37
38
|
|
|
38
39
|
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
40
|
|
|
@@ -57,8 +58,12 @@ android {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
compileOptions {
|
|
60
|
-
sourceCompatibility JavaVersion.
|
|
61
|
-
targetCompatibility JavaVersion.
|
|
61
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
62
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
kotlinOptions {
|
|
66
|
+
jvmTarget = "17"
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -72,7 +77,7 @@ dependencies {
|
|
|
72
77
|
// Native Beekon SDK — published from beekon-android/ via Maven Central.
|
|
73
78
|
// Pinned exact in v0.x to avoid surprise breakage; loosen to a range when
|
|
74
79
|
// the SDK reaches v1 stability.
|
|
75
|
-
implementation "io.github.wayqteam:beekon:0.0.
|
|
80
|
+
implementation "io.github.wayqteam:beekon:0.0.5"
|
|
76
81
|
// Kotlin coroutines — required for collecting Beekon's StateFlow/SharedFlow.
|
|
77
82
|
// Beekon already depends on coroutines transitively, but declaring it here
|
|
78
83
|
// makes the dependency intent explicit.
|