@glassly/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,132 @@
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
+ QuotaStatusPayload,
14
+ TranscriptionData,
15
+ TranslationData,
16
+ ProtocolError,
17
+ } from "@glassly/cloud-protocol";
18
+ import type { RuntimeSnapshot } from "./status";
19
+
20
+ /**
21
+ * The runtime event map: event name to payload type.
22
+ *
23
+ * `transcript` / `translation` carry the canonical wire result types, so a
24
+ * consumer gets exactly what the cloud sent. `connected` has no payload (the
25
+ * fact of connecting is the whole signal), so its type is `void`. `error`
26
+ * carries the wire `ProtocolError` (a non-fatal one; fatal ones close the
27
+ * socket and surface as a reconnect), not a thrown JS error.
28
+ */
29
+ export interface RuntimeEvents {
30
+ transcript: TranscriptionData;
31
+ translation: TranslationData;
32
+ /** Plan-quota block state changed for a metered stream (stream.quota). */
33
+ quota: QuotaStatusPayload;
34
+ connected: void;
35
+ disconnected: { reason: string };
36
+ status: RuntimeSnapshot;
37
+ error: ProtocolError;
38
+ }
39
+
40
+ /** A handler stored for a single event name, payload typed by the map. */
41
+ type Listener<K extends keyof RuntimeEvents> = (data: RuntimeEvents[K]) => void;
42
+
43
+ /** A handler that receives every event, used for forwarding and logging. */
44
+ type AnyListener = (event: keyof RuntimeEvents, data: unknown) => void;
45
+
46
+ /**
47
+ * A typed multi-listener emitter for runtime events.
48
+ *
49
+ * Listeners are kept in a `Set` per event so the same handler is not registered
50
+ * twice and `off` is an O(1) removal. `onAny` listeners are kept separately and
51
+ * fired on every `emit`, which is what powers island re-emitting all runtime
52
+ * events without naming each one.
53
+ */
54
+ export class RuntimeEmitter {
55
+ // One listener set per event name. A `Map` keyed by the event name keeps
56
+ // dispatch touching only the handlers for the event that fired. The stored set
57
+ // is typed loosely (the element type varies per key, which a single field
58
+ // cannot express); the public `on`/`off`/`emit` signatures below are fully
59
+ // typed via the event map, so callers never see the loose inner type.
60
+ private readonly listeners = new Map<
61
+ keyof RuntimeEvents,
62
+ Set<Listener<keyof RuntimeEvents>>
63
+ >();
64
+
65
+ // Listeners that want every event, kept apart so `emit` can fan out to them
66
+ // after the per-event handlers run.
67
+ private readonly anyListeners = new Set<AnyListener>();
68
+
69
+ /**
70
+ * Subscribe to one event. Returns an unsubscribe function, which is the only
71
+ * way callers are expected to detach: holding the returned function means a
72
+ * caller never needs a reference to the original handler to remove it.
73
+ */
74
+ on<K extends keyof RuntimeEvents>(e: K, cb: Listener<K>): () => void {
75
+ let set = this.listeners.get(e);
76
+ if (!set) {
77
+ set = new Set();
78
+ this.listeners.set(e, set);
79
+ }
80
+ set.add(cb as Listener<keyof RuntimeEvents>);
81
+ return () => this.off(e, cb);
82
+ }
83
+
84
+ /**
85
+ * Remove a previously registered handler for one event. A no-op if the handler
86
+ * was never registered, so double-unsubscribe is safe.
87
+ */
88
+ off<K extends keyof RuntimeEvents>(e: K, cb: Listener<K>): void {
89
+ this.listeners.get(e)?.delete(cb as Listener<keyof RuntimeEvents>);
90
+ }
91
+
92
+ /**
93
+ * Subscribe to every event in one handler (for forwarding, iteration, or
94
+ * logging). The payload is `unknown` because it varies per event; a consumer
95
+ * narrows on the event name. Returns an unsubscribe function like `on`.
96
+ */
97
+ onAny(cb: AnyListener): () => void {
98
+ this.anyListeners.add(cb);
99
+ return () => {
100
+ this.anyListeners.delete(cb);
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Deliver an event to its handlers, then to every `onAny` handler.
106
+ *
107
+ * We iterate over a copy of each listener set so a handler that subscribes or
108
+ * unsubscribes during dispatch does not mutate the set we are walking. A throw
109
+ * from one handler must not stop the others or skip the `onAny` fan-out, so
110
+ * each call is isolated and a failure is swallowed (the emitter has no logger;
111
+ * a handler owns its own error handling).
112
+ */
113
+ emit<K extends keyof RuntimeEvents>(e: K, d: RuntimeEvents[K]): void {
114
+ const set = this.listeners.get(e);
115
+ if (set) {
116
+ for (const cb of [...set]) {
117
+ try {
118
+ (cb as Listener<K>)(d);
119
+ } catch {
120
+ // A misbehaving listener must not break delivery to the others.
121
+ }
122
+ }
123
+ }
124
+ for (const cb of [...this.anyListeners]) {
125
+ try {
126
+ cb(e, d);
127
+ } catch {
128
+ // Same isolation for the catch-all listeners.
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @fileoverview Runtime LLM API: provider-neutral completions.
3
+ *
4
+ * A thin client over the runtime's `/api/llm/*` REST endpoints. Plain
5
+ * request/response (no WebSocket push, like maps), so this module only needs
6
+ * the shared HTTP helper. The cloud holds the provider credential; the consumer
7
+ * (a miniapp via `session.ai`) never sees a key or a vendor response shape.
8
+ *
9
+ * The wire types are canonical in the protocol package (the runtime server uses
10
+ * the same ones); re-export them so a host gets them from this module.
11
+ */
12
+ import type { HttpClient } from "../../http";
13
+ import type {
14
+ LlmCompleteRequest,
15
+ LlmCompleteResult,
16
+ LlmGenerateJobCreated,
17
+ LlmGenerateJobStatus,
18
+ LlmGenerateRequest,
19
+ LlmGenerateResult,
20
+ } from "@glassly/cloud-protocol";
21
+
22
+ export type {
23
+ LlmCompleteRequest,
24
+ LlmCompleteResult,
25
+ LlmGenerateJobCreated,
26
+ LlmGenerateJobStatus,
27
+ LlmGenerateRequest,
28
+ LlmGenerateResult,
29
+ LlmGeneratedImage,
30
+ LlmMessage,
31
+ LlmProvider,
32
+ LlmStopReason,
33
+ LlmTool,
34
+ LlmToolCall,
35
+ LlmToolResult,
36
+ LlmUsage,
37
+ } from "@glassly/cloud-protocol";
38
+
39
+ const COMPLETE_PATH = "/api/llm/complete";
40
+ const GENERATE_PATH = "/api/llm/generate";
41
+
42
+ export interface LlmDeps {
43
+ http: HttpClient;
44
+ }
45
+
46
+ /** Extra per-call options that ride as headers rather than in the body. */
47
+ export interface LlmCompleteOptions {
48
+ /**
49
+ * The user's own provider key. Sent as a header so it never lands in a
50
+ * request-body log. When present the platform bills that key instead of its
51
+ * own and the call is not metered.
52
+ */
53
+ userApiKey?: string;
54
+ }
55
+
56
+ export class Llm {
57
+ private readonly http: HttpClient;
58
+
59
+ constructor(deps: LlmDeps) {
60
+ this.http = deps.http;
61
+ }
62
+
63
+ /**
64
+ * Run one completion turn.
65
+ *
66
+ * NOT marked idempotent: a completion is a billed, non-deterministic
67
+ * generation, so a transparent retry after a transient blip could charge
68
+ * twice and return different text. Callers decide whether to retry.
69
+ */
70
+ complete(req: LlmCompleteRequest, opts: LlmCompleteOptions = {}): Promise<LlmCompleteResult> {
71
+ return this.http.post<LlmCompleteResult>(COMPLETE_PATH, req, {
72
+ headers: opts.userApiKey ? { "x-provider-key": opts.userApiKey } : undefined,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Run one agentic visual-generation call (server-side web search + code
78
+ * execution; returns text plus rendered images). Long-running — the server
79
+ * budgets ~150s of wall clock — and billed like a completion plus a flat
80
+ * surcharge, so like complete() it is NOT idempotent and never auto-retried.
81
+ */
82
+ generate(req: LlmGenerateRequest, opts: LlmCompleteOptions = {}): Promise<LlmGenerateResult> {
83
+ return this.http.post<LlmGenerateResult>(GENERATE_PATH, req, {
84
+ headers: opts.userApiKey ? { "x-provider-key": opts.userApiKey } : undefined,
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Async flavor of generate(): start a pollable job. The job is per runtime
90
+ * instance and short-lived; a host that starts one polls generateJobStatus()
91
+ * every couple of seconds and cancels via cancelGenerateJob() when the
92
+ * consumer goes away. Old clouds without the surface answer 404 — callers
93
+ * fall back to the sync generate().
94
+ */
95
+ startGenerateJob(req: LlmGenerateRequest, opts: LlmCompleteOptions = {}): Promise<LlmGenerateJobCreated> {
96
+ return this.http.post<LlmGenerateJobCreated>(`${GENERATE_PATH}/jobs`, req, {
97
+ headers: opts.userApiKey ? { "x-provider-key": opts.userApiKey } : undefined,
98
+ });
99
+ }
100
+
101
+ /** Poll one job. GET, so the http layer already retries transient blips. */
102
+ generateJobStatus(jobId: string): Promise<LlmGenerateJobStatus> {
103
+ return this.http.get<LlmGenerateJobStatus>(`${GENERATE_PATH}/jobs/${encodeURIComponent(jobId)}`);
104
+ }
105
+
106
+ /** Cancel a running job so an abandoned run stops burning tokens. */
107
+ cancelGenerateJob(jobId: string): Promise<LlmGenerateJobStatus> {
108
+ return this.http.delete<LlmGenerateJobStatus>(`${GENERATE_PATH}/jobs/${encodeURIComponent(jobId)}`);
109
+ }
110
+ }
@@ -0,0 +1,105 @@
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
+ MapsRenderTokenResult,
19
+ PlaceAutocompleteResult,
20
+ PlaceDetailsResult,
21
+ ReverseGeocodeResult,
22
+ } from "@glassly/cloud-protocol";
23
+
24
+ export type {
25
+ DirectionsRequest,
26
+ DirectionsResult,
27
+ Route,
28
+ RouteStep,
29
+ LatLng,
30
+ TravelMode,
31
+ ManeuverKind,
32
+ RouteAvoidances,
33
+ ReverseGeocodeResult,
34
+ PlaceSuggestion,
35
+ PlaceAutocompleteResult,
36
+ PlaceDetailsResult,
37
+ MapsRenderTokenResult,
38
+ } from "@glassly/cloud-protocol";
39
+
40
+ const DIRECTIONS_PATH = "/api/maps/directions";
41
+ const REVERSE_GEOCODE_PATH = "/api/maps/reverse-geocode";
42
+ const PLACE_AUTOCOMPLETE_PATH = "/api/maps/place-autocomplete";
43
+ const PLACE_DETAILS_PATH = "/api/maps/place-details";
44
+ const RENDER_TOKEN_PATH = "/api/maps/render-token";
45
+
46
+ export interface MapsDeps {
47
+ http: HttpClient;
48
+ }
49
+
50
+ export class Maps {
51
+ private readonly http: HttpClient;
52
+
53
+ constructor(deps: MapsDeps) {
54
+ this.http = deps.http;
55
+ }
56
+
57
+ /** Compute routes from origin through stops. Primary route first, alternates after. */
58
+ directions(req: DirectionsRequest): Promise<DirectionsResult> {
59
+ // Idempotent: a directions request is a pure read, safe to retry on a
60
+ // transient network failure.
61
+ return this.http.post<DirectionsResult>(DIRECTIONS_PATH, req, { idempotent: true });
62
+ }
63
+
64
+ /**
65
+ * Resolve a coordinate to a short road name (`road`) + full formatted address
66
+ * (`address`). Each is null when none of that kind is found near the coord.
67
+ */
68
+ reverseGeocode(coord: LatLng): Promise<ReverseGeocodeResult> {
69
+ return this.http.post<ReverseGeocodeResult>(REVERSE_GEOCODE_PATH, coord, {
70
+ idempotent: true,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Type-ahead place search. `sessionToken` groups the keystrokes with the
76
+ * following `placeDetails` pick into one billed search session.
77
+ */
78
+ placeAutocomplete(req: {
79
+ query: string;
80
+ near?: LatLng;
81
+ sessionToken: string;
82
+ }): Promise<PlaceAutocompleteResult> {
83
+ // Idempotent: autocomplete is a pure read, safe to retry on a transient blip.
84
+ return this.http.post<PlaceAutocompleteResult>(PLACE_AUTOCOMPLETE_PATH, req, {
85
+ idempotent: true,
86
+ });
87
+ }
88
+
89
+ /** Resolve an autocomplete suggestion (`placeId` + same `sessionToken`) to coordinates. */
90
+ placeDetails(req: { placeId: string; sessionToken: string }): Promise<PlaceDetailsResult> {
91
+ return this.http.post<PlaceDetailsResult>(PLACE_DETAILS_PATH, req, {
92
+ idempotent: true,
93
+ });
94
+ }
95
+
96
+ /**
97
+ * The deployment's public client-side render token, for the map consumers
98
+ * that cannot be proxied (tile rendering, native map SDKs). `token` is null
99
+ * when the deployment serves none — the caller falls back to requiring a
100
+ * user-supplied key.
101
+ */
102
+ renderToken(): Promise<MapsRenderTokenResult> {
103
+ return this.http.get<MapsRenderTokenResult>(RENDER_TOKEN_PATH);
104
+ }
105
+ }