@mentra/cloud-client 0.1.0-dev.0

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.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @fileoverview The single typed event emitter behind `cloud.runtime`'s `on*`.
3
+ *
4
+ * There is exactly one emitter so there is one source of truth for runtime
5
+ * events: the friendly per-event methods (`onTranscript`, `onConnected`, ...),
6
+ * the generic `on(event, cb)`, and `onAny(cb)` are all thin wrappers over this.
7
+ * Keying by an event map (`RuntimeEvents`) means event names are checked by the
8
+ * compiler and payloads are typed, so there are no magic strings to mistype.
9
+ *
10
+ * See docs/issues/004-cloud-client/design.md ("src/modules/runtime/emitter.ts").
11
+ */
12
+ import type {
13
+ TranscriptionData,
14
+ TranslationData,
15
+ ProtocolError,
16
+ } from "@mentra/cloud-protocol";
17
+ import type { RuntimeSnapshot } from "./status";
18
+
19
+ /**
20
+ * The runtime event map: event name to payload type.
21
+ *
22
+ * `transcript` / `translation` carry the canonical wire result types, so a
23
+ * consumer gets exactly what the cloud sent. `connected` has no payload (the
24
+ * fact of connecting is the whole signal), so its type is `void`. `error`
25
+ * carries the wire `ProtocolError` (a non-fatal one; fatal ones close the
26
+ * socket and surface as a reconnect), not a thrown JS error.
27
+ */
28
+ export interface RuntimeEvents {
29
+ transcript: TranscriptionData;
30
+ translation: TranslationData;
31
+ connected: void;
32
+ disconnected: { reason: string };
33
+ status: RuntimeSnapshot;
34
+ error: ProtocolError;
35
+ }
36
+
37
+ /** A handler stored for a single event name, payload typed by the map. */
38
+ type Listener<K extends keyof RuntimeEvents> = (data: RuntimeEvents[K]) => void;
39
+
40
+ /** A handler that receives every event, used for forwarding and logging. */
41
+ type AnyListener = (event: keyof RuntimeEvents, data: unknown) => void;
42
+
43
+ /**
44
+ * A typed multi-listener emitter for runtime events.
45
+ *
46
+ * Listeners are kept in a `Set` per event so the same handler is not registered
47
+ * twice and `off` is an O(1) removal. `onAny` listeners are kept separately and
48
+ * fired on every `emit`, which is what powers island re-emitting all runtime
49
+ * events without naming each one.
50
+ */
51
+ export class RuntimeEmitter {
52
+ // One listener set per event name. A `Map` keyed by the event name keeps
53
+ // dispatch touching only the handlers for the event that fired. The stored set
54
+ // is typed loosely (the element type varies per key, which a single field
55
+ // cannot express); the public `on`/`off`/`emit` signatures below are fully
56
+ // typed via the event map, so callers never see the loose inner type.
57
+ private readonly listeners = new Map<
58
+ keyof RuntimeEvents,
59
+ Set<Listener<keyof RuntimeEvents>>
60
+ >();
61
+
62
+ // Listeners that want every event, kept apart so `emit` can fan out to them
63
+ // after the per-event handlers run.
64
+ private readonly anyListeners = new Set<AnyListener>();
65
+
66
+ /**
67
+ * Subscribe to one event. Returns an unsubscribe function, which is the only
68
+ * way callers are expected to detach: holding the returned function means a
69
+ * caller never needs a reference to the original handler to remove it.
70
+ */
71
+ on<K extends keyof RuntimeEvents>(e: K, cb: Listener<K>): () => void {
72
+ let set = this.listeners.get(e);
73
+ if (!set) {
74
+ set = new Set();
75
+ this.listeners.set(e, set);
76
+ }
77
+ set.add(cb as Listener<keyof RuntimeEvents>);
78
+ return () => this.off(e, cb);
79
+ }
80
+
81
+ /**
82
+ * Remove a previously registered handler for one event. A no-op if the handler
83
+ * was never registered, so double-unsubscribe is safe.
84
+ */
85
+ off<K extends keyof RuntimeEvents>(e: K, cb: Listener<K>): void {
86
+ this.listeners.get(e)?.delete(cb as Listener<keyof RuntimeEvents>);
87
+ }
88
+
89
+ /**
90
+ * Subscribe to every event in one handler (for forwarding, iteration, or
91
+ * logging). The payload is `unknown` because it varies per event; a consumer
92
+ * narrows on the event name. Returns an unsubscribe function like `on`.
93
+ */
94
+ onAny(cb: AnyListener): () => void {
95
+ this.anyListeners.add(cb);
96
+ return () => {
97
+ this.anyListeners.delete(cb);
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Deliver an event to its handlers, then to every `onAny` handler.
103
+ *
104
+ * We iterate over a copy of each listener set so a handler that subscribes or
105
+ * unsubscribes during dispatch does not mutate the set we are walking. A throw
106
+ * from one handler must not stop the others or skip the `onAny` fan-out, so
107
+ * each call is isolated and a failure is swallowed (the emitter has no logger;
108
+ * a handler owns its own error handling).
109
+ */
110
+ emit<K extends keyof RuntimeEvents>(e: K, d: RuntimeEvents[K]): void {
111
+ const set = this.listeners.get(e);
112
+ if (set) {
113
+ for (const cb of [...set]) {
114
+ try {
115
+ (cb as Listener<K>)(d);
116
+ } catch {
117
+ // A misbehaving listener must not break delivery to the others.
118
+ }
119
+ }
120
+ }
121
+ for (const cb of [...this.anyListeners]) {
122
+ try {
123
+ cb(e, d);
124
+ } catch {
125
+ // Same isolation for the catch-all listeners.
126
+ }
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview Runtime maps API: directions + reverse geocoding.
3
+ *
4
+ * A thin client over the runtime's `/api/maps/*` REST endpoints. Both calls are
5
+ * plain request/response (no WebSocket push, unlike camera), so this module only
6
+ * needs the shared HTTP helper. The cloud holds the maps provider token; the
7
+ * consumer (e.g. the navigation miniapp via the SDK) calls these and never sees
8
+ * a provider credential or a vendor-specific response shape.
9
+ *
10
+ * The wire types are canonical in the protocol package (the runtime server uses
11
+ * the same ones); re-export them so a host gets them from this module.
12
+ */
13
+ import type { HttpClient } from "../../http";
14
+ import type {
15
+ DirectionsRequest,
16
+ DirectionsResult,
17
+ LatLng,
18
+ PlaceAutocompleteResult,
19
+ PlaceDetailsResult,
20
+ ReverseGeocodeResult,
21
+ } from "@mentra/cloud-protocol";
22
+
23
+ export type {
24
+ DirectionsRequest,
25
+ DirectionsResult,
26
+ Route,
27
+ RouteStep,
28
+ LatLng,
29
+ TravelMode,
30
+ ManeuverKind,
31
+ RouteAvoidances,
32
+ ReverseGeocodeResult,
33
+ PlaceSuggestion,
34
+ PlaceAutocompleteResult,
35
+ PlaceDetailsResult,
36
+ } from "@mentra/cloud-protocol";
37
+
38
+ const DIRECTIONS_PATH = "/api/maps/directions";
39
+ const REVERSE_GEOCODE_PATH = "/api/maps/reverse-geocode";
40
+ const PLACE_AUTOCOMPLETE_PATH = "/api/maps/place-autocomplete";
41
+ const PLACE_DETAILS_PATH = "/api/maps/place-details";
42
+
43
+ export interface MapsDeps {
44
+ http: HttpClient;
45
+ }
46
+
47
+ export class Maps {
48
+ private readonly http: HttpClient;
49
+
50
+ constructor(deps: MapsDeps) {
51
+ this.http = deps.http;
52
+ }
53
+
54
+ /** Compute routes from origin through stops. Primary route first, alternates after. */
55
+ directions(req: DirectionsRequest): Promise<DirectionsResult> {
56
+ // Idempotent: a directions request is a pure read, safe to retry on a
57
+ // transient network failure.
58
+ return this.http.post<DirectionsResult>(DIRECTIONS_PATH, req, { idempotent: true });
59
+ }
60
+
61
+ /**
62
+ * Resolve a coordinate to a short road name (`road`) + full formatted address
63
+ * (`address`). Each is null when none of that kind is found near the coord.
64
+ */
65
+ reverseGeocode(coord: LatLng): Promise<ReverseGeocodeResult> {
66
+ return this.http.post<ReverseGeocodeResult>(REVERSE_GEOCODE_PATH, coord, {
67
+ idempotent: true,
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Type-ahead place search. `sessionToken` groups the keystrokes with the
73
+ * following `placeDetails` pick into one billed search session.
74
+ */
75
+ placeAutocomplete(req: {
76
+ query: string;
77
+ near?: LatLng;
78
+ sessionToken: string;
79
+ }): Promise<PlaceAutocompleteResult> {
80
+ // Idempotent: autocomplete is a pure read, safe to retry on a transient blip.
81
+ return this.http.post<PlaceAutocompleteResult>(PLACE_AUTOCOMPLETE_PATH, req, {
82
+ idempotent: true,
83
+ });
84
+ }
85
+
86
+ /** Resolve an autocomplete suggestion (`placeId` + same `sessionToken`) to coordinates. */
87
+ placeDetails(req: { placeId: string; sessionToken: string }): Promise<PlaceDetailsResult> {
88
+ return this.http.post<PlaceDetailsResult>(PLACE_DETAILS_PATH, req, {
89
+ idempotent: true,
90
+ });
91
+ }
92
+ }