@tryterra/react-native 0.0.0-stage → 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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/TerraReactNative.podspec +48 -0
  4. package/android/build.gradle +87 -0
  5. package/android/src/bridgeCore/kotlin/README.md +1 -0
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
  8. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +557 -0
  9. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
  10. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
  11. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
  12. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
  13. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
  14. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +300 -0
  15. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +50 -0
  16. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
  17. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
  18. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +582 -0
  19. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +1569 -0
  20. package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
  21. package/android/src/main/AndroidManifest.xml +24 -0
  22. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
  23. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
  24. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
  25. package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
  26. package/docs/availability.md +396 -0
  27. package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
  28. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  29. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +82672 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2186 -0
  31. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2186 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  33. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2163 -0
  34. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  36. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  37. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  38. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6594 -0
  39. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  40. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +82672 -0
  41. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2186 -0
  42. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2186 -0
  43. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  44. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2163 -0
  45. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +82672 -0
  46. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2186 -0
  47. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2186 -0
  48. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  49. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2163 -0
  50. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  51. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  52. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
  53. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  54. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  55. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6595 -0
  56. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6513 -0
  57. package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
  58. package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
  59. package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
  60. package/ios/TerraBridgeCore/Conversions.swift +594 -0
  61. package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
  62. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +967 -0
  63. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +2050 -0
  64. package/ios/TerraBridgeCore/Locked.swift +15 -0
  65. package/ios/TerraBridgeCore/README.md +1 -0
  66. package/ios/TerraBridgeCore/Rejections.swift +130 -0
  67. package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
  68. package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
  69. package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
  70. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +318 -0
  71. package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
  72. package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
  73. package/ios/TerraBridgeModule.mm +79 -0
  74. package/ios/TerraBridgeModuleImpl.swift +49 -0
  75. package/ios/TerraReactNative.swift +19 -0
  76. package/lib/EndUser.d.ts +12 -0
  77. package/lib/EndUser.js +13 -0
  78. package/lib/NativeTerraBridge.d.ts +23 -0
  79. package/lib/NativeTerraBridge.js +13 -0
  80. package/lib/Terra.d.ts +223 -0
  81. package/lib/Terra.js +336 -0
  82. package/lib/TerraError.d.ts +42 -0
  83. package/lib/TerraError.js +95 -0
  84. package/lib/createTerra.d.ts +8 -0
  85. package/lib/createTerra.js +19 -0
  86. package/lib/generated/client.d.ts +128 -0
  87. package/lib/generated/client.js +256 -0
  88. package/lib/generated/codecs.d.ts +219 -0
  89. package/lib/generated/codecs.js +2066 -0
  90. package/lib/generated/errors.d.ts +13 -0
  91. package/lib/generated/errors.js +68 -0
  92. package/lib/generated/ops.d.ts +13 -0
  93. package/lib/generated/ops.js +161 -0
  94. package/lib/generated/streams.d.ts +23 -0
  95. package/lib/generated/streams.js +58 -0
  96. package/lib/generated/types.d.ts +794 -0
  97. package/lib/generated/types.js +224 -0
  98. package/lib/index.d.ts +29 -0
  99. package/lib/index.js +55 -0
  100. package/package.json +71 -4
  101. package/src/EndUser.ts +15 -0
  102. package/src/NativeTerraBridge.ts +27 -0
  103. package/src/Terra.ts +592 -0
  104. package/src/TerraError.ts +116 -0
  105. package/src/createTerra.ts +18 -0
  106. package/src/generated/client.ts +315 -0
  107. package/src/generated/codecs.ts +1989 -0
  108. package/src/generated/errors.ts +113 -0
  109. package/src/generated/ops.ts +233 -0
  110. package/src/generated/streams.ts +74 -0
  111. package/src/generated/types.ts +1222 -0
  112. package/src/index.ts +54 -0
package/lib/EndUser.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * Who a connect is for. Every connect names one: a second account on the same phone is a different
4
+ * person, and must not be handed the first one's link.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.EndUser = void 0;
8
+ exports.EndUser = {
9
+ /** The person your backend knows as `id`: the same value it mints their token with. */
10
+ reference: (id) => ({ kind: "reference", id }),
11
+ /** Nobody your backend tracks. Every anonymous connect on one device is the same person. */
12
+ anonymous: { kind: "anonymous" },
13
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The TurboModule spec. React Native's Codegen reads this file at the host app's build and
3
+ * generates the native interfaces that `ios/TerraBridgeModule.mm` and
4
+ * `android/.../TerraBridgeModule.kt` implement.
5
+ *
6
+ * Deliberately narrow: one op call, one stream subscription, one emitter. The op strings and their
7
+ * payloads are typed on both sides by `surface/gen`, so nothing here changes when the SDK grows an
8
+ * op.
9
+ */
10
+ import type { CodegenTypes, TurboModule } from "react-native";
11
+ export type StreamMessageEvent = {
12
+ subscriptionId: string;
13
+ /** A `StreamMessage`, as the wire spells it. */
14
+ message: string;
15
+ };
16
+ export interface Spec extends TurboModule {
17
+ invoke(op: string, payload: string): Promise<string>;
18
+ subscribe(stream: string, params: string): Promise<string>;
19
+ unsubscribe(subscriptionId: string): void;
20
+ readonly onMessage: CodegenTypes.EventEmitter<StreamMessageEvent>;
21
+ }
22
+ declare const _default: Spec;
23
+ export default _default;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * The TurboModule spec. React Native's Codegen reads this file at the host app's build and
4
+ * generates the native interfaces that `ios/TerraBridgeModule.mm` and
5
+ * `android/.../TerraBridgeModule.kt` implement.
6
+ *
7
+ * Deliberately narrow: one op call, one stream subscription, one emitter. The op strings and their
8
+ * payloads are typed on both sides by `surface/gen`, so nothing here changes when the SDK grows an
9
+ * op.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const react_native_1 = require("react-native");
13
+ exports.default = react_native_1.TurboModuleRegistry.getEnforcing("TerraBridge");
package/lib/Terra.d.ts ADDED
@@ -0,0 +1,223 @@
1
+ /**
2
+ * The client an app holds: namespaces over the generated ops, the token provider, and the streams.
3
+ *
4
+ * Hand-written, deliberately thin. Every method is one generated `GeneratedClient` call; what lives
5
+ * here is only what the generated layer cannot know - which runtime the app is on, how a JS
6
+ * function answers a native token request, and how one native event emitter fans out to many
7
+ * listeners. `facade.test.ts` drives every method and fails when an op in the schema has no member
8
+ * here, so this file cannot quietly fall behind the surface the way the 1.x wrapper did.
9
+ */
10
+ import { TerraError } from "./TerraError";
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
+ /** What the facade needs from the native module. `NativeTerraBridge` satisfies it; so does a double. */
13
+ export interface NativeBridge {
14
+ invoke(op: string, payload: string): Promise<string>;
15
+ subscribe(stream: string, params: string): Promise<string>;
16
+ unsubscribe(subscriptionId: string): void;
17
+ onMessage(listener: (event: {
18
+ subscriptionId: string;
19
+ message: string;
20
+ }) => void): {
21
+ remove(): void;
22
+ };
23
+ }
24
+ export type Platform = "ios" | "android";
25
+ /** Asked for a one-shot link token, and only while `connect` establishes a new link. */
26
+ export type TokenProvider = () => Promise<string>;
27
+ export interface TerraOptions {
28
+ /**
29
+ * Called only when `connect` has to make a new link and was given no token. A restored link
30
+ * never asks, and neither does a background wake: uploads are signed by the device key.
31
+ */
32
+ readonly tokenProvider?: TokenProvider;
33
+ /** How long the native side waits for `tokenProvider`. 30 seconds by default. */
34
+ readonly tokenTimeoutMs?: number;
35
+ }
36
+ export type Unsubscribe = () => void;
37
+ /** A stream's listener. `onError` receives the error a stream ended with; a stream that completes ends quietly. */
38
+ export interface StreamListener<T> {
39
+ (element: T): void;
40
+ }
41
+ export interface Permissions {
42
+ /**
43
+ * One sheet covering both directions and everything asked before. Android answers what this call
44
+ * obtained; HealthKit will not report read grants, so iOS answers `undefined`.
45
+ *
46
+ * `source` defaults to `health`. `samsungHealth` presents Samsung Health's own consent popup and
47
+ * is Android only.
48
+ */
49
+ request(access: Partial<HealthAccess>, source?: Source): Promise<HealthAccess | undefined>;
50
+ status(category: DataCategory, source?: Source): Promise<PermissionStatus>;
51
+ writeStatus(category: DataCategory, source?: Source): Promise<PermissionStatus>;
52
+ requested(source?: Source): Promise<HealthAccess>;
53
+ /** Android only. */
54
+ readonly statuses?: (source?: Source) => Promise<Partial<Record<DataCategory, PermissionStatus>>>;
55
+ /** Android only. */
56
+ readonly backgroundReadStatus?: (source?: Source) => Promise<PermissionStatus>;
57
+ /** Android only. */
58
+ readonly historyReadStatus?: (source?: Source) => Promise<PermissionStatus>;
59
+ }
60
+ export interface Diagnostics {
61
+ state(source?: Source): Promise<DiagnosticsState>;
62
+ lastDelivery(source?: Source): Promise<Date | undefined>;
63
+ report(source?: Source): Promise<SupportReport>;
64
+ send(source?: Source): Promise<void>;
65
+ /** Everything the SDK does, as it happens. Never carries a health value. */
66
+ events(listener: StreamListener<TerraEvent>, onError?: (error: TerraError) => void): Unsubscribe;
67
+ }
68
+ export interface Background {
69
+ /** Whether the native bootstrap registered background sync at launch. False is a host setup bug. */
70
+ isRegistered(): Promise<boolean>;
71
+ /** Android only: drops the schedule. */
72
+ readonly cancel?: () => Promise<boolean>;
73
+ }
74
+ interface Changes {
75
+ /** Which days changed in the health store. Re-read through this namespace's accessor. */
76
+ changes(listener: StreamListener<HealthChange>, onError?: (error: TerraError) => void): Unsubscribe;
77
+ }
78
+ export interface BodyMetrics extends Changes {
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[]>;
91
+ logHeartRate(entries: readonly HeartRateEntry[]): Promise<readonly string[]>;
92
+ log(entries: readonly BodyEntry[]): Promise<readonly string[]>;
93
+ delete(id: string): Promise<void>;
94
+ }
95
+ export interface DailyMetrics extends Changes {
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[]>;
105
+ log(entries: readonly StepEntry[]): Promise<readonly string[]>;
106
+ delete(id: string): Promise<void>;
107
+ }
108
+ export interface SleepMetrics extends Changes {
109
+ segments(range: TimeRange): Promise<readonly SleepSegment[]>;
110
+ log(entries: readonly SleepEntry[]): Promise<readonly string[]>;
111
+ delete(id: string): Promise<void>;
112
+ }
113
+ export interface ActivityMetrics extends Changes {
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[]>;
122
+ log(entries: readonly WorkoutEntry[]): Promise<readonly string[]>;
123
+ delete(id: string): Promise<void>;
124
+ }
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[]>;
134
+ log(entries: readonly NutritionEntry[]): Promise<readonly string[]>;
135
+ delete(id: string): Promise<void>;
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
+ }
156
+ /** iOS only. */
157
+ export interface PlannedWorkouts {
158
+ planned(): Promise<readonly PlannedWorkout[]>;
159
+ markComplete(id: string, at?: Date): Promise<void>;
160
+ delete(id: string): Promise<void>;
161
+ /** Whether plans can be put on the person's Apple Watch. Never prompts. */
162
+ schedulingStatus(): Promise<WorkoutSchedulingStatus>;
163
+ /**
164
+ * Shows WorkoutKit's sheet if the person has not been asked. Once authorized, every sync run
165
+ * keeps the Watch schedule in step with Terra; nothing else needs calling.
166
+ */
167
+ requestScheduling(): Promise<WorkoutSchedulingStatus>;
168
+ }
169
+ export declare class Terra {
170
+ readonly platform: Platform;
171
+ readonly permissions: Permissions;
172
+ readonly diagnostics: Diagnostics;
173
+ readonly background: Background;
174
+ readonly body: BodyMetrics;
175
+ readonly daily: DailyMetrics;
176
+ readonly sleep: SleepMetrics;
177
+ readonly activity: ActivityMetrics;
178
+ readonly nutrition: NutritionMetrics;
179
+ readonly menstruation: MenstruationMetrics;
180
+ /** `undefined` on Android, which has no characteristics to read. */
181
+ readonly athlete: AthleteMetrics | undefined;
182
+ /** `undefined` on Android, which has no planned workouts. */
183
+ readonly plannedWorkouts: PlannedWorkouts | undefined;
184
+ private readonly ops;
185
+ private readonly streams;
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;
193
+ constructor(native: NativeBridge, platform: Platform, options?: TerraOptions);
194
+ /**
195
+ * The native client this runtime is attached to. Rejects with `bridge.not_bootstrapped`, carrying
196
+ * the exact line to add in `details.line`, when the app's native launch never set Terra up.
197
+ */
198
+ attach(): Promise<BootstrapState>;
199
+ /**
200
+ * Links `source` for the person `endUser` names, or restores the link already made for them.
201
+ *
202
+ * Every connect names its person: a second account on the same phone must not be handed the
203
+ * first one's link. With `token` the grant in hand is spent (a QR code, a deep link); without it
204
+ * the `tokenProvider` is asked, and only when a new link is actually needed.
205
+ */
206
+ connect(source: Source, endUser: EndUser, options?: {
207
+ readonly token?: string;
208
+ }): Promise<Connection>;
209
+ connections(): Promise<readonly Connection[]>;
210
+ disconnect(source?: Source): Promise<void>;
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. */
221
+ private answerTokens;
222
+ }
223
+ export {};
package/lib/Terra.js ADDED
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ /**
3
+ * The client an app holds: namespaces over the generated ops, the token provider, and the streams.
4
+ *
5
+ * Hand-written, deliberately thin. Every method is one generated `GeneratedClient` call; what lives
6
+ * here is only what the generated layer cannot know - which runtime the app is on, how a JS
7
+ * function answers a native token request, and how one native event emitter fans out to many
8
+ * listeners. `facade.test.ts` drives every method and fails when an op in the schema has no member
9
+ * here, so this file cannot quietly fall behind the surface the way the 1.x wrapper did.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Terra = void 0;
13
+ const TerraError_1 = require("./TerraError");
14
+ const client_1 = require("./generated/client");
15
+ const ops_1 = require("./generated/ops");
16
+ const streams_1 = require("./generated/streams");
17
+ class Terra {
18
+ constructor(native, platform, options = {}) {
19
+ this.platform = platform;
20
+ const invoker = native;
21
+ const ops = new client_1.GeneratedClient(invoker);
22
+ this.ops = ops;
23
+ this.streams = new StreamHub(native);
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
+ const on = (op) => ops_1.availableOn[op].includes(platform);
30
+ const stream = (id) => (listener, onError) => this.streams.subscribe(id, listener, onError);
31
+ this.permissions = {
32
+ request: async (access, source) => {
33
+ const grant = await ops.permissionsRequest({
34
+ read: access.read ?? [],
35
+ write: access.write ?? [],
36
+ workouts: access.workouts ?? [],
37
+ source,
38
+ });
39
+ return grant.granted;
40
+ },
41
+ status: async (category, source) => (await ops.permissionsStatus({ category, source })).status,
42
+ writeStatus: async (category, source) => (await ops.permissionsWriteStatus({ category, source })).status,
43
+ requested: (source) => ops.permissionsRequested({ source }),
44
+ ...(on("permissions.statuses") && {
45
+ statuses: async (source) => (await ops.permissionsStatuses({ source })).statuses,
46
+ }),
47
+ ...(on("permissions.backgroundReadStatus") && {
48
+ backgroundReadStatus: async (source) => (await ops.permissionsBackgroundReadStatus({ source })).status,
49
+ }),
50
+ ...(on("permissions.historyReadStatus") && {
51
+ historyReadStatus: async (source) => (await ops.permissionsHistoryReadStatus({ source })).status,
52
+ }),
53
+ };
54
+ this.diagnostics = {
55
+ state: (source) => ops.diagnosticsState({ source }),
56
+ lastDelivery: async (source) => (await ops.diagnosticsLastDelivery({ source })).at,
57
+ report: (source) => ops.diagnosticsReport({ source }),
58
+ send: async (source) => {
59
+ await ops.diagnosticsSend({ source });
60
+ },
61
+ events: stream("diagnostics.events"),
62
+ };
63
+ this.background = {
64
+ isRegistered: async () => (await ops.backgroundIsRegistered({})).value,
65
+ ...(on("background.cancel") && { cancel: async () => (await ops.backgroundCancel({})).value }),
66
+ };
67
+ this.body = {
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,
71
+ logHeartRate: async (entries) => (await ops.bodyLogHeartRate({ entries })).ids,
72
+ log: async (entries) => (await ops.bodyLog({ entries })).ids,
73
+ delete: async (id) => {
74
+ await ops.bodyDelete({ id });
75
+ },
76
+ changes: stream("body.changes"),
77
+ };
78
+ this.daily = {
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,
82
+ log: async (entries) => (await ops.dailyLog({ entries })).ids,
83
+ delete: async (id) => {
84
+ await ops.dailyDelete({ id });
85
+ },
86
+ changes: stream("daily.changes"),
87
+ };
88
+ this.sleep = {
89
+ segments: async (range) => (await ops.sleepSegments({ range })).samples,
90
+ log: async (entries) => (await ops.sleepLog({ entries })).ids,
91
+ delete: async (id) => {
92
+ await ops.sleepDelete({ id });
93
+ },
94
+ changes: stream("sleep.changes"),
95
+ };
96
+ this.activity = {
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,
100
+ log: async (entries) => (await ops.activityLog({ entries })).ids,
101
+ delete: async (id) => {
102
+ await ops.activityDelete({ id });
103
+ },
104
+ changes: stream("activity.changes"),
105
+ };
106
+ this.nutrition = {
107
+ quantities: async (range) => (await ops.nutritionQuantities({ range })).samples,
108
+ hydration: async (range) => (await ops.nutritionHydration({ range })).samples,
109
+ log: async (entries) => (await ops.nutritionLog({ entries })).ids,
110
+ delete: async (id) => {
111
+ await ops.nutritionDelete({ id });
112
+ },
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;
121
+ this.plannedWorkouts = on("plannedWorkouts.planned")
122
+ ? {
123
+ planned: async () => (await ops.plannedWorkoutsPlanned({})).workouts,
124
+ markComplete: async (id, at) => {
125
+ await ops.plannedWorkoutsMarkComplete({ id, at });
126
+ },
127
+ delete: async (id) => {
128
+ await ops.plannedWorkoutsDelete({ id });
129
+ },
130
+ schedulingStatus: async () => (await ops.plannedWorkoutsSchedulingStatus({})).status,
131
+ requestScheduling: async () => (await ops.plannedWorkoutsRequestScheduling({})).status,
132
+ }
133
+ : undefined;
134
+ }
135
+ /**
136
+ * The native client this runtime is attached to. Rejects with `bridge.not_bootstrapped`, carrying
137
+ * the exact line to add in `details.line`, when the app's native launch never set Terra up.
138
+ */
139
+ attach() {
140
+ return this.ops.clientAttach({});
141
+ }
142
+ /**
143
+ * Links `source` for the person `endUser` names, or restores the link already made for them.
144
+ *
145
+ * Every connect names its person: a second account on the same phone must not be handed the
146
+ * first one's link. With `token` the grant in hand is spent (a QR code, a deep link); without it
147
+ * the `tokenProvider` is asked, and only when a new link is actually needed.
148
+ */
149
+ async connect(source, endUser, options = {}) {
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();
153
+ return this.ops.clientConnect({ source, endUser, token: options.token });
154
+ }
155
+ async connections() {
156
+ return (await this.ops.clientConnections({})).connections;
157
+ }
158
+ async disconnect(source) {
159
+ await this.ops.clientDisconnect({ source });
160
+ }
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 }) => {
191
+ void (async () => {
192
+ let token;
193
+ try {
194
+ token = await provider();
195
+ }
196
+ catch (failed) {
197
+ const message = failed instanceof Error ? failed.message : String(failed);
198
+ await this.ops.tokenReject({ requestId, message }).catch(() => undefined);
199
+ return;
200
+ }
201
+ await this.ops.tokenResolve({ requestId, token }).catch(() => undefined);
202
+ })();
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
+ }
212
+ }
213
+ }
214
+ exports.Terra = Terra;
215
+ class StreamHub {
216
+ constructor(native) {
217
+ this.native = native;
218
+ this.handlers = new Map();
219
+ this.early = new Map();
220
+ this.pending = 0;
221
+ }
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) {
227
+ this.listen();
228
+ this.pending += 1;
229
+ let id;
230
+ try {
231
+ id = await this.native.subscribe(stream, "{}");
232
+ }
233
+ catch (thrown) {
234
+ this.settled();
235
+ throw TerraError_1.TerraError.from(thrown);
236
+ }
237
+ const handle = (message) => {
238
+ const delivery = (0, streams_1.decodeStreamDelivery)(stream, JSON.parse(message));
239
+ switch (delivery.kind) {
240
+ case "data":
241
+ listener(delivery.element);
242
+ return;
243
+ case "error":
244
+ this.ended(id);
245
+ onError?.(new TerraError_1.TerraError(delivery.error));
246
+ onEnd?.();
247
+ return;
248
+ case "complete":
249
+ this.ended(id);
250
+ onEnd?.();
251
+ return;
252
+ }
253
+ };
254
+ this.handlers.set(id, handle);
255
+ const held = this.early.get(id);
256
+ this.early.delete(id);
257
+ this.settled();
258
+ held?.forEach(handle);
259
+ return id;
260
+ }
261
+ settled() {
262
+ this.pending -= 1;
263
+ if (this.pending === 0)
264
+ this.early.clear();
265
+ this.releaseIfIdle();
266
+ }
267
+ ended(id) {
268
+ this.handlers.delete(id);
269
+ this.releaseIfIdle();
270
+ }
271
+ releaseIfIdle() {
272
+ if (this.handlers.size > 0 || this.pending > 0)
273
+ return;
274
+ this.listening?.remove();
275
+ this.listening = undefined;
276
+ }
277
+ /** Subscribes without waiting; the returned function cancels whether or not the stream has opened yet. */
278
+ subscribe(stream, listener, onError) {
279
+ let cancelled = false;
280
+ let opened;
281
+ this.open(stream, (element) => {
282
+ if (!cancelled)
283
+ listener(element);
284
+ }, (error) => {
285
+ if (!cancelled)
286
+ onError?.(error);
287
+ }).then((id) => {
288
+ opened = id;
289
+ if (cancelled)
290
+ this.close(id);
291
+ }, (error) => {
292
+ if (!cancelled)
293
+ onError?.(error);
294
+ });
295
+ return () => {
296
+ if (cancelled)
297
+ return;
298
+ cancelled = true;
299
+ if (opened !== undefined)
300
+ this.close(opened);
301
+ };
302
+ }
303
+ close(id) {
304
+ this.handlers.delete(id);
305
+ this.early.delete(id);
306
+ this.native.unsubscribe(id);
307
+ this.releaseIfIdle();
308
+ }
309
+ listen() {
310
+ if (this.listening !== undefined)
311
+ return;
312
+ this.listening = this.native.onMessage(({ subscriptionId, message }) => {
313
+ const handler = this.handlers.get(subscriptionId);
314
+ if (handler !== undefined) {
315
+ handler(message);
316
+ return;
317
+ }
318
+ if (this.pending === 0)
319
+ return;
320
+ const held = this.early.get(subscriptionId) ?? [];
321
+ if (held.length < EARLY_LIMIT || isTerminal(message))
322
+ held.push(message);
323
+ this.early.set(subscriptionId, held);
324
+ });
325
+ }
326
+ }
327
+ const EARLY_LIMIT = 64;
328
+ function isTerminal(message) {
329
+ try {
330
+ const kind = JSON.parse(message).kind;
331
+ return kind === "error" || kind === "complete";
332
+ }
333
+ catch {
334
+ return false;
335
+ }
336
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The error every rejected op carries, hydrated from the bridge's JSON.
3
+ *
4
+ * Hand-written, and one of only two files here that are: the shape is fixed, while the *vocabulary*
5
+ * it carries is generated. `code` is a `TerraErrorCode`, so a code renamed natively stops a
6
+ * `switch` over it compiling rather than silently never matching.
7
+ */
8
+ import { type TerraErrorCode } from "./generated/errors";
9
+ import type { Source, TerraErrorPayload } from "./generated/types";
10
+ /**
11
+ * The JSON a native rejection carries in its userInfo / message: the schema's own payload, except
12
+ * that `retryable` may be left out, in which case the per-code table answers.
13
+ */
14
+ export type TerraErrorJSON = Omit<TerraErrorPayload, "retryable"> & {
15
+ readonly retryable?: boolean;
16
+ };
17
+ export declare class TerraError extends Error {
18
+ readonly code: TerraErrorCode;
19
+ /**
20
+ * Whether the same call could succeed if repeated.
21
+ *
22
+ * Read from the rejection rather than from `retryableByCode`, because one code decides per
23
+ * instance: `network.server_error` is retryable for a 503 and not for a 400. The table is the
24
+ * fallback for a rejection that did not carry the flag at all.
25
+ */
26
+ readonly retryable: boolean;
27
+ readonly source: Source | undefined;
28
+ readonly details: Record<string, unknown> | undefined;
29
+ constructor(json: TerraErrorJSON, options?: {
30
+ readonly cause?: unknown;
31
+ });
32
+ /**
33
+ * Rebuilds a `TerraError` from whatever the native module rejected with.
34
+ *
35
+ * Anything unrecognisable becomes `platform` rather than being rethrown as-is: a caller that has
36
+ * to handle both `TerraError` and an arbitrary `Error` ends up handling neither properly, and the
37
+ * original is kept as `cause` and its text in `message`.
38
+ */
39
+ static from(thrown: unknown): TerraError;
40
+ /** A response the bridge resolved that this build cannot read: the native side and JS disagree. */
41
+ static invalidPayload(op: string, thrown: unknown): TerraError;
42
+ }