@bikefinder/api-client 0.0.0-stage → 0.2.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.
- package/README.md +118 -2
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/client/client.gen.d.ts +3 -0
- package/dist/generated/client/client.gen.d.ts.map +1 -0
- package/dist/generated/client/client.gen.js +218 -0
- package/dist/generated/client/client.gen.js.map +1 -0
- package/dist/generated/client/index.d.ts +11 -0
- package/dist/generated/client/index.d.ts.map +1 -0
- package/dist/generated/client/index.js +7 -0
- package/dist/generated/client/index.js.map +1 -0
- package/dist/generated/client/types.gen.d.ts +121 -0
- package/dist/generated/client/types.gen.d.ts.map +1 -0
- package/dist/generated/client/types.gen.js +3 -0
- package/dist/generated/client/types.gen.js.map +1 -0
- package/dist/generated/client/utils.gen.d.ts +38 -0
- package/dist/generated/client/utils.gen.d.ts.map +1 -0
- package/dist/generated/client/utils.gen.js +232 -0
- package/dist/generated/client/utils.gen.js.map +1 -0
- package/dist/generated/client.gen.d.ts +13 -0
- package/dist/generated/client.gen.d.ts.map +1 -0
- package/dist/generated/client.gen.js +4 -0
- package/dist/generated/client.gen.js.map +1 -0
- package/dist/generated/core/auth.gen.d.ts +26 -0
- package/dist/generated/core/auth.gen.d.ts.map +1 -0
- package/dist/generated/core/auth.gen.js +15 -0
- package/dist/generated/core/auth.gen.js.map +1 -0
- package/dist/generated/core/bodySerializer.gen.d.ts +26 -0
- package/dist/generated/core/bodySerializer.gen.d.ts.map +1 -0
- package/dist/generated/core/bodySerializer.gen.js +58 -0
- package/dist/generated/core/bodySerializer.gen.js.map +1 -0
- package/dist/generated/core/params.gen.d.ts +44 -0
- package/dist/generated/core/params.gen.d.ts.map +1 -0
- package/dist/generated/core/params.gen.js +113 -0
- package/dist/generated/core/params.gen.js.map +1 -0
- package/dist/generated/core/pathSerializer.gen.d.ts +34 -0
- package/dist/generated/core/pathSerializer.gen.d.ts.map +1 -0
- package/dist/generated/core/pathSerializer.gen.js +115 -0
- package/dist/generated/core/pathSerializer.gen.js.map +1 -0
- package/dist/generated/core/queryKeySerializer.gen.d.ts +19 -0
- package/dist/generated/core/queryKeySerializer.gen.d.ts.map +1 -0
- package/dist/generated/core/queryKeySerializer.gen.js +100 -0
- package/dist/generated/core/queryKeySerializer.gen.js.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.d.ts +72 -0
- package/dist/generated/core/serverSentEvents.gen.d.ts.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.js +137 -0
- package/dist/generated/core/serverSentEvents.gen.js.map +1 -0
- package/dist/generated/core/types.gen.d.ts +84 -0
- package/dist/generated/core/types.gen.d.ts.map +1 -0
- package/dist/generated/core/types.gen.js +3 -0
- package/dist/generated/core/types.gen.js.map +1 -0
- package/dist/generated/core/utils.gen.d.ts +20 -0
- package/dist/generated/core/utils.gen.d.ts.map +1 -0
- package/dist/generated/core/utils.gen.js +88 -0
- package/dist/generated/core/utils.gen.js.map +1 -0
- package/dist/generated/index.d.ts +3 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +3 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/generated/sdk.gen.d.ts +177 -0
- package/dist/generated/sdk.gen.d.ts.map +1 -0
- package/dist/generated/sdk.gen.js +536 -0
- package/dist/generated/sdk.gen.js.map +1 -0
- package/dist/generated/types.gen.d.ts +2019 -0
- package/dist/generated/types.gen.d.ts.map +1 -0
- package/dist/generated/types.gen.js +3 -0
- package/dist/generated/types.gen.js.map +1 -0
- package/dist/generated/zod.gen.d.ts +1165 -0
- package/dist/generated/zod.gen.d.ts.map +1 -0
- package/dist/generated/zod.gen.js +874 -0
- package/dist/generated/zod.gen.js.map +1 -0
- package/dist/http-client.d.ts +3 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +29 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +13 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +40 -0
- package/dist/options.js.map +1 -0
- package/dist/realtime/api-stream-error.d.ts +8 -0
- package/dist/realtime/api-stream-error.d.ts.map +1 -0
- package/dist/realtime/api-stream-error.js +16 -0
- package/dist/realtime/api-stream-error.js.map +1 -0
- package/dist/realtime/watch.d.ts +21 -0
- package/dist/realtime/watch.d.ts.map +1 -0
- package/dist/realtime/watch.js +95 -0
- package/dist/realtime/watch.js.map +1 -0
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +84 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -4
- package/src/errors.ts +6 -0
- package/src/generated/client/client.gen.ts +300 -0
- package/src/generated/client/index.ts +27 -0
- package/src/generated/client/types.gen.ts +238 -0
- package/src/generated/client/utils.gen.ts +337 -0
- package/src/generated/client.gen.ts +24 -0
- package/src/generated/core/auth.gen.ts +49 -0
- package/src/generated/core/bodySerializer.gen.ts +96 -0
- package/src/generated/core/params.gen.ts +188 -0
- package/src/generated/core/pathSerializer.gen.ts +186 -0
- package/src/generated/core/queryKeySerializer.gen.ts +134 -0
- package/src/generated/core/serverSentEvents.gen.ts +265 -0
- package/src/generated/core/types.gen.ts +126 -0
- package/src/generated/core/utils.gen.ts +146 -0
- package/src/generated/index.ts +261 -0
- package/src/generated/sdk.gen.ts +1226 -0
- package/src/generated/types.gen.ts +2352 -0
- package/src/generated/zod.gen.ts +1039 -0
- package/src/http-client.ts +31 -0
- package/src/index.ts +30 -0
- package/src/options.ts +61 -0
- package/src/realtime/api-stream-error.ts +20 -0
- package/src/realtime/watch.ts +151 -0
- package/src/transport.ts +99 -0
- package/src/types.ts +5 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createClient } from "./generated/client/index.js";
|
|
2
|
+
import type { ResolvedClientOptions } from "./options.js";
|
|
3
|
+
import { createAuthenticatedFetch } from "./transport.js";
|
|
4
|
+
|
|
5
|
+
export function createHttpClient(options: ResolvedClientOptions) {
|
|
6
|
+
const authenticatedFetch = createAuthenticatedFetch(options);
|
|
7
|
+
const client = createClient({
|
|
8
|
+
baseUrl: options.baseUrl,
|
|
9
|
+
redirect: "error",
|
|
10
|
+
credentials: "omit",
|
|
11
|
+
fetch: authenticatedFetch,
|
|
12
|
+
responseStyle: "fields",
|
|
13
|
+
});
|
|
14
|
+
client.interceptors.request.use((request, requestOptions) => {
|
|
15
|
+
// A per-call fetch would bypass authentication and transport policy.
|
|
16
|
+
if (requestOptions.fetch !== authenticatedFetch) {
|
|
17
|
+
throw new TypeError(
|
|
18
|
+
"Configure fetch on createBikeFinderClient, not individual requests.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return request;
|
|
22
|
+
});
|
|
23
|
+
client.interceptors.error.use((error, response) => {
|
|
24
|
+
// Transport/auth/cancellation failures are exceptions, not HTTP Problem Details.
|
|
25
|
+
if (!response) throw error;
|
|
26
|
+
if (response.ok)
|
|
27
|
+
throw new TypeError("The API returned an invalid response.");
|
|
28
|
+
return error;
|
|
29
|
+
});
|
|
30
|
+
return client;
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { resolveClientOptions, type ApiClientOptions } from "./options.js";
|
|
2
|
+
import { BikeFinderSdk } from "./generated/sdk.gen.js";
|
|
3
|
+
import { createHttpClient } from "./http-client.js";
|
|
4
|
+
import {
|
|
5
|
+
watch,
|
|
6
|
+
type StreamOperation,
|
|
7
|
+
type StreamOptions,
|
|
8
|
+
} from "./realtime/watch.js";
|
|
9
|
+
import type { TrackerPageQuery } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export { AuthenticationError } from "./errors.js";
|
|
12
|
+
export { ApiStreamError } from "./realtime/api-stream-error.js";
|
|
13
|
+
export type { ApiClientOptions } from "./options.js";
|
|
14
|
+
export type * from "./types.js";
|
|
15
|
+
|
|
16
|
+
export function createBikeFinderClient(options: ApiClientOptions) {
|
|
17
|
+
const resolved = resolveClientOptions(options);
|
|
18
|
+
const requests = new BikeFinderSdk({ client: createHttpClient(resolved) });
|
|
19
|
+
return {
|
|
20
|
+
requests,
|
|
21
|
+
watch: <TOperation extends StreamOperation>(
|
|
22
|
+
operation: TOperation,
|
|
23
|
+
options: StreamOptions<TOperation>,
|
|
24
|
+
) => watch(requests, operation, options),
|
|
25
|
+
watchTrackers: (query: TrackerPageQuery = {}) =>
|
|
26
|
+
watch(requests, "subscribeTrackers", { query }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type BikeFinderClient = ReturnType<typeof createBikeFinderClient>;
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AuthenticationError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export interface ApiClientOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
getToken: (forceRefresh?: boolean) => Promise<string>;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolvedClientOptions {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
getToken: ApiClientOptions["getToken"];
|
|
12
|
+
fetch: typeof globalThis.fetch;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveClientOptions(
|
|
16
|
+
options: ApiClientOptions,
|
|
17
|
+
): ResolvedClientOptions {
|
|
18
|
+
let url: URL;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(options.baseUrl);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new TypeError("baseUrl must be an absolute HTTP or HTTPS URL.");
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
26
|
+
url.username ||
|
|
27
|
+
url.password ||
|
|
28
|
+
url.search ||
|
|
29
|
+
url.hash
|
|
30
|
+
) {
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
"baseUrl must be HTTP or HTTPS without credentials, query or fragment.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
37
|
+
if (!fetch) {
|
|
38
|
+
throw new TypeError("Supply a Fetch API implementation for this runtime.");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
baseUrl: url.href.replace(/\/+$/, ""),
|
|
42
|
+
getToken: options.getToken,
|
|
43
|
+
fetch,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getAuthorization(
|
|
48
|
+
options: Pick<ApiClientOptions, "getToken">,
|
|
49
|
+
forceRefresh = false,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
try {
|
|
52
|
+
const token = await options.getToken(forceRefresh);
|
|
53
|
+
if (typeof token !== "string" || !token || /[^\x21-\x7e]/.test(token)) {
|
|
54
|
+
throw new AuthenticationError();
|
|
55
|
+
}
|
|
56
|
+
return `Bearer ${token}`;
|
|
57
|
+
} catch {
|
|
58
|
+
// Identity-provider errors can contain credentials. Expose no original cause.
|
|
59
|
+
throw new AuthenticationError();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ApiStreamErrorCode = "http" | "protocol" | "network";
|
|
2
|
+
|
|
3
|
+
/** A safe stream error. Response bodies, bearer tokens and transport errors are omitted. */
|
|
4
|
+
export class ApiStreamError extends Error {
|
|
5
|
+
readonly code: ApiStreamErrorCode;
|
|
6
|
+
readonly status: number | undefined;
|
|
7
|
+
|
|
8
|
+
constructor(code: ApiStreamErrorCode, status?: number) {
|
|
9
|
+
super(
|
|
10
|
+
code === "http"
|
|
11
|
+
? `The tracker stream returned HTTP ${status}.`
|
|
12
|
+
: code === "protocol"
|
|
13
|
+
? "The tracker stream returned an invalid response."
|
|
14
|
+
: "The tracker stream connection was interrupted.",
|
|
15
|
+
);
|
|
16
|
+
this.name = "ApiStreamError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.status = status;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defer,
|
|
3
|
+
filter,
|
|
4
|
+
merge,
|
|
5
|
+
Observable,
|
|
6
|
+
ReplaySubject,
|
|
7
|
+
retry,
|
|
8
|
+
share,
|
|
9
|
+
Subject,
|
|
10
|
+
timer,
|
|
11
|
+
} from "rxjs";
|
|
12
|
+
import { ZodError } from "zod";
|
|
13
|
+
|
|
14
|
+
import { AuthenticationError } from "../errors.js";
|
|
15
|
+
import type { BikeFinderSdk } from "../generated/sdk.gen.js";
|
|
16
|
+
import type { Options } from "../generated/sdk.gen.js";
|
|
17
|
+
import { ApiStreamError } from "./api-stream-error.js";
|
|
18
|
+
|
|
19
|
+
type Method = (...args: never[]) => unknown;
|
|
20
|
+
type Result<TKey extends keyof BikeFinderSdk> =
|
|
21
|
+
BikeFinderSdk[TKey] extends Method
|
|
22
|
+
? Awaited<ReturnType<BikeFinderSdk[TKey]>>
|
|
23
|
+
: never;
|
|
24
|
+
|
|
25
|
+
export type StreamOperation = {
|
|
26
|
+
[TKey in keyof BikeFinderSdk]: Result<TKey> extends {
|
|
27
|
+
stream: AsyncIterable<unknown>;
|
|
28
|
+
}
|
|
29
|
+
? TKey
|
|
30
|
+
: never;
|
|
31
|
+
}[keyof BikeFinderSdk];
|
|
32
|
+
|
|
33
|
+
export type StreamPayload<TKey extends StreamOperation> =
|
|
34
|
+
Result<TKey> extends {
|
|
35
|
+
stream: AsyncIterable<infer TPayload>;
|
|
36
|
+
}
|
|
37
|
+
? Exclude<TPayload, null>
|
|
38
|
+
: never;
|
|
39
|
+
|
|
40
|
+
export type StreamOptions<TKey extends StreamOperation> = Pick<
|
|
41
|
+
NonNullable<Parameters<BikeFinderSdk[TKey]>[0]>,
|
|
42
|
+
Extract<
|
|
43
|
+
keyof NonNullable<Parameters<BikeFinderSdk[TKey]>[0]>,
|
|
44
|
+
"path" | "query"
|
|
45
|
+
>
|
|
46
|
+
>;
|
|
47
|
+
/**
|
|
48
|
+
* Observe complete snapshots of one tracker page, shared by this observable's consumers.
|
|
49
|
+
* Unsubscribe when leaving the view or changing accounts. Re-subscribe after an error
|
|
50
|
+
* to start a fresh retry budget. Reconnects never request historical event replay.
|
|
51
|
+
*/
|
|
52
|
+
export function watch<TOperation extends StreamOperation>(
|
|
53
|
+
requests: BikeFinderSdk,
|
|
54
|
+
operation: TOperation,
|
|
55
|
+
options: StreamOptions<TOperation>,
|
|
56
|
+
): Observable<StreamPayload<TOperation>> {
|
|
57
|
+
// The generated SDK supplies paths, arguments and validators for every stream.
|
|
58
|
+
// This boundary only adds the shared connection lifecycle.
|
|
59
|
+
const invoke = requests[operation] as (
|
|
60
|
+
options: Options,
|
|
61
|
+
) => Promise<{ stream: AsyncIterable<unknown> }>;
|
|
62
|
+
return defer(() => {
|
|
63
|
+
const disconnected = new Subject<undefined>();
|
|
64
|
+
const snapshots = new Observable<StreamPayload<TOperation>>(
|
|
65
|
+
(subscriber) => {
|
|
66
|
+
disconnected.next(undefined);
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
|
|
69
|
+
const consume = async () => {
|
|
70
|
+
try {
|
|
71
|
+
const { stream } = await invoke.call(requests, {
|
|
72
|
+
...options,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
headers: { Accept: "text/event-stream" },
|
|
75
|
+
// Each RxJS retry starts a new generated stream with fresh credentials.
|
|
76
|
+
// The generated transport must not reconnect with an old token itself.
|
|
77
|
+
sseMaxRetryAttempts: 1,
|
|
78
|
+
onSseError: (error) => {
|
|
79
|
+
throw error;
|
|
80
|
+
},
|
|
81
|
+
onSseEvent: ({ event, data }) => {
|
|
82
|
+
if (subscriber.closed || event === "keepalive") return;
|
|
83
|
+
// A comment-only frame is a transport heartbeat, not an observation.
|
|
84
|
+
if (event === undefined && data === undefined) return;
|
|
85
|
+
if (event !== "snapshot") throw new ApiStreamError("protocol");
|
|
86
|
+
// The generated transformer validates and normalizes every JSON
|
|
87
|
+
// payload. Non-JSON SSE strings bypass it and are never snapshots.
|
|
88
|
+
if (data === null || typeof data !== "object") {
|
|
89
|
+
throw new ApiStreamError("protocol");
|
|
90
|
+
}
|
|
91
|
+
subscriber.next(data as StreamPayload<TOperation>);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
// Event callbacks retain SSE event names; consuming drives the generated
|
|
95
|
+
// async iterator. There is no handwritten URL, query serializer or parser.
|
|
96
|
+
for await (const _ of stream) {
|
|
97
|
+
if (subscriber.closed) break;
|
|
98
|
+
}
|
|
99
|
+
if (!subscriber.closed) throw new ApiStreamError("network");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (subscriber.closed) return;
|
|
102
|
+
disconnected.next(undefined);
|
|
103
|
+
subscriber.error(
|
|
104
|
+
error instanceof AuthenticationError ||
|
|
105
|
+
error instanceof ApiStreamError
|
|
106
|
+
? error
|
|
107
|
+
: new ApiStreamError(
|
|
108
|
+
error instanceof ZodError ? "protocol" : "network",
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
void consume();
|
|
114
|
+
return () => controller.abort();
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return merge(
|
|
119
|
+
disconnected,
|
|
120
|
+
snapshots.pipe(
|
|
121
|
+
retry({
|
|
122
|
+
count: 8,
|
|
123
|
+
resetOnSuccess: true,
|
|
124
|
+
delay: (error: unknown, attempt) => {
|
|
125
|
+
const retryable =
|
|
126
|
+
error instanceof ApiStreamError &&
|
|
127
|
+
(error.code === "network" ||
|
|
128
|
+
(error.code === "http" &&
|
|
129
|
+
(error.status === 408 ||
|
|
130
|
+
error.status === 429 ||
|
|
131
|
+
(error.status ?? 0) >= 500)));
|
|
132
|
+
if (!retryable) throw error;
|
|
133
|
+
const maximum = Math.min(30_000, 1_000 * 2 ** (attempt - 1));
|
|
134
|
+
return timer(maximum * (0.5 + Math.random() * 0.5));
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}).pipe(
|
|
140
|
+
share({
|
|
141
|
+
connector: () =>
|
|
142
|
+
new ReplaySubject<StreamPayload<TOperation> | undefined>(1),
|
|
143
|
+
resetOnError: true,
|
|
144
|
+
resetOnComplete: true,
|
|
145
|
+
resetOnRefCountZero: true,
|
|
146
|
+
}),
|
|
147
|
+
// An internal disconnect marker replaces the cached page during backoff, so a
|
|
148
|
+
// new subscriber never receives an old snapshot as though it were current.
|
|
149
|
+
filter((page): page is StreamPayload<TOperation> => page !== undefined),
|
|
150
|
+
);
|
|
151
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AuthenticationError } from "./errors.js";
|
|
2
|
+
import { getAuthorization, type ResolvedClientOptions } from "./options.js";
|
|
3
|
+
import { ApiStreamError } from "./realtime/api-stream-error.js";
|
|
4
|
+
|
|
5
|
+
/** Shared bearer authentication and transport policy for generated REST and SSE calls. */
|
|
6
|
+
export function createAuthenticatedFetch(
|
|
7
|
+
options: ResolvedClientOptions,
|
|
8
|
+
): typeof globalThis.fetch {
|
|
9
|
+
const origin = new URL(options.baseUrl).origin;
|
|
10
|
+
return async (input, init) => {
|
|
11
|
+
const request = new Request(input, {
|
|
12
|
+
...init,
|
|
13
|
+
redirect: "error",
|
|
14
|
+
credentials: "omit",
|
|
15
|
+
});
|
|
16
|
+
const streaming =
|
|
17
|
+
request.headers
|
|
18
|
+
.get("Accept")
|
|
19
|
+
?.split(",")
|
|
20
|
+
.some(
|
|
21
|
+
(value) =>
|
|
22
|
+
value.split(";")[0]?.trim().toLowerCase() === "text/event-stream",
|
|
23
|
+
) ?? false;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
throwIfAborted(request.signal);
|
|
27
|
+
// Generated clients allow per-call base URLs; credentials belong only to
|
|
28
|
+
// the API origin configured on the factory.
|
|
29
|
+
if (new URL(request.url).origin !== origin) {
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
"Authenticated requests must target the configured API origin.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
request.headers.set("Authorization", await getAuthorization(options));
|
|
35
|
+
throwIfAborted(request.signal);
|
|
36
|
+
const retry = request.clone();
|
|
37
|
+
// RN's Request polyfill drops redirect. Expo fetch accepts that Request,
|
|
38
|
+
// but needs the redirect policy explicitly in its second argument.
|
|
39
|
+
let response = await options.fetch(request, {
|
|
40
|
+
redirect: "error",
|
|
41
|
+
credentials: "omit",
|
|
42
|
+
signal: request.signal,
|
|
43
|
+
});
|
|
44
|
+
if (response.status === 401) {
|
|
45
|
+
throwIfAborted(request.signal);
|
|
46
|
+
await response.body?.cancel().catch(() => {});
|
|
47
|
+
retry.headers.set(
|
|
48
|
+
"Authorization",
|
|
49
|
+
await getAuthorization(options, true),
|
|
50
|
+
);
|
|
51
|
+
throwIfAborted(retry.signal);
|
|
52
|
+
response = await options.fetch(retry, {
|
|
53
|
+
redirect: "error",
|
|
54
|
+
credentials: "omit",
|
|
55
|
+
signal: retry.signal,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!streaming) return response;
|
|
59
|
+
|
|
60
|
+
if (response.status !== 200) {
|
|
61
|
+
await response.body?.cancel().catch(() => {});
|
|
62
|
+
if (response.status === 401) throw new AuthenticationError();
|
|
63
|
+
throw new ApiStreamError("http", response.status);
|
|
64
|
+
}
|
|
65
|
+
const contentType = response.headers
|
|
66
|
+
.get("Content-Type")
|
|
67
|
+
?.split(";")[0]
|
|
68
|
+
?.trim()
|
|
69
|
+
.toLowerCase();
|
|
70
|
+
if (contentType !== "text/event-stream" || !response.body) {
|
|
71
|
+
await response.body?.cancel().catch(() => {});
|
|
72
|
+
throw new ApiStreamError("protocol");
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (
|
|
77
|
+
!streaming ||
|
|
78
|
+
error instanceof AuthenticationError ||
|
|
79
|
+
error instanceof ApiStreamError
|
|
80
|
+
) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
throwIfAborted(request.signal);
|
|
84
|
+
throw new ApiStreamError("network");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
90
|
+
// Expo's React Native AbortSignal supports aborted, but not throwIfAborted().
|
|
91
|
+
if (signal?.aborted) {
|
|
92
|
+
throw (
|
|
93
|
+
signal.reason ??
|
|
94
|
+
Object.assign(new Error("The request was cancelled."), {
|
|
95
|
+
name: "AbortError",
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SubscribeTrackersData } from "./generated/types.gen.js";
|
|
2
|
+
|
|
3
|
+
export type * from "./generated/types.gen.js";
|
|
4
|
+
export type { ProblemDetails as ApiProblem } from "./generated/types.gen.js";
|
|
5
|
+
export type TrackerPageQuery = NonNullable<SubscribeTrackersData["query"]>;
|