@toapi/client 0.12.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.
- package/dist/cache.d.ts +38 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +207 -0
- package/dist/client-types.d.ts +55 -0
- package/dist/client-types.d.ts.map +1 -0
- package/dist/client-types.js +0 -0
- package/dist/create-fetch-client.d.ts +15 -0
- package/dist/create-fetch-client.d.ts.map +1 -0
- package/dist/create-fetch-client.js +152 -0
- package/dist/handle-response.d.ts +2 -0
- package/dist/handle-response.d.ts.map +1 -0
- package/dist/handle-response.js +54 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/pub-sub.d.ts +11 -0
- package/dist/pub-sub.d.ts.map +1 -0
- package/dist/pub-sub.js +33 -0
- package/dist/route-types.d.ts +4 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +0 -0
- package/package.json +43 -0
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Logger } from "@toapi/common";
|
|
2
|
+
import type { Observable } from "./client-types.js";
|
|
3
|
+
type ObservablePromise = Promise<unknown> & Observable<unknown>;
|
|
4
|
+
type Subscription = (data: Promise<unknown>) => void;
|
|
5
|
+
interface CacheEntry {
|
|
6
|
+
current?: {
|
|
7
|
+
value: ObservablePromise;
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
tags: Set<string>;
|
|
10
|
+
};
|
|
11
|
+
next?: ObservablePromise;
|
|
12
|
+
queuedRevalidation?: Promise<void>;
|
|
13
|
+
fetch: () => Promise<Response>;
|
|
14
|
+
subscriptions: Set<Subscription>;
|
|
15
|
+
timeout: ReturnType<typeof setTimeout> | null;
|
|
16
|
+
}
|
|
17
|
+
interface Options {
|
|
18
|
+
minTTL?: number;
|
|
19
|
+
maxOverdueTTL?: number;
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
}
|
|
22
|
+
export declare class Cache {
|
|
23
|
+
private storage;
|
|
24
|
+
private tagIndex;
|
|
25
|
+
private minTTL;
|
|
26
|
+
private maxOverdueTTL;
|
|
27
|
+
private errorLog;
|
|
28
|
+
constructor(options: Options);
|
|
29
|
+
private emplace;
|
|
30
|
+
private revalidateRequest;
|
|
31
|
+
request(url: string, fetch: () => Promise<Response>): ObservablePromise;
|
|
32
|
+
queueRevalidation(url: string, entry: CacheEntry): Promise<void>;
|
|
33
|
+
revalidateUrl(url: string): Promise<void>;
|
|
34
|
+
revalidateTags(tags: string[]): Promise<void>;
|
|
35
|
+
queueClearEntry(url: string): void;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,KAAK,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;AAChE,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;AAErD,UAAU,UAAU;IAClB,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,iBAAiB,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;KACnB,CAAC;IACF,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B,aAAa,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;IACjC,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAAC;CAC/C;AAED,UAAU,OAAO;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,qBAAa,KAAK;IAChB,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAA2C;gBAE/C,OAAO,EAAE,OAAO;IAM5B,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,iBAAiB;IA+HzB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,iBAAiB;IAqBjE,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU;IAMhD,aAAa,CAAC,GAAG,EAAE,MAAM;IAuBzB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE;IAcnC,eAAe,CAAC,GAAG,EAAE,MAAM;CAiB5B"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { EXPIRES_AT_HEADER, TAGS_HEADER } from "@toapi/common";
|
|
2
|
+
import { handleResponse } from "./handle-response.js";
|
|
3
|
+
const DEFAULT_MIN_TTL = 5 * 1000;
|
|
4
|
+
const DEFAULT_MAX_OVERDUE_TTL = 1000;
|
|
5
|
+
export class Cache {
|
|
6
|
+
storage = new Map();
|
|
7
|
+
tagIndex = new Map();
|
|
8
|
+
minTTL;
|
|
9
|
+
maxOverdueTTL;
|
|
10
|
+
errorLog;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.minTTL = options.minTTL ?? DEFAULT_MIN_TTL;
|
|
13
|
+
this.maxOverdueTTL = options.maxOverdueTTL ?? DEFAULT_MAX_OVERDUE_TTL;
|
|
14
|
+
this.errorLog = options.logger?.error ?? console.error;
|
|
15
|
+
}
|
|
16
|
+
emplace(url, fetch) {
|
|
17
|
+
const currentEntry = this.storage.get(url);
|
|
18
|
+
if (currentEntry) {
|
|
19
|
+
currentEntry.fetch = fetch;
|
|
20
|
+
return currentEntry;
|
|
21
|
+
}
|
|
22
|
+
const newEntry = {
|
|
23
|
+
subscriptions: new Set(),
|
|
24
|
+
timeout: null,
|
|
25
|
+
fetch,
|
|
26
|
+
};
|
|
27
|
+
this.storage.set(url, newEntry);
|
|
28
|
+
return newEntry;
|
|
29
|
+
}
|
|
30
|
+
revalidateRequest(url, entry) {
|
|
31
|
+
if (entry.timeout) {
|
|
32
|
+
// clear timeout to make sure it the entry doesn't get revalidated again or removed based on TTL
|
|
33
|
+
clearTimeout(entry.timeout);
|
|
34
|
+
}
|
|
35
|
+
// actually load fresh data
|
|
36
|
+
const responsePromise = entry.fetch();
|
|
37
|
+
const observable = Object.assign(responsePromise.then(handleResponse), {
|
|
38
|
+
subscribe: (callback) => {
|
|
39
|
+
if (entry.subscriptions.size === 0) {
|
|
40
|
+
if (entry.timeout) {
|
|
41
|
+
clearTimeout(entry.timeout);
|
|
42
|
+
}
|
|
43
|
+
if (entry.current?.expiresAt) {
|
|
44
|
+
entry.timeout = setTimeout(() => {
|
|
45
|
+
this.revalidateRequest(url, entry);
|
|
46
|
+
}, entry.current.expiresAt - Date.now() +
|
|
47
|
+
Math.round(Math.random() * this.maxOverdueTTL));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
entry.subscriptions.add(callback);
|
|
51
|
+
// If there's a newer revalidation in-flight or completed since
|
|
52
|
+
// this observable was created, notify the new subscriber immediately
|
|
53
|
+
// so it doesn't miss the update.
|
|
54
|
+
if (entry.next && entry.next !== observable) {
|
|
55
|
+
callback(entry.next);
|
|
56
|
+
}
|
|
57
|
+
else if (entry.current && entry.current.value !== observable) {
|
|
58
|
+
// A revalidation completed between cache.request() returning this
|
|
59
|
+
// observable and subscribe() being called — notify with the newer value.
|
|
60
|
+
callback(entry.current.value);
|
|
61
|
+
}
|
|
62
|
+
return () => {
|
|
63
|
+
entry.subscriptions.delete(callback);
|
|
64
|
+
if (entry.subscriptions.size === 0) {
|
|
65
|
+
this.queueClearEntry(url);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
entry.next = observable;
|
|
71
|
+
// notify subscribers
|
|
72
|
+
for (const callback of entry.subscriptions) {
|
|
73
|
+
callback(observable);
|
|
74
|
+
}
|
|
75
|
+
const next = entry.next;
|
|
76
|
+
const waitForRevalidation = async () => {
|
|
77
|
+
try {
|
|
78
|
+
// wait for request to finish
|
|
79
|
+
const response = await responsePromise;
|
|
80
|
+
// this will throw an error if the response is not ok
|
|
81
|
+
await observable;
|
|
82
|
+
if (entry.next !== next) {
|
|
83
|
+
// request was superseded by a new request
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// update entry
|
|
87
|
+
const oldTags = entry.current?.tags ?? new Set();
|
|
88
|
+
const newTags = new Set(response.headers.get(TAGS_HEADER)?.split(" ") ?? []);
|
|
89
|
+
const expiresAtHeader = response.headers.get(EXPIRES_AT_HEADER);
|
|
90
|
+
const expiresAt = expiresAtHeader
|
|
91
|
+
? parseInt(expiresAtHeader, 10)
|
|
92
|
+
: undefined;
|
|
93
|
+
entry.current = {
|
|
94
|
+
value: observable,
|
|
95
|
+
tags: newTags,
|
|
96
|
+
expiresAt,
|
|
97
|
+
};
|
|
98
|
+
entry.next = undefined;
|
|
99
|
+
// add url to tagIndex for new tags
|
|
100
|
+
for (const tag of newTags.difference(oldTags)) {
|
|
101
|
+
const urls = this.tagIndex.get(tag);
|
|
102
|
+
if (urls) {
|
|
103
|
+
urls.add(url);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.tagIndex.set(tag, new Set([url]));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// remove url from tagIndex for removed tags
|
|
110
|
+
for (const tag of oldTags.difference(newTags)) {
|
|
111
|
+
this.tagIndex.get(tag)?.delete(url);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!entry.current) {
|
|
116
|
+
// no current value, we are stuck with the rejected promise
|
|
117
|
+
entry.current = {
|
|
118
|
+
value: observable,
|
|
119
|
+
tags: new Set(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// there is a current valid value, we'll keep that an drop the error
|
|
123
|
+
entry.next = undefined;
|
|
124
|
+
await this.errorLog(error);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
if (entry.subscriptions.size === 0) {
|
|
128
|
+
// clear response after minTTL if no subscribers active
|
|
129
|
+
this.queueClearEntry(url);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
return { observable, revalidated: waitForRevalidation() };
|
|
134
|
+
}
|
|
135
|
+
request(url, fetch) {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
let entry = this.emplace(url, fetch);
|
|
138
|
+
if (entry.current) {
|
|
139
|
+
// has cached value, serve from cache
|
|
140
|
+
return entry.current.value;
|
|
141
|
+
}
|
|
142
|
+
// no cached value
|
|
143
|
+
if (entry.next) {
|
|
144
|
+
// already loading, serve from next
|
|
145
|
+
return entry.next;
|
|
146
|
+
}
|
|
147
|
+
// nothing cached, load fresh
|
|
148
|
+
const { observable } = this.revalidateRequest(url, entry);
|
|
149
|
+
return observable;
|
|
150
|
+
}
|
|
151
|
+
async queueRevalidation(url, entry) {
|
|
152
|
+
await entry.next;
|
|
153
|
+
const { revalidated } = this.revalidateRequest(url, entry);
|
|
154
|
+
await revalidated;
|
|
155
|
+
}
|
|
156
|
+
async revalidateUrl(url) {
|
|
157
|
+
const entry = this.storage.get(url);
|
|
158
|
+
if (!entry)
|
|
159
|
+
return;
|
|
160
|
+
if (entry.next) {
|
|
161
|
+
// we are currently fetching that url
|
|
162
|
+
if (entry.queuedRevalidation) {
|
|
163
|
+
// another revalidation is already queued, wait for it to finish
|
|
164
|
+
await entry.queuedRevalidation;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// queue another revalidation
|
|
168
|
+
const queuedRevalidation = this.queueRevalidation(url, entry);
|
|
169
|
+
entry.queuedRevalidation = queuedRevalidation;
|
|
170
|
+
await queuedRevalidation;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// there is no request in flight, revalidate immediately
|
|
175
|
+
const { revalidated } = this.revalidateRequest(url, entry);
|
|
176
|
+
await revalidated;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async revalidateTags(tags) {
|
|
180
|
+
// collect urls to revalidate
|
|
181
|
+
let urls = new Set();
|
|
182
|
+
for (const tag of tags) {
|
|
183
|
+
const taggedUrls = this.tagIndex.get(tag);
|
|
184
|
+
if (!taggedUrls)
|
|
185
|
+
continue;
|
|
186
|
+
urls = urls.union(taggedUrls);
|
|
187
|
+
}
|
|
188
|
+
// revalidate urls and wait until all are resolved or rejected
|
|
189
|
+
await Promise.allSettled(Array.from(urls).map((url) => this.revalidateUrl(url)));
|
|
190
|
+
}
|
|
191
|
+
queueClearEntry(url) {
|
|
192
|
+
const entry = this.storage.get(url);
|
|
193
|
+
if (!entry)
|
|
194
|
+
return;
|
|
195
|
+
if (entry.timeout) {
|
|
196
|
+
clearTimeout(entry.timeout);
|
|
197
|
+
}
|
|
198
|
+
entry.timeout = setTimeout(() => {
|
|
199
|
+
if (entry.current) {
|
|
200
|
+
for (const tag of entry.current.tags) {
|
|
201
|
+
this.tagIndex.get(tag)?.delete(url);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
this.storage.delete(url);
|
|
205
|
+
}, this.minTTL);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Path as BasePath } from "@toapi/common";
|
|
2
|
+
import type { BaseRoute } from "@toapi/common";
|
|
3
|
+
import type { Handler as BaseHandler } from "@toapi/common";
|
|
4
|
+
import type { MaybePromise } from "@toapi/common";
|
|
5
|
+
type Segment<Path> = Path extends `/${infer Segment}/${string}` ? Segment : Path extends `/${infer Segment}` ? Segment : never;
|
|
6
|
+
type Rest<Path, Segment extends string> = Path extends `/${Segment}/${infer Rest}` ? `/${Rest}` : never;
|
|
7
|
+
export type Client<Routes extends Record<BasePath, MaybePromise<BaseRoute>>> = {
|
|
8
|
+
[segment in Segment<keyof Routes> as segment extends `:${string}` ? string | number : segment extends `*${string}` ? string | number : segment]: (Extract<keyof Routes, `/${segment}`> extends never ? {} : ClientRoute<Routes[`/${segment}`]>) & (Exclude<keyof Routes, `/${segment}`> extends never ? {} : Client<{
|
|
9
|
+
[rest in Rest<keyof Routes, segment>]: Routes[`/${segment}${rest}`];
|
|
10
|
+
}>);
|
|
11
|
+
};
|
|
12
|
+
type ClientRoute<Route extends MaybePromise<BaseRoute>> = {
|
|
13
|
+
get: QueryWithoutBody<Awaited<Route>["GET"]>;
|
|
14
|
+
post: MutationWithBody<Awaited<Route>["POST"]>;
|
|
15
|
+
delete: MutationWithoutBody<Awaited<Route>["DELETE"]>;
|
|
16
|
+
put: MutationWithBody<Awaited<Route>["PUT"]>;
|
|
17
|
+
patch: MutationWithBody<Awaited<Route>["PATCH"]>;
|
|
18
|
+
revalidate: () => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export type QueryWithoutBody<Handler extends BaseHandler<any, any, any, undefined> | undefined> = Handler extends undefined ? never : RequiredKeys<QueryType<Handler>> extends never ? (query?: Partial<QueryType<Handler>>, req?: RequestInit) => Promise<ResponseType<Handler>> & Observable<ResponseType<Handler>> : (query: OptionalizeUndefined<QueryType<Handler>>, req?: RequestInit) => Promise<ResponseType<Handler>> & Observable<ResponseType<Handler>>;
|
|
21
|
+
export type Observable<T> = {
|
|
22
|
+
subscribe(callback: (value: Promise<T>) => void): () => void;
|
|
23
|
+
};
|
|
24
|
+
export type Revalidating = {
|
|
25
|
+
revalidated: Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
export type MutationWithoutBody<Handler extends BaseHandler<any, any, any, undefined> | undefined> = Handler extends undefined ? never : RequiredKeys<QueryType<Handler>> extends never ? (query?: Partial<QueryType<Handler>>, req?: RequestInit) => Promise<ResponseType<Handler>> & Revalidating : (query: OptionalizeUndefined<QueryType<Handler>>, req?: RequestInit) => Promise<ResponseType<Handler>> & Revalidating;
|
|
28
|
+
export type MutationWithBody<Handler extends BaseHandler<any, any, any, unknown> | undefined> = Handler extends undefined ? never : (body?: BodyType<Handler> | FormData, req?: RequestInit & {
|
|
29
|
+
query?: OptionalizeUndefined<QueryType<Handler>>;
|
|
30
|
+
}) => Promise<ResponseType<Handler>> & Revalidating;
|
|
31
|
+
type QueryType<Handler extends {
|
|
32
|
+
schema: {
|
|
33
|
+
__q?: any;
|
|
34
|
+
};
|
|
35
|
+
} | undefined> = NonNullable<NonNullable<Handler>["schema"]["__q"]>;
|
|
36
|
+
type RequiredKeys<T> = {
|
|
37
|
+
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
|
38
|
+
}[keyof T];
|
|
39
|
+
type OptionalizeUndefined<T> = {
|
|
40
|
+
[K in keyof T as undefined extends T[K] ? never : K]: T[K];
|
|
41
|
+
} & {
|
|
42
|
+
[K in keyof T as undefined extends T[K] ? K : never]?: T[K];
|
|
43
|
+
};
|
|
44
|
+
type BodyType<Handler extends {
|
|
45
|
+
schema: {
|
|
46
|
+
__b?: any;
|
|
47
|
+
};
|
|
48
|
+
} | undefined> = NonNullable<NonNullable<Handler>["schema"]["__b"]>;
|
|
49
|
+
type ResponseType<Handler extends {
|
|
50
|
+
schema: {
|
|
51
|
+
__r?: any;
|
|
52
|
+
};
|
|
53
|
+
} | undefined> = NonNullable<NonNullable<Handler>["schema"]["__r"]>;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=client-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-types.d.ts","sourceRoot":"","sources":["../src/client-types.ts"],"names":[],"mappings":"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"}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Logger } from "@toapi/common";
|
|
2
|
+
import type { MaybePromise } from "@toapi/common";
|
|
3
|
+
import type { Path as BasePath } from "@toapi/common";
|
|
4
|
+
import type { BaseRoute } from "@toapi/common";
|
|
5
|
+
import type { Client } from "./client-types.js";
|
|
6
|
+
interface Options {
|
|
7
|
+
fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
8
|
+
minTTL?: number;
|
|
9
|
+
maxOverdueTTL?: number;
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
invalidationsUrl?: string | false;
|
|
12
|
+
}
|
|
13
|
+
export declare function createFetchClient<Routes extends Record<BasePath, MaybePromise<BaseRoute>>>(apiUrl: string, options?: Options): Client<Routes>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=create-fetch-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-fetch-client.d.ts","sourceRoot":"","sources":["../src/create-fetch-client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,mBAAmB,CAAC;AAiC9D,UAAU,OAAO;IACf,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACnC;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,SAAS,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,EACxD,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,OAAY,GAyGpB,MAAM,CAAC,MAAM,CAAC,CAChC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { INVALIDATION_POST_EVENT, INVALIDATIONS_ROUTE, TAGS_HEADER, } from "@toapi/common";
|
|
2
|
+
import { Cache } from "./cache.js";
|
|
3
|
+
import { handleResponse } from "./handle-response.js";
|
|
4
|
+
const globalFetch = fetch;
|
|
5
|
+
async function listenForInvalidations(url, cache) {
|
|
6
|
+
const MAX_ATTEMPTS = 1000;
|
|
7
|
+
for (let retry = 0; retry < MAX_ATTEMPTS; retry++) {
|
|
8
|
+
try {
|
|
9
|
+
const res = await globalFetch(url);
|
|
10
|
+
if (!res.ok || !res.body)
|
|
11
|
+
break;
|
|
12
|
+
let buffer = "";
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
for await (const chunk of res.body) {
|
|
15
|
+
buffer += decoder.decode(chunk);
|
|
16
|
+
const lines = buffer.split("\n");
|
|
17
|
+
buffer = lines.pop() ?? "";
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const rawTags = line.trim();
|
|
20
|
+
if (!rawTags)
|
|
21
|
+
continue;
|
|
22
|
+
await cache.revalidateTags(rawTags.split(" "));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// network error — retry below
|
|
28
|
+
}
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * Math.pow(2, Math.min(retry, 10))));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function createFetchClient(apiUrl, options = {}) {
|
|
33
|
+
const fetch = options.fetch ?? globalFetch;
|
|
34
|
+
const cache = new Cache({
|
|
35
|
+
minTTL: options.minTTL,
|
|
36
|
+
maxOverdueTTL: options.maxOverdueTTL,
|
|
37
|
+
logger: options.logger,
|
|
38
|
+
});
|
|
39
|
+
const invalidationsUrl = options.invalidationsUrl === false
|
|
40
|
+
? null
|
|
41
|
+
: (options.invalidationsUrl ?? apiUrl + INVALIDATIONS_ROUTE);
|
|
42
|
+
if ("serviceWorker" in navigator) {
|
|
43
|
+
navigator.serviceWorker.addEventListener("message", async (event) => {
|
|
44
|
+
if (typeof event.data === "object" &&
|
|
45
|
+
event.data !== null &&
|
|
46
|
+
"type" in event.data &&
|
|
47
|
+
event.data.type === INVALIDATION_POST_EVENT) {
|
|
48
|
+
try {
|
|
49
|
+
await cache.revalidateTags(event.data.tags);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.warn("TApi: Failed to revalidate tags: received invalid post message", event.data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (invalidationsUrl && !navigator.serviceWorker.controller) {
|
|
57
|
+
listenForInvalidations(invalidationsUrl, cache);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (invalidationsUrl && typeof window !== "undefined") {
|
|
61
|
+
listenForInvalidations(invalidationsUrl, cache);
|
|
62
|
+
}
|
|
63
|
+
function load(url, init = {}) {
|
|
64
|
+
return cache.request(url, () => fetch(url, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
...init,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
async function revalidate(url) {
|
|
70
|
+
await cache.revalidateUrl(url);
|
|
71
|
+
}
|
|
72
|
+
function mutate(method, url, data, init = {}) {
|
|
73
|
+
const headers = new Headers(init.headers);
|
|
74
|
+
if (!(data instanceof FormData) && !headers.has("Content-Type")) {
|
|
75
|
+
headers.set("Content-Type", "application/json");
|
|
76
|
+
}
|
|
77
|
+
const res = fetch(url, {
|
|
78
|
+
method,
|
|
79
|
+
body: typeof data === "undefined"
|
|
80
|
+
? undefined
|
|
81
|
+
: data instanceof FormData
|
|
82
|
+
? data
|
|
83
|
+
: JSON.stringify(data),
|
|
84
|
+
...init,
|
|
85
|
+
headers,
|
|
86
|
+
});
|
|
87
|
+
const revalidationPromise = res.then((res) => cache.revalidateTags(res.headers.get(TAGS_HEADER)?.split(" ") ?? []));
|
|
88
|
+
return Object.assign(res
|
|
89
|
+
.then((res) => handleResponse(res))
|
|
90
|
+
.catch(async (error) => {
|
|
91
|
+
await options?.logger?.error?.(error);
|
|
92
|
+
throw error;
|
|
93
|
+
}), {
|
|
94
|
+
revalidated: revalidationPromise,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return new Proxy(() => { }, {
|
|
98
|
+
get(_target, prop) {
|
|
99
|
+
return createProxy({
|
|
100
|
+
revalidate,
|
|
101
|
+
load,
|
|
102
|
+
mutate,
|
|
103
|
+
}, apiUrl, prop);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function buildUrl(baseUrl, query) {
|
|
108
|
+
if (!query || typeof query !== "object")
|
|
109
|
+
return baseUrl;
|
|
110
|
+
const searchParams = new URLSearchParams();
|
|
111
|
+
for (const [key, value] of Object.entries(query)) {
|
|
112
|
+
if (typeof value === "undefined")
|
|
113
|
+
continue;
|
|
114
|
+
searchParams.append(key, value);
|
|
115
|
+
}
|
|
116
|
+
return searchParams.size > 0 ? baseUrl + "?" + searchParams : baseUrl;
|
|
117
|
+
}
|
|
118
|
+
function createProxy(methods, baseUrl, lastProp) {
|
|
119
|
+
return new Proxy(() => { }, {
|
|
120
|
+
get(_target, prop) {
|
|
121
|
+
if (typeof prop === "symbol") {
|
|
122
|
+
if (prop === Symbol.toPrimitive) {
|
|
123
|
+
return function toPrimitive() {
|
|
124
|
+
return `[TApi Route ${baseUrl}/${lastProp}]`;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return createProxy(methods, baseUrl + "/" + lastProp, prop);
|
|
130
|
+
},
|
|
131
|
+
apply(_target, _thisArg, args) {
|
|
132
|
+
switch (lastProp) {
|
|
133
|
+
case "revalidate": {
|
|
134
|
+
return methods.revalidate(buildUrl(baseUrl, args[0]));
|
|
135
|
+
}
|
|
136
|
+
case "get": {
|
|
137
|
+
return methods.load(buildUrl(baseUrl, args[0]), args[1]);
|
|
138
|
+
}
|
|
139
|
+
case "delete": {
|
|
140
|
+
return methods.mutate("DELETE", buildUrl(baseUrl, args[0]), undefined, args[1]);
|
|
141
|
+
}
|
|
142
|
+
case "post":
|
|
143
|
+
case "put":
|
|
144
|
+
case "patch": {
|
|
145
|
+
return methods.mutate(lastProp.toUpperCase(), buildUrl(baseUrl, args[1]?.query), args[0], args[1]);
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Tapi: Unsupported method: ${lastProp}`);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle-response.d.ts","sourceRoot":"","sources":["../src/handle-response.ts"],"names":[],"mappings":"AAGA,wBAAsB,cAAc,CAAC,GAAG,EAAE,QAAQ,gBAmBjD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { HttpError } from "@toapi/common";
|
|
3
|
+
export async function handleResponse(res) {
|
|
4
|
+
switch (res.status) {
|
|
5
|
+
case 200:
|
|
6
|
+
return parseBody(res);
|
|
7
|
+
// @ts-expect-error explicit fallthrough
|
|
8
|
+
case 400:
|
|
9
|
+
if (res.headers.get("Content-Type") === "application/json+zodissues") {
|
|
10
|
+
const issues = await res.json();
|
|
11
|
+
throw new ZodError(issues);
|
|
12
|
+
}
|
|
13
|
+
default:
|
|
14
|
+
if (res.headers.get("Content-Type") === "application/json+httperror") {
|
|
15
|
+
const { message, data } = await res.json();
|
|
16
|
+
throw new HttpError(res.status, message, data);
|
|
17
|
+
}
|
|
18
|
+
throw new HttpError(res.status, res.statusText);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function parseBody(res) {
|
|
22
|
+
const contentType = res.headers.get("Content-Type");
|
|
23
|
+
if (contentType == "application/x-ndjson") {
|
|
24
|
+
return streamBody(res);
|
|
25
|
+
}
|
|
26
|
+
if (contentType?.startsWith("application/json")) {
|
|
27
|
+
return await res.json();
|
|
28
|
+
}
|
|
29
|
+
if (contentType?.startsWith("text/plain")) {
|
|
30
|
+
return await res.text();
|
|
31
|
+
}
|
|
32
|
+
const contentLength = res.headers.get("Content-Length");
|
|
33
|
+
if (contentLength === "0") {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Tapi: Unsupported content type: ${contentType}`);
|
|
37
|
+
}
|
|
38
|
+
async function* streamBody(res) {
|
|
39
|
+
if (!res.body)
|
|
40
|
+
return;
|
|
41
|
+
let buffer = "";
|
|
42
|
+
const textDecoder = new TextDecoder();
|
|
43
|
+
for await (const chunk of res.body) {
|
|
44
|
+
buffer += textDecoder.decode(chunk);
|
|
45
|
+
const lines = buffer.split("\n");
|
|
46
|
+
buffer = lines.pop() ?? "";
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
yield JSON.parse(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (buffer) {
|
|
52
|
+
yield JSON.parse(buffer);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { Client, Observable } from "./client-types.js";
|
|
2
|
+
export { createFetchClient } from "./create-fetch-client.js";
|
|
3
|
+
export type { GetRoute, PostRoute } from "./route-types.js";
|
|
4
|
+
export { HttpError } from "@toapi/common";
|
|
5
|
+
export { INVALIDATIONS_ROUTE } from "@toapi/common";
|
|
6
|
+
export type { Logger } from "@toapi/common";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class PubSub {
|
|
2
|
+
private subscriptions;
|
|
3
|
+
private onClear;
|
|
4
|
+
constructor({ onClear }: {
|
|
5
|
+
onClear: (url: string) => void;
|
|
6
|
+
});
|
|
7
|
+
subscribe(url: string, callback: (data: Promise<unknown>) => void): () => void;
|
|
8
|
+
publish(url: string, data: Promise<unknown>): void;
|
|
9
|
+
has(url: string): boolean;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=pub-sub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pub-sub.d.ts","sourceRoot":"","sources":["../src/pub-sub.ts"],"names":[],"mappings":"AAAA,qBAAa,MAAM;IACjB,OAAO,CAAC,aAAa,CAGjB;IACJ,OAAO,CAAC,OAAO,CAAwB;gBAE3B,EAAE,OAAO,EAAE,EAAE;QAAE,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE;IAI3D,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI;IAgBjE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC;IAS3C,GAAG,CAAC,GAAG,EAAE,MAAM;CAGhB"}
|
package/dist/pub-sub.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class PubSub {
|
|
2
|
+
subscriptions = new Map();
|
|
3
|
+
onClear;
|
|
4
|
+
constructor({ onClear }) {
|
|
5
|
+
this.onClear = onClear;
|
|
6
|
+
}
|
|
7
|
+
subscribe(url, callback) {
|
|
8
|
+
let urlSubscriptions = this.subscriptions.get(url);
|
|
9
|
+
if (!urlSubscriptions) {
|
|
10
|
+
urlSubscriptions = new Set();
|
|
11
|
+
this.subscriptions.set(url, urlSubscriptions);
|
|
12
|
+
}
|
|
13
|
+
urlSubscriptions.add(callback);
|
|
14
|
+
return () => {
|
|
15
|
+
urlSubscriptions.delete(callback);
|
|
16
|
+
if (urlSubscriptions.size === 0) {
|
|
17
|
+
this.subscriptions.delete(url);
|
|
18
|
+
this.onClear(url);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
publish(url, data) {
|
|
23
|
+
const urlSubscriptions = this.subscriptions.get(url);
|
|
24
|
+
if (urlSubscriptions) {
|
|
25
|
+
for (const callback of urlSubscriptions.values()) {
|
|
26
|
+
callback(data);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
has(url) {
|
|
31
|
+
return this.subscriptions.has(url);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Observable } from "./client-types.js";
|
|
2
|
+
export type GetRoute<R, Q = undefined> = Q extends undefined ? (query?: {}, req?: RequestInit) => Promise<R> & Observable<R> : (query: Q, req?: RequestInit) => Promise<R> & Observable<R>;
|
|
3
|
+
export type PostRoute<R, Q = unknown, B = unknown> = (query: Q | FormData, body: B | FormData, req?: RequestInit) => Promise<R>;
|
|
4
|
+
//# sourceMappingURL=route-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-types.d.ts","sourceRoot":"","sources":["../src/route-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,IAAI,CAAC,SAAS,SAAS,GACxD,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAC7D,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;AAIhE,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,IAAI,CACnD,KAAK,EAAE,CAAC,GAAG,QAAQ,EACnB,IAAI,EAAE,CAAC,GAAG,QAAQ,EAClB,GAAG,CAAC,EAAE,WAAW,KACd,OAAO,CAAC,CAAC,CAAC,CAAC"}
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toapi/client",
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Michel Smola",
|
|
6
|
+
"email": "michel.smola@farbenmeer.de"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"private": false,
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@toapi/common": "^0.12.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.0.3",
|
|
27
|
+
"vitest": "^4.0.16",
|
|
28
|
+
"zod": "^4.0.17"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"typescript": "^5 || ^6.0.0",
|
|
32
|
+
"zod": "^4.0.17"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/farbenmeer/tapi.git"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc --noEmit false",
|
|
40
|
+
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
41
|
+
"test": "vitest run --passWithNoTests"
|
|
42
|
+
}
|
|
43
|
+
}
|