@tryterra/react-native 2.1.0 → 2.2.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.
Files changed (59) hide show
  1. package/README.md +31 -0
  2. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +103 -14
  3. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
  4. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +47 -5
  5. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +1 -3
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +89 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +672 -0
  8. package/docs/availability.md +154 -4
  9. package/frameworks/TerraSDK.xcframework/Info.plist +5 -5
  10. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  11. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +1385 -226
  12. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +37 -1
  13. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +37 -1
  14. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  15. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +37 -1
  16. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  17. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +2 -2
  18. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  19. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6589 -6468
  20. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  21. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +1385 -226
  22. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +37 -1
  23. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +37 -1
  24. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  25. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +37 -1
  26. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +1385 -226
  27. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +37 -1
  28. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +37 -1
  29. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +37 -1
  31. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +21 -21
  33. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +2 -2
  34. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6590 -6469
  36. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6508 -6387
  37. package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
  38. package/ios/TerraBridgeCore/Conversions.swift +129 -2
  39. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +172 -0
  40. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +734 -1
  41. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +64 -0
  42. package/lib/Terra.d.ts +74 -5
  43. package/lib/Terra.js +69 -13
  44. package/lib/generated/client.d.ts +37 -1
  45. package/lib/generated/client.js +60 -0
  46. package/lib/generated/codecs.d.ts +85 -1
  47. package/lib/generated/codecs.js +710 -2
  48. package/lib/generated/ops.d.ts +1 -1
  49. package/lib/generated/ops.js +24 -0
  50. package/lib/generated/types.d.ts +314 -1
  51. package/lib/generated/types.js +133 -1
  52. package/lib/index.d.ts +1 -1
  53. package/package.json +3 -3
  54. package/src/Terra.ts +146 -16
  55. package/src/generated/client.ts +74 -2
  56. package/src/generated/codecs.ts +705 -3
  57. package/src/generated/ops.ts +36 -0
  58. package/src/generated/types.ts +556 -1
  59. package/src/index.ts +2 -0
@@ -43,6 +43,11 @@ struct TerraBridgeOpsImpl: TerraBridgeOps {
43
43
  ConnectionList(connections: try await terra.connections.map(Conversions.connection))
44
44
  }
45
45
 
46
+ func clientAvailability(_ request: SourceRequest) async throws -> AvailabilityResult {
47
+ AvailabilityResult(availability: Conversions.availability(
48
+ await terra.availability(for: try Conversions.source(request.source))))
49
+ }
50
+
46
51
  func clientDisconnect(_ request: SourceRequest) async throws -> Empty {
47
52
  try await connection(request.source).disconnect()
48
53
  return Empty()
@@ -127,6 +132,16 @@ struct TerraBridgeOpsImpl: TerraBridgeOps {
127
132
  .map(Conversions.heartRate))
128
133
  }
129
134
 
135
+ func bodyRestingHeartRate(_ request: RangeRequest) async throws -> RestingHeartRateList {
136
+ RestingHeartRateList(samples: try await terra.body.restingHeartRate(in: Conversions.timeRange(request.range, at: "$.range"))
137
+ .map(Conversions.restingHeartRate))
138
+ }
139
+
140
+ func bodyReadings(_ request: RangeRequest) async throws -> BodyReadingList {
141
+ BodyReadingList(samples: try await terra.body.readings(in: Conversions.timeRange(request.range, at: "$.range"))
142
+ .compactMap(Conversions.bodyReading))
143
+ }
144
+
130
145
  func bodyLogHeartRate(_ request: HeartRateEntryList) async throws -> RecordIdList {
131
146
  ids(try await terra.body.log(entries(request.entries, Conversions.entry)))
132
147
  }
@@ -147,6 +162,16 @@ struct TerraBridgeOpsImpl: TerraBridgeOps {
147
162
  .map(Conversions.steps))
148
163
  }
149
164
 
165
+ func dailyTotals(_ request: RangeRequest) async throws -> DayTotalList {
166
+ DayTotalList(totals: try await terra.daily.totals(in: Conversions.timeRange(request.range, at: "$.range"))
167
+ .compactMap(Conversions.dayTotal))
168
+ }
169
+
170
+ func dailyIntraday(_ request: RangeRequest) async throws -> IntradaySampleList {
171
+ IntradaySampleList(samples: try await terra.daily.intraday(in: Conversions.timeRange(request.range, at: "$.range"))
172
+ .compactMap(Conversions.intraday))
173
+ }
174
+
150
175
  func dailyLog(_ request: StepEntryList) async throws -> RecordIdList {
151
176
  ids(try await terra.daily.log(entries(request.entries, Conversions.entry)))
152
177
  }
@@ -179,6 +204,16 @@ struct TerraBridgeOpsImpl: TerraBridgeOps {
179
204
  .map(Conversions.workout))
180
205
  }
181
206
 
207
+ func activityRoutes(_ request: RangeRequest) async throws -> WorkoutRouteList {
208
+ WorkoutRouteList(routes: try await terra.activity.routes(in: Conversions.timeRange(request.range, at: "$.range"))
209
+ .map(Conversions.route))
210
+ }
211
+
212
+ func activityIntensity(_ request: RangeRequest) async throws -> WorkoutIntensityList {
213
+ WorkoutIntensityList(samples: try await terra.activity.intensity(in: Conversions.timeRange(request.range, at: "$.range"))
214
+ .compactMap(Conversions.intensity))
215
+ }
216
+
182
217
  func activityLog(_ request: WorkoutEntryList) async throws -> RecordIdList {
183
218
  ids(try await terra.activity.log(entries(request.entries, Conversions.entry)))
184
219
  }
@@ -190,6 +225,11 @@ struct TerraBridgeOpsImpl: TerraBridgeOps {
190
225
 
191
226
  // MARK: nutrition
192
227
 
228
+ func nutritionQuantities(_ request: RangeRequest) async throws -> DietaryQuantityList {
229
+ DietaryQuantityList(samples: try await terra.nutrition.quantities(in: Conversions.timeRange(request.range, at: "$.range"))
230
+ .compactMap(Conversions.quantity))
231
+ }
232
+
193
233
  func nutritionLog(_ request: NutritionEntryList) async throws -> RecordIdList {
194
234
  ids(try await terra.nutrition.log(entries(request.entries, Conversions.entry)))
195
235
  }
@@ -199,6 +239,30 @@ struct TerraBridgeOpsImpl: TerraBridgeOps {
199
239
  return Empty()
200
240
  }
201
241
 
242
+ func nutritionHydration(_ request: RangeRequest) async throws -> HydrationList {
243
+ HydrationList(samples: try await terra.nutrition.hydration(in: Conversions.timeRange(request.range, at: "$.range"))
244
+ .map(Conversions.hydration))
245
+ }
246
+
247
+ // MARK: menstruation
248
+
249
+ func menstruationFlow(_ request: RangeRequest) async throws -> MenstruationFlowDayList {
250
+ MenstruationFlowDayList(samples: try await terra.menstruation.flow(in: Conversions.timeRange(request.range, at: "$.range"))
251
+ .map(Conversions.flowDay))
252
+ }
253
+
254
+ func menstruationEvents(_ request: RangeRequest) async throws -> MenstrualEventList {
255
+ MenstrualEventList(samples: try await terra.menstruation.events(in: Conversions.timeRange(request.range, at: "$.range"))
256
+ .compactMap(Conversions.menstrualEvent))
257
+ }
258
+
259
+ // MARK: athlete
260
+
261
+ func athleteProfile(_ request: Empty) async throws -> AthleteProfileResult {
262
+ AthleteProfileResult(profile: try await terra.athlete.profile()
263
+ .map { Conversions.athleteProfile($0, zone: .current) })
264
+ }
265
+
202
266
  // MARK: planned workouts
203
267
 
204
268
  func plannedWorkoutsPlanned(_ request: Empty) async throws -> PlannedWorkoutList {
package/lib/Terra.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * here, so this file cannot quietly fall behind the surface the way the 1.x wrapper did.
9
9
  */
10
10
  import { TerraError } from "./TerraError";
11
- import type { BodyEntry, BootstrapState, Connection, DataCategory, DiagnosticsState, EndUser, HealthAccess, HealthChange, HeartRateEntry, HeartRateSample, NutritionEntry, PermissionStatus, PlannedWorkout, WorkoutSchedulingStatus, SleepEntry, SleepSegment, Source, StepCount, StepEntry, SupportReport, TerraEvent, TimeRange, Workout, WorkoutEntry } from "./generated/types";
11
+ import type { AthleteProfile, BodyEntry, BodyReading, BootstrapState, Connection, DataCategory, DayTotal, DiagnosticsState, DietaryQuantity, EndUser, HealthAccess, HealthChange, HeartRateEntry, HeartRateSample, HydrationSample, IntradaySample, MenstrualEvent, MenstruationFlowDay, NutritionEntry, PermissionStatus, PlannedWorkout, RestingHeartRateSample, StoreAvailability, WorkoutSchedulingStatus, SleepEntry, SleepSegment, Source, StepCount, StepEntry, SupportReport, TerraEvent, TimeRange, Workout, WorkoutEntry, WorkoutIntensitySample, WorkoutRoute } from "./generated/types";
12
12
  /** What the facade needs from the native module. `NativeTerraBridge` satisfies it; so does a double. */
13
13
  export interface NativeBridge {
14
14
  invoke(op: string, payload: string): Promise<string>;
@@ -77,12 +77,31 @@ interface Changes {
77
77
  }
78
78
  export interface BodyMetrics extends Changes {
79
79
  heartRate(range: TimeRange): Promise<readonly HeartRateSample[]>;
80
+ /**
81
+ * The store's own resting heart rate, one per day it computed, never derived from `heartRate`.
82
+ * Health Connect grants it under `daily`, so on Android ask for `daily` as well as `body`.
83
+ */
84
+ restingHeartRate(range: TimeRange): Promise<readonly RestingHeartRateSample[]>;
85
+ /**
86
+ * Weight, height, BMI (iOS), body fat, lean mass, heart rate variability, VO2 max, blood pressure,
87
+ * blood glucose, oxygen saturation, temperature and respiratory rate. Each reading's `kind` says
88
+ * what it measured and fixes the unit; blood glucose keeps the unit its writer chose.
89
+ */
90
+ readings(range: TimeRange): Promise<readonly BodyReading[]>;
80
91
  logHeartRate(entries: readonly HeartRateEntry[]): Promise<readonly string[]>;
81
92
  log(entries: readonly BodyEntry[]): Promise<readonly string[]>;
82
93
  delete(id: string): Promise<void>;
83
94
  }
84
95
  export interface DailyMetrics extends Changes {
85
96
  steps(range: TimeRange): Promise<readonly StepCount[]>;
97
+ /**
98
+ * The store's own total for each whole local day the range touches: distance, active and basal
99
+ * energy, flights climbed and, on iOS, exercise minutes. A day with nothing recorded has no total
100
+ * rather than a zero. Steps are `steps`, which keeps writers apart.
101
+ */
102
+ totals(range: TimeRange): Promise<readonly DayTotal[]>;
103
+ /** The active energy, basal energy (iOS), distance and floors records the totals are made of. */
104
+ intraday(range: TimeRange): Promise<readonly IntradaySample[]>;
86
105
  log(entries: readonly StepEntry[]): Promise<readonly string[]>;
87
106
  delete(id: string): Promise<void>;
88
107
  }
@@ -93,14 +112,47 @@ export interface SleepMetrics extends Changes {
93
112
  }
94
113
  export interface ActivityMetrics extends Changes {
95
114
  workouts(range: TimeRange): Promise<readonly Workout[]>;
115
+ /** The GPS tracks of the workouts in the range, each naming its workout by `workoutId`. */
116
+ routes(range: TimeRange): Promise<readonly WorkoutRoute[]>;
117
+ /**
118
+ * Speed, power and cycling cadence readings in the range. iOS names each one's workout; Health
119
+ * Connect stores them free-standing, so on Android `workoutId` is absent and `start` equals `end`.
120
+ */
121
+ intensity(range: TimeRange): Promise<readonly WorkoutIntensitySample[]>;
96
122
  log(entries: readonly WorkoutEntry[]): Promise<readonly string[]>;
97
123
  delete(id: string): Promise<void>;
98
124
  }
99
- /** Write-only on both platforms. */
100
125
  export interface NutritionMetrics {
126
+ /**
127
+ * One quantity per nutrient a meal recorded in the range, not grouped into meals. Quantities of
128
+ * one meal share `foodCorrelationId`. Water is not among them. Each `value` is in its nutrient's
129
+ * fixed unit: kcal for `energyConsumed`, grams for the macronutrients, milligrams for the rest.
130
+ */
131
+ quantities(range: TimeRange): Promise<readonly DietaryQuantity[]>;
132
+ /** Water drunk in the range, one sample per drink, in millilitres. */
133
+ hydration(range: TimeRange): Promise<readonly HydrationSample[]>;
101
134
  log(entries: readonly NutritionEntry[]): Promise<readonly string[]>;
102
135
  delete(id: string): Promise<void>;
103
136
  }
137
+ /** Read-only on both platforms: neither SDK has a write route for menstruation. */
138
+ export interface MenstruationMetrics {
139
+ /**
140
+ * Logged flow days overlapping the range, one per day, each bounded by that day's local
141
+ * midnights. A day logged with no flow comes back as `none` rather than being left out.
142
+ */
143
+ flow(range: TimeRange): Promise<readonly MenstruationFlowDay[]>;
144
+ /** Ovulation tests, cervical mucus and intermenstrual bleeding logged in the range. */
145
+ events(range: TimeRange): Promise<readonly MenstrualEvent[]>;
146
+ }
147
+ /** iOS only, and read-only. */
148
+ export interface AthleteMetrics {
149
+ /**
150
+ * HealthKit's characteristics as one snapshot. No range, because characteristics have no
151
+ * history. `undefined` where the store has no notion of them at all; a store that has them and
152
+ * holds no values answers a profile whose `biologicalSex` is `notSet`.
153
+ */
154
+ profile(): Promise<AthleteProfile | undefined>;
155
+ }
104
156
  /** iOS only. */
105
157
  export interface PlannedWorkouts {
106
158
  planned(): Promise<readonly PlannedWorkout[]>;
@@ -124,12 +176,20 @@ export declare class Terra {
124
176
  readonly sleep: SleepMetrics;
125
177
  readonly activity: ActivityMetrics;
126
178
  readonly nutrition: NutritionMetrics;
179
+ readonly menstruation: MenstruationMetrics;
180
+ /** `undefined` on Android, which has no characteristics to read. */
181
+ readonly athlete: AthleteMetrics | undefined;
127
182
  /** `undefined` on Android, which has no planned workouts. */
128
183
  readonly plannedWorkouts: PlannedWorkouts | undefined;
129
184
  private readonly ops;
130
185
  private readonly streams;
131
- /** Settles once the token provider, when there is one, can be asked. */
132
- private readonly tokensReady;
186
+ private readonly tokenProvider;
187
+ private readonly tokenTimeoutMs;
188
+ /**
189
+ * The provider's registration, in flight or done. `undefined` before it starts and after it failed
190
+ * or its stream ended, so the next connect that needs it registers again.
191
+ */
192
+ private tokenSetup;
133
193
  constructor(native: NativeBridge, platform: Platform, options?: TerraOptions);
134
194
  /**
135
195
  * The native client this runtime is attached to. Rejects with `bridge.not_bootstrapped`, carrying
@@ -148,7 +208,16 @@ export declare class Terra {
148
208
  }): Promise<Connection>;
149
209
  connections(): Promise<readonly Connection[]>;
150
210
  disconnect(source?: Source): Promise<void>;
151
- /** Answers the native broker's token requests with `provider`, for the lifetime of this runtime. */
211
+ /**
212
+ * Whether the device can serve `source` at all, and if not, why. Needs no connection and prompts
213
+ * for nothing, so an app can decide whether to offer the link before `connect`. iOS answers
214
+ * `available` or `unsupportedDevice`; Android names the reason, such as `providerAppMissing` when
215
+ * Health Connect needs installing.
216
+ */
217
+ availability(source?: Source): Promise<StoreAvailability>;
218
+ /** Settles once the token provider, when there is one, can be asked. */
219
+ private tokensReady;
220
+ /** Answers the native broker's token requests with `provider` until the stream ends. */
152
221
  private answerTokens;
153
222
  }
154
223
  export {};
package/lib/Terra.js CHANGED
@@ -21,11 +21,11 @@ class Terra {
21
21
  const ops = new client_1.GeneratedClient(invoker);
22
22
  this.ops = ops;
23
23
  this.streams = new StreamHub(native);
24
- this.tokensReady = options.tokenProvider === undefined
25
- ? Promise.resolve()
26
- : this.answerTokens(options.tokenProvider, options.tokenTimeoutMs ?? 30000);
27
- // The runtime can report an unhandled rejection before a later call awaits this promise.
28
- this.tokensReady.catch(() => undefined);
24
+ this.tokenProvider = options.tokenProvider;
25
+ this.tokenTimeoutMs = options.tokenTimeoutMs ?? 30000;
26
+ // Registered now, so the provider is in place before anything asks. A failure here is reported
27
+ // to the connect that needs the provider, which also tries again.
28
+ void this.tokensReady();
29
29
  const on = (op) => ops_1.availableOn[op].includes(platform);
30
30
  const stream = (id) => (listener, onError) => this.streams.subscribe(id, listener, onError);
31
31
  this.permissions = {
@@ -66,6 +66,8 @@ class Terra {
66
66
  };
67
67
  this.body = {
68
68
  heartRate: async (range) => (await ops.bodyHeartRate({ range })).samples,
69
+ restingHeartRate: async (range) => (await ops.bodyRestingHeartRate({ range })).samples,
70
+ readings: async (range) => (await ops.bodyReadings({ range })).samples,
69
71
  logHeartRate: async (entries) => (await ops.bodyLogHeartRate({ entries })).ids,
70
72
  log: async (entries) => (await ops.bodyLog({ entries })).ids,
71
73
  delete: async (id) => {
@@ -75,6 +77,8 @@ class Terra {
75
77
  };
76
78
  this.daily = {
77
79
  steps: async (range) => (await ops.dailySteps({ range })).samples,
80
+ totals: async (range) => (await ops.dailyTotals({ range })).totals,
81
+ intraday: async (range) => (await ops.dailyIntraday({ range })).samples,
78
82
  log: async (entries) => (await ops.dailyLog({ entries })).ids,
79
83
  delete: async (id) => {
80
84
  await ops.dailyDelete({ id });
@@ -91,6 +95,8 @@ class Terra {
91
95
  };
92
96
  this.activity = {
93
97
  workouts: async (range) => (await ops.activityWorkouts({ range })).samples,
98
+ routes: async (range) => (await ops.activityRoutes({ range })).routes,
99
+ intensity: async (range) => (await ops.activityIntensity({ range })).samples,
94
100
  log: async (entries) => (await ops.activityLog({ entries })).ids,
95
101
  delete: async (id) => {
96
102
  await ops.activityDelete({ id });
@@ -98,11 +104,20 @@ class Terra {
98
104
  changes: stream("activity.changes"),
99
105
  };
100
106
  this.nutrition = {
107
+ quantities: async (range) => (await ops.nutritionQuantities({ range })).samples,
108
+ hydration: async (range) => (await ops.nutritionHydration({ range })).samples,
101
109
  log: async (entries) => (await ops.nutritionLog({ entries })).ids,
102
110
  delete: async (id) => {
103
111
  await ops.nutritionDelete({ id });
104
112
  },
105
113
  };
114
+ this.menstruation = {
115
+ flow: async (range) => (await ops.menstruationFlow({ range })).samples,
116
+ events: async (range) => (await ops.menstruationEvents({ range })).samples,
117
+ };
118
+ this.athlete = on("athlete.profile")
119
+ ? { profile: async () => (await ops.athleteProfile({})).profile }
120
+ : undefined;
106
121
  this.plannedWorkouts = on("plannedWorkouts.planned")
107
122
  ? {
108
123
  planned: async () => (await ops.plannedWorkoutsPlanned({})).workouts,
@@ -132,7 +147,9 @@ class Terra {
132
147
  * the `tokenProvider` is asked, and only when a new link is actually needed.
133
148
  */
134
149
  async connect(source, endUser, options = {}) {
135
- await this.tokensReady;
150
+ // A connect that carries its token never asks the provider, so it does not wait on it either.
151
+ if (options.token === undefined)
152
+ await this.tokensReady();
136
153
  return this.ops.clientConnect({ source, endUser, token: options.token });
137
154
  }
138
155
  async connections() {
@@ -141,9 +158,36 @@ class Terra {
141
158
  async disconnect(source) {
142
159
  await this.ops.clientDisconnect({ source });
143
160
  }
144
- /** Answers the native broker's token requests with `provider`, for the lifetime of this runtime. */
145
- async answerTokens(provider, timeoutMs) {
146
- await this.streams.open("bridge.tokenRequest", ({ requestId }) => {
161
+ /**
162
+ * Whether the device can serve `source` at all, and if not, why. Needs no connection and prompts
163
+ * for nothing, so an app can decide whether to offer the link before `connect`. iOS answers
164
+ * `available` or `unsupportedDevice`; Android names the reason, such as `providerAppMissing` when
165
+ * Health Connect needs installing.
166
+ */
167
+ async availability(source) {
168
+ return (await this.ops.clientAvailability({ source })).availability;
169
+ }
170
+ /** Settles once the token provider, when there is one, can be asked. */
171
+ tokensReady() {
172
+ const provider = this.tokenProvider;
173
+ if (provider === undefined)
174
+ return Promise.resolve();
175
+ if (this.tokenSetup !== undefined)
176
+ return this.tokenSetup;
177
+ const forget = () => {
178
+ if (this.tokenSetup === setup)
179
+ this.tokenSetup = undefined;
180
+ };
181
+ const setup = this.answerTokens(provider, this.tokenTimeoutMs, forget);
182
+ this.tokenSetup = setup;
183
+ // Handles the failure, so it is never an unhandled rejection, and forgets the setup so the next
184
+ // connect tries again rather than rethrowing a failure that may have passed.
185
+ setup.catch(forget);
186
+ return setup;
187
+ }
188
+ /** Answers the native broker's token requests with `provider` until the stream ends. */
189
+ async answerTokens(provider, timeoutMs, onEnd) {
190
+ const id = await this.streams.open("bridge.tokenRequest", ({ requestId }) => {
147
191
  void (async () => {
148
192
  let token;
149
193
  try {
@@ -156,8 +200,15 @@ class Terra {
156
200
  }
157
201
  await this.ops.tokenResolve({ requestId, token }).catch(() => undefined);
158
202
  })();
159
- });
160
- await this.ops.tokenSetProviderPresent({ present: true, timeoutMs });
203
+ }, undefined, onEnd);
204
+ try {
205
+ await this.ops.tokenSetProviderPresent({ present: true, timeoutMs });
206
+ }
207
+ catch (failed) {
208
+ // A provider the broker was never told about answers nothing, so its stream is let go of.
209
+ this.streams.close(id);
210
+ throw failed;
211
+ }
161
212
  }
162
213
  }
163
214
  exports.Terra = Terra;
@@ -168,8 +219,11 @@ class StreamHub {
168
219
  this.early = new Map();
169
220
  this.pending = 0;
170
221
  }
171
- /** Subscribes and answers once the native stream is open. */
172
- async open(stream, listener, onError) {
222
+ /**
223
+ * Subscribes and answers once the native stream is open. `onEnd` runs once the stream ends from the
224
+ * native side, by error or by completing; not when it is closed from here.
225
+ */
226
+ async open(stream, listener, onError, onEnd) {
173
227
  this.listen();
174
228
  this.pending += 1;
175
229
  let id;
@@ -189,9 +243,11 @@ class StreamHub {
189
243
  case "error":
190
244
  this.ended(id);
191
245
  onError?.(new TerraError_1.TerraError(delivery.error));
246
+ onEnd?.();
192
247
  return;
193
248
  case "complete":
194
249
  this.ended(id);
250
+ onEnd?.();
195
251
  return;
196
252
  }
197
253
  };
@@ -1,4 +1,4 @@
1
- import type { BodyEntryList, BoolResult, BootstrapState, CategoryRequest, ConnectRequest, Connection, ConnectionList, DiagnosticsState, Empty, HealthAccess, HeartRateEntryList, HeartRateList, LastDeliveryResult, MarkCompleteRequest, NutritionEntryList, PermissionGrant, PermissionRequest, PlannedWorkoutList, RangeRequest, RecordIdList, RecordIdRequest, SleepEntryList, SleepSegmentList, SourceRequest, StatusResult, StatusesResult, StepCountList, StepEntryList, SupportReport, TokenProviderRequest, TokenRejectRequest, TokenResolveRequest, WorkoutEntryList, WorkoutList, WorkoutScheduling } from "./types";
1
+ import type { AthleteProfileResult, AvailabilityResult, BodyEntryList, BodyReadingList, BoolResult, BootstrapState, CategoryRequest, ConnectRequest, Connection, ConnectionList, DayTotalList, DiagnosticsState, DietaryQuantityList, Empty, HealthAccess, HeartRateEntryList, HeartRateList, HydrationList, IntradaySampleList, LastDeliveryResult, MarkCompleteRequest, MenstrualEventList, MenstruationFlowDayList, NutritionEntryList, PermissionGrant, PermissionRequest, PlannedWorkoutList, RangeRequest, RecordIdList, RecordIdRequest, RestingHeartRateList, SleepEntryList, SleepSegmentList, SourceRequest, StatusResult, StatusesResult, StepCountList, StepEntryList, SupportReport, TokenProviderRequest, TokenRejectRequest, TokenResolveRequest, WorkoutEntryList, WorkoutIntensityList, WorkoutList, WorkoutRouteList, WorkoutScheduling } from "./types";
2
2
  import type { OpId } from "./ops";
3
3
  /** What the facade needs from the native module. The TurboModule spec satisfies it; so does a test double. */
4
4
  export interface Invoker {
@@ -21,6 +21,8 @@ export declare class GeneratedClient {
21
21
  clientAttach(request: Empty): Promise<BootstrapState>;
22
22
  clientConnect(request: ConnectRequest): Promise<Connection>;
23
23
  clientConnections(request: Empty): Promise<ConnectionList>;
24
+ /** Whether the device can serve the source at all. Needs no connection and prompts for nothing. */
25
+ clientAvailability(request: SourceRequest): Promise<AvailabilityResult>;
24
26
  clientDisconnect(request: SourceRequest): Promise<Empty>;
25
27
  /**
26
28
  * Tells the broker whether JS can answer at all. Without it a link asks a provider that is not
@@ -55,21 +57,55 @@ export declare class GeneratedClient {
55
57
  /** Available on android only. */
56
58
  backgroundCancel(request: Empty): Promise<BoolResult>;
57
59
  bodyHeartRate(request: RangeRequest): Promise<HeartRateList>;
60
+ /**
61
+ * The store's own daily figures. Health Connect grants them under daily, so Android answers
62
+ * authorization_not_determined where only body was asked for.
63
+ */
64
+ bodyRestingHeartRate(request: RangeRequest): Promise<RestingHeartRateList>;
65
+ bodyReadings(request: RangeRequest): Promise<BodyReadingList>;
58
66
  /** Its own op rather than an overload: Swift overloads log by element type and JS cannot. */
59
67
  bodyLogHeartRate(request: HeartRateEntryList): Promise<RecordIdList>;
60
68
  bodyLog(request: BodyEntryList): Promise<RecordIdList>;
61
69
  bodyDelete(request: RecordIdRequest): Promise<Empty>;
62
70
  dailySteps(request: RangeRequest): Promise<StepCountList>;
71
+ /**
72
+ * One total per metric per whole local day the range touches. A day with nothing recorded has
73
+ * no total, not a zero.
74
+ */
75
+ dailyTotals(request: RangeRequest): Promise<DayTotalList>;
76
+ /** The active energy, basal energy, distance and floors records the totals are made of. */
77
+ dailyIntraday(request: RangeRequest): Promise<IntradaySampleList>;
63
78
  dailyLog(request: StepEntryList): Promise<RecordIdList>;
64
79
  dailyDelete(request: RecordIdRequest): Promise<Empty>;
65
80
  sleepSegments(request: RangeRequest): Promise<SleepSegmentList>;
66
81
  sleepLog(request: SleepEntryList): Promise<RecordIdList>;
67
82
  sleepDelete(request: RecordIdRequest): Promise<Empty>;
68
83
  activityWorkouts(request: RangeRequest): Promise<WorkoutList>;
84
+ /** The GPS tracks of the workouts in the range, each naming its workout. */
85
+ activityRoutes(request: RangeRequest): Promise<WorkoutRouteList>;
86
+ /** Speed, power and cycling cadence readings in the range. */
87
+ activityIntensity(request: RangeRequest): Promise<WorkoutIntensityList>;
69
88
  activityLog(request: WorkoutEntryList): Promise<RecordIdList>;
70
89
  activityDelete(request: RecordIdRequest): Promise<Empty>;
90
+ /** One quantity per nutrient a meal recorded, not grouped into meals. Water is not among them. */
91
+ nutritionQuantities(request: RangeRequest): Promise<DietaryQuantityList>;
71
92
  nutritionLog(request: NutritionEntryList): Promise<RecordIdList>;
72
93
  nutritionDelete(request: RecordIdRequest): Promise<Empty>;
94
+ /** Water drunk in the range, one sample per drink. */
95
+ nutritionHydration(request: RangeRequest): Promise<HydrationList>;
96
+ /**
97
+ * Logged flow days overlapping the range, one per day. A day logged with no flow is noFlow,
98
+ * not left out.
99
+ */
100
+ menstruationFlow(request: RangeRequest): Promise<MenstruationFlowDayList>;
101
+ /** Ovulation tests, cervical mucus and intermenstrual bleeding logged in the range. */
102
+ menstruationEvents(request: RangeRequest): Promise<MenstrualEventList>;
103
+ /**
104
+ * Characteristics have no history.
105
+ *
106
+ * Available on ios only.
107
+ */
108
+ athleteProfile(request: Empty): Promise<AthleteProfileResult>;
73
109
  /** Available on ios only. */
74
110
  plannedWorkoutsPlanned(request: Empty): Promise<PlannedWorkoutList>;
75
111
  /** Available on ios only. */
@@ -50,6 +50,10 @@ class GeneratedClient {
50
50
  async clientConnections(request) {
51
51
  return this.call("client.connections", () => (0, codecs_1.encodeEmpty)(request), (raw) => (0, codecs_1.decodeConnectionList)(raw, "ConnectionList"));
52
52
  }
53
+ /** Whether the device can serve the source at all. Needs no connection and prompts for nothing. */
54
+ async clientAvailability(request) {
55
+ return this.call("client.availability", () => (0, codecs_1.encodeSourceRequest)(request), (raw) => (0, codecs_1.decodeAvailabilityResult)(raw, "AvailabilityResult"));
56
+ }
53
57
  async clientDisconnect(request) {
54
58
  return this.call("client.disconnect", () => (0, codecs_1.encodeSourceRequest)(request), (raw) => (0, codecs_1.decodeEmpty)(raw, "Empty"));
55
59
  }
@@ -120,6 +124,16 @@ class GeneratedClient {
120
124
  async bodyHeartRate(request) {
121
125
  return this.call("body.heartRate", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeHeartRateList)(raw, "HeartRateList"));
122
126
  }
127
+ /**
128
+ * The store's own daily figures. Health Connect grants them under daily, so Android answers
129
+ * authorization_not_determined where only body was asked for.
130
+ */
131
+ async bodyRestingHeartRate(request) {
132
+ return this.call("body.restingHeartRate", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeRestingHeartRateList)(raw, "RestingHeartRateList"));
133
+ }
134
+ async bodyReadings(request) {
135
+ return this.call("body.readings", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeBodyReadingList)(raw, "BodyReadingList"));
136
+ }
123
137
  /** Its own op rather than an overload: Swift overloads log by element type and JS cannot. */
124
138
  async bodyLogHeartRate(request) {
125
139
  return this.call("body.logHeartRate", () => (0, codecs_1.encodeHeartRateEntryList)(request), (raw) => (0, codecs_1.decodeRecordIdList)(raw, "RecordIdList"));
@@ -133,6 +147,17 @@ class GeneratedClient {
133
147
  async dailySteps(request) {
134
148
  return this.call("daily.steps", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeStepCountList)(raw, "StepCountList"));
135
149
  }
150
+ /**
151
+ * One total per metric per whole local day the range touches. A day with nothing recorded has
152
+ * no total, not a zero.
153
+ */
154
+ async dailyTotals(request) {
155
+ return this.call("daily.totals", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeDayTotalList)(raw, "DayTotalList"));
156
+ }
157
+ /** The active energy, basal energy, distance and floors records the totals are made of. */
158
+ async dailyIntraday(request) {
159
+ return this.call("daily.intraday", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeIntradaySampleList)(raw, "IntradaySampleList"));
160
+ }
136
161
  async dailyLog(request) {
137
162
  return this.call("daily.log", () => (0, codecs_1.encodeStepEntryList)(request), (raw) => (0, codecs_1.decodeRecordIdList)(raw, "RecordIdList"));
138
163
  }
@@ -151,18 +176,53 @@ class GeneratedClient {
151
176
  async activityWorkouts(request) {
152
177
  return this.call("activity.workouts", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeWorkoutList)(raw, "WorkoutList"));
153
178
  }
179
+ /** The GPS tracks of the workouts in the range, each naming its workout. */
180
+ async activityRoutes(request) {
181
+ return this.call("activity.routes", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeWorkoutRouteList)(raw, "WorkoutRouteList"));
182
+ }
183
+ /** Speed, power and cycling cadence readings in the range. */
184
+ async activityIntensity(request) {
185
+ return this.call("activity.intensity", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeWorkoutIntensityList)(raw, "WorkoutIntensityList"));
186
+ }
154
187
  async activityLog(request) {
155
188
  return this.call("activity.log", () => (0, codecs_1.encodeWorkoutEntryList)(request), (raw) => (0, codecs_1.decodeRecordIdList)(raw, "RecordIdList"));
156
189
  }
157
190
  async activityDelete(request) {
158
191
  return this.call("activity.delete", () => (0, codecs_1.encodeRecordIdRequest)(request), (raw) => (0, codecs_1.decodeEmpty)(raw, "Empty"));
159
192
  }
193
+ /** One quantity per nutrient a meal recorded, not grouped into meals. Water is not among them. */
194
+ async nutritionQuantities(request) {
195
+ return this.call("nutrition.quantities", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeDietaryQuantityList)(raw, "DietaryQuantityList"));
196
+ }
160
197
  async nutritionLog(request) {
161
198
  return this.call("nutrition.log", () => (0, codecs_1.encodeNutritionEntryList)(request), (raw) => (0, codecs_1.decodeRecordIdList)(raw, "RecordIdList"));
162
199
  }
163
200
  async nutritionDelete(request) {
164
201
  return this.call("nutrition.delete", () => (0, codecs_1.encodeRecordIdRequest)(request), (raw) => (0, codecs_1.decodeEmpty)(raw, "Empty"));
165
202
  }
203
+ /** Water drunk in the range, one sample per drink. */
204
+ async nutritionHydration(request) {
205
+ return this.call("nutrition.hydration", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeHydrationList)(raw, "HydrationList"));
206
+ }
207
+ /**
208
+ * Logged flow days overlapping the range, one per day. A day logged with no flow is noFlow,
209
+ * not left out.
210
+ */
211
+ async menstruationFlow(request) {
212
+ return this.call("menstruation.flow", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeMenstruationFlowDayList)(raw, "MenstruationFlowDayList"));
213
+ }
214
+ /** Ovulation tests, cervical mucus and intermenstrual bleeding logged in the range. */
215
+ async menstruationEvents(request) {
216
+ return this.call("menstruation.events", () => (0, codecs_1.encodeRangeRequest)(request), (raw) => (0, codecs_1.decodeMenstrualEventList)(raw, "MenstrualEventList"));
217
+ }
218
+ /**
219
+ * Characteristics have no history.
220
+ *
221
+ * Available on ios only.
222
+ */
223
+ async athleteProfile(request) {
224
+ return this.call("athlete.profile", () => (0, codecs_1.encodeEmpty)(request), (raw) => (0, codecs_1.decodeAthleteProfileResult)(raw, "AthleteProfileResult"));
225
+ }
166
226
  /** Available on ios only. */
167
227
  async plannedWorkoutsPlanned(request) {
168
228
  return this.call("plannedWorkouts.planned", () => (0, codecs_1.encodeEmpty)(request), (raw) => (0, codecs_1.decodePlannedWorkoutList)(raw, "PlannedWorkoutList"));