@toapi/client 1.2.4 → 1.3.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 +24 -0
- package/dist/build-observable.d.ts +13 -0
- package/dist/build-observable.d.ts.map +1 -0
- package/dist/build-observable.js +19 -0
- package/dist/cache.d.ts +16 -13
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +61 -213
- package/dist/create-fetch-client.d.ts.map +1 -1
- package/dist/create-fetch-client.js +40 -62
- package/dist/extract-metadata.d.ts +6 -0
- package/dist/extract-metadata.d.ts.map +1 -0
- package/dist/extract-metadata.js +8 -0
- package/dist/invalidation-stream.d.ts +11 -0
- package/dist/invalidation-stream.d.ts.map +1 -0
- package/dist/invalidation-stream.js +85 -0
- package/dist/pub-sub.d.ts +11 -7
- package/dist/pub-sub.d.ts.map +1 -1
- package/dist/pub-sub.js +30 -24
- package/dist/request.d.ts +10 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +21 -0
- package/package.json +7 -7
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @toapi/client
|
|
2
|
+
|
|
3
|
+
The typed fetch client for Toapi: a proxy that mirrors your [@toapi/server](https://www.npmjs.com/package/@toapi/server) API, with deduplication, tag-based revalidation, and subscriptions.
|
|
4
|
+
|
|
5
|
+
📚 [Docs](https://toapi-js.github.io/toapi/client/)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @toapi/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createFetchClient } from "@toapi/client";
|
|
17
|
+
import type { api } from "./api"; // the type of your server's defineApi(...)
|
|
18
|
+
|
|
19
|
+
const client = createFetchClient<typeof api.routes>("https://example.com/api");
|
|
20
|
+
|
|
21
|
+
const users = await client.users.get();
|
|
22
|
+
await client.users.post({ name: "Alice" });
|
|
23
|
+
const user = await client.users["123"].get();
|
|
24
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Observable } from "@toapi/common";
|
|
2
|
+
import type { PubSub } from "./pub-sub.js";
|
|
3
|
+
import type { Cache } from "./cache.js";
|
|
4
|
+
interface Options {
|
|
5
|
+
fetch(url: string, init: RequestInit): Promise<Response>;
|
|
6
|
+
url: string;
|
|
7
|
+
init: RequestInit;
|
|
8
|
+
pubSub: PubSub;
|
|
9
|
+
cache: Cache;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildObservable(options: Options): Observable<unknown> & Promise<unknown>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=build-observable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-observable.d.ts","sourceRoot":"","sources":["../src/build-observable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,UAAU,OAAO;IACf,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAMD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,OAAO,GACf,UAAU,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBxC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { request } from "./request.js";
|
|
2
|
+
export function buildObservable(options) {
|
|
3
|
+
const { url, pubSub } = options;
|
|
4
|
+
const { data } = request(options);
|
|
5
|
+
function subscribe(callback) {
|
|
6
|
+
const { data } = request(options);
|
|
7
|
+
callback(data);
|
|
8
|
+
return pubSub.subscribe(async (invalidUrls) => {
|
|
9
|
+
if (invalidUrls.has(url)) {
|
|
10
|
+
const { data } = request(options);
|
|
11
|
+
callback(data);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return Object.assign(data, {
|
|
16
|
+
subscribe,
|
|
17
|
+
queryKey: url,
|
|
18
|
+
});
|
|
19
|
+
}
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { type Logger } from "@toapi/common";
|
|
2
|
-
import {
|
|
2
|
+
import type { Metadata } from "./extract-metadata.js";
|
|
3
|
+
import type { PubSub } from "./pub-sub.js";
|
|
4
|
+
export interface CacheEntry {
|
|
5
|
+
response: Promise<Response>;
|
|
6
|
+
meta: Promise<Metadata>;
|
|
7
|
+
data: Promise<unknown>;
|
|
8
|
+
}
|
|
3
9
|
interface Options {
|
|
4
|
-
minTTL?: number;
|
|
5
10
|
maxOverdueTTL?: number;
|
|
6
11
|
logger?: Logger;
|
|
12
|
+
minTTL: number;
|
|
13
|
+
pubSub: PubSub;
|
|
7
14
|
}
|
|
8
15
|
export declare class Cache {
|
|
9
16
|
private storage;
|
|
10
17
|
private tagIndex;
|
|
11
|
-
private minTTL;
|
|
12
18
|
private maxOverdueTTL;
|
|
19
|
+
private minTTL;
|
|
13
20
|
private errorLog;
|
|
21
|
+
private pubSub;
|
|
14
22
|
constructor(options: Options);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
private loadFreshData;
|
|
21
|
-
private subscribe;
|
|
22
|
-
private unsubscribe;
|
|
23
|
-
private setTimer;
|
|
24
|
-
private extractMetadata;
|
|
23
|
+
set(url: string, entry: CacheEntry): void;
|
|
24
|
+
get(url: string): CacheEntry | undefined;
|
|
25
|
+
invalidateUrl(url: string): Promise<void>;
|
|
26
|
+
invalidateTags(tags: string[]): Promise<void>;
|
|
27
|
+
invalidateAll(): Promise<void>;
|
|
25
28
|
}
|
|
26
29
|
export {};
|
|
27
30
|
//# sourceMappingURL=cache.d.ts.map
|
package/dist/cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5B,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CACxB;AAED,UAAU,OAAO;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,qBAAa,KAAK;IAChB,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAA2C;IAC3D,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,OAAO;IAOrB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU;IA8ClC,GAAG,CAAC,GAAG,EAAE,MAAM;IAIf,aAAa,CAAC,GAAG,EAAE,MAAM;IAKzB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE;IAiB7B,aAAa;CAMrB"}
|
package/dist/cache.js
CHANGED
|
@@ -1,240 +1,88 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { handleResponse } from "./handle-response.js";
|
|
3
|
-
import { init, queue, resolve, revalidate, revert, } from "./state-machine.js";
|
|
4
|
-
const DEFAULT_MIN_TTL = 5 * 1000;
|
|
1
|
+
import {} from "@toapi/common";
|
|
5
2
|
const DEFAULT_MAX_OVERDUE_TTL = 1000;
|
|
6
3
|
export class Cache {
|
|
7
4
|
storage = new Map();
|
|
8
5
|
tagIndex = new Map();
|
|
9
|
-
minTTL;
|
|
10
6
|
maxOverdueTTL;
|
|
7
|
+
minTTL;
|
|
11
8
|
errorLog;
|
|
9
|
+
pubSub;
|
|
12
10
|
constructor(options) {
|
|
13
|
-
this.minTTL = options.minTTL ?? DEFAULT_MIN_TTL;
|
|
14
11
|
this.maxOverdueTTL = options.maxOverdueTTL ?? DEFAULT_MAX_OVERDUE_TTL;
|
|
15
12
|
this.errorLog = options.logger?.error ?? console.error;
|
|
13
|
+
this.pubSub = options.pubSub;
|
|
14
|
+
this.minTTL = options.minTTL;
|
|
16
15
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return entry.state.queued ?? entry.state.next;
|
|
24
|
-
case "cached":
|
|
25
|
-
return entry.state.value;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
const { observable } = this.loadFreshData(url, fetch);
|
|
30
|
-
this.storage.set(url, {
|
|
31
|
-
state: init(observable),
|
|
32
|
-
fetch,
|
|
33
|
-
subscriptions: new Set(),
|
|
34
|
-
timeout: null,
|
|
35
|
-
});
|
|
36
|
-
return observable;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async setResolveHook(url, responsePromise, observable) {
|
|
40
|
-
try {
|
|
41
|
-
const response = await responsePromise;
|
|
42
|
-
await observable;
|
|
43
|
-
const entry = this.storage.get(url);
|
|
44
|
-
switch (entry?.state.status) {
|
|
45
|
-
case "pending": {
|
|
46
|
-
const { tags, expiresAt } = this.extractMetadata(response);
|
|
47
|
-
for (const tag of tags) {
|
|
48
|
-
const urls = this.tagIndex.get(tag);
|
|
49
|
-
if (urls) {
|
|
50
|
-
urls.add(url);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
this.tagIndex.set(tag, new Set([url]));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
entry.state = resolve(entry.state, observable, tags, expiresAt);
|
|
57
|
-
this.setTimer(url, entry);
|
|
58
|
-
return;
|
|
16
|
+
set(url, entry) {
|
|
17
|
+
this.storage.set(url, entry);
|
|
18
|
+
entry.data.catch((error) => {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
if (this.storage.get(url) === entry) {
|
|
21
|
+
this.storage.delete(url);
|
|
59
22
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
for (const tag of entry.state.tags.difference(tags)) {
|
|
74
|
-
this.tagIndex.get(tag)?.delete(url);
|
|
75
|
-
}
|
|
76
|
-
entry.state = resolve(entry.state, observable, tags, expiresAt);
|
|
77
|
-
this.setTimer(url, entry);
|
|
78
|
-
return;
|
|
23
|
+
}, this.minTTL);
|
|
24
|
+
return this.errorLog(error);
|
|
25
|
+
});
|
|
26
|
+
entry.meta
|
|
27
|
+
.then(({ tags, expiresAt }) => {
|
|
28
|
+
const tagsToSet = new Set(tags);
|
|
29
|
+
// sync with tagIndex
|
|
30
|
+
for (const [tag, urls] of this.tagIndex) {
|
|
31
|
+
if (tagsToSet.delete(tag)) {
|
|
32
|
+
urls.add(url);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
urls.delete(url);
|
|
79
36
|
}
|
|
80
37
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// not found, no access etc, evict the entry
|
|
85
|
-
this.evictEntry(url);
|
|
86
|
-
return;
|
|
38
|
+
for (const tag of tagsToSet) {
|
|
39
|
+
// these are only tags that are not in the index yet
|
|
40
|
+
this.tagIndex.set(tag, new Set([url]));
|
|
87
41
|
}
|
|
88
|
-
|
|
89
|
-
if (!entry) {
|
|
90
|
-
// evicted, ignore
|
|
42
|
+
if (!expiresAt)
|
|
91
43
|
return;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.evictEntry(url);
|
|
99
|
-
return;
|
|
100
|
-
case "revalidating":
|
|
101
|
-
// revert to last good state
|
|
102
|
-
entry.state = revert(entry.state);
|
|
44
|
+
const timeUntilRevalidation = expiresAt -
|
|
45
|
+
Date.now() +
|
|
46
|
+
Math.round(Math.random() * this.maxOverdueTTL);
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
const cached = this.storage.get(url);
|
|
49
|
+
if (cached !== entry)
|
|
103
50
|
return;
|
|
104
|
-
|
|
105
|
-
|
|
51
|
+
this.storage.delete(url);
|
|
52
|
+
this.pubSub.publish(new Set([url]));
|
|
53
|
+
}, Math.max(0, timeUntilRevalidation));
|
|
54
|
+
})
|
|
55
|
+
.catch(() => { });
|
|
106
56
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// no entry, nothing to do
|
|
110
|
-
if (!entry)
|
|
111
|
-
return Promise.resolve();
|
|
112
|
-
// no subscribers, evict the entry
|
|
113
|
-
if (entry.subscriptions.size === 0) {
|
|
114
|
-
this.evictEntry(url);
|
|
115
|
-
return Promise.resolve();
|
|
116
|
-
}
|
|
117
|
-
const { observable, resolved } = this.loadFreshData(url, entry.fetch, entry.state.status === "revalidating"
|
|
118
|
-
? entry.state.next
|
|
119
|
-
: Promise.resolve());
|
|
120
|
-
switch (entry.state.status) {
|
|
121
|
-
case "pending":
|
|
122
|
-
// pending is stale now, replace it with a fresh request
|
|
123
|
-
entry.state = init(observable);
|
|
124
|
-
break;
|
|
125
|
-
case "cached":
|
|
126
|
-
// revalidate
|
|
127
|
-
entry.state = revalidate(entry.state, observable);
|
|
128
|
-
break;
|
|
129
|
-
case "revalidating":
|
|
130
|
-
// already revalidating, queue
|
|
131
|
-
entry.state = queue(entry.state, observable);
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
// notify subscribers
|
|
135
|
-
for (const callback of entry.subscriptions) {
|
|
136
|
-
callback(observable);
|
|
137
|
-
}
|
|
138
|
-
return resolved;
|
|
57
|
+
get(url) {
|
|
58
|
+
return this.storage.get(url);
|
|
139
59
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
60
|
+
invalidateUrl(url) {
|
|
61
|
+
this.storage.delete(url);
|
|
62
|
+
return this.pubSub.publish(new Set([url]));
|
|
63
|
+
}
|
|
64
|
+
invalidateTags(tags) {
|
|
65
|
+
const urls = new Set();
|
|
143
66
|
for (const tag of tags) {
|
|
144
67
|
const taggedUrls = this.tagIndex.get(tag);
|
|
145
68
|
if (!taggedUrls)
|
|
146
69
|
continue;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
await Promise.allSettled(Array.from(urls).map((url) => this.revalidateUrl(url)));
|
|
151
|
-
}
|
|
152
|
-
evictEntry(url) {
|
|
153
|
-
const entry = this.storage.get(url);
|
|
154
|
-
if (!entry)
|
|
155
|
-
return;
|
|
156
|
-
switch (entry.state.status) {
|
|
157
|
-
case "cached":
|
|
158
|
-
case "revalidating":
|
|
159
|
-
// remove tags from index
|
|
160
|
-
for (const tag of entry.state.tags) {
|
|
161
|
-
this.tagIndex.get(tag)?.delete(url);
|
|
70
|
+
for (const url of taggedUrls) {
|
|
71
|
+
if (this.storage.has(url)) {
|
|
72
|
+
this.storage.delete(url);
|
|
162
73
|
}
|
|
74
|
+
else {
|
|
75
|
+
taggedUrls.delete(url);
|
|
76
|
+
}
|
|
77
|
+
urls.add(url);
|
|
78
|
+
}
|
|
163
79
|
}
|
|
164
|
-
this.
|
|
165
|
-
}
|
|
166
|
-
loadFreshData(url, fetch, waitFor = Promise.resolve()) {
|
|
167
|
-
// actually load fresh data
|
|
168
|
-
const responsePromise = waitFor.then(() => fetch());
|
|
169
|
-
const observable = Object.assign(responsePromise.then(handleResponse), {
|
|
170
|
-
subscribe: (callback) => this.subscribe(url, fetch, callback),
|
|
171
|
-
});
|
|
172
|
-
const resolved = this.setResolveHook(url, responsePromise, observable);
|
|
173
|
-
return { observable, resolved };
|
|
174
|
-
}
|
|
175
|
-
subscribe(url, fetch, callback) {
|
|
176
|
-
const entry = this.storage.get(url);
|
|
177
|
-
if (!entry) {
|
|
178
|
-
// has been evicted
|
|
179
|
-
const { observable } = this.loadFreshData(url, fetch);
|
|
180
|
-
const newEntry = {
|
|
181
|
-
state: init(observable),
|
|
182
|
-
fetch,
|
|
183
|
-
subscriptions: new Set([callback]),
|
|
184
|
-
timeout: null,
|
|
185
|
-
};
|
|
186
|
-
this.storage.set(url, newEntry);
|
|
187
|
-
callback(observable);
|
|
188
|
-
return () => this.unsubscribe(url, newEntry, callback);
|
|
189
|
-
}
|
|
190
|
-
// set up subscription
|
|
191
|
-
entry.subscriptions.add(callback);
|
|
192
|
-
if (entry.subscriptions.size === 1) {
|
|
193
|
-
// was scheduled for eviction, reset the timer
|
|
194
|
-
this.setTimer(url, entry);
|
|
195
|
-
}
|
|
196
|
-
// Immediately push the current value to the new subscriber
|
|
197
|
-
// so it doesn't miss any updates
|
|
198
|
-
switch (entry.state.status) {
|
|
199
|
-
case "cached":
|
|
200
|
-
callback(entry.state.value);
|
|
201
|
-
break;
|
|
202
|
-
case "pending":
|
|
203
|
-
case "revalidating":
|
|
204
|
-
callback(entry.state.queued ?? entry.state.next);
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
return () => this.unsubscribe(url, entry, callback);
|
|
208
|
-
}
|
|
209
|
-
unsubscribe(url, entry, callback) {
|
|
210
|
-
entry.subscriptions.delete(callback);
|
|
211
|
-
this.setTimer(url, entry);
|
|
212
|
-
}
|
|
213
|
-
setTimer(url, entry) {
|
|
214
|
-
if (entry.timeout) {
|
|
215
|
-
clearTimeout(entry.timeout);
|
|
216
|
-
}
|
|
217
|
-
if (entry.subscriptions.size === 0) {
|
|
218
|
-
// no active subscriptions, set up eviction timer
|
|
219
|
-
entry.timeout = setTimeout(() => this.evictEntry(url), this.minTTL);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (entry.state.status === "cached" && entry.state.expiresAt) {
|
|
223
|
-
// entry expires, set up revalidation timer
|
|
224
|
-
const timeUntilRevalidation = entry.state.expiresAt -
|
|
225
|
-
Date.now() +
|
|
226
|
-
Math.round(Math.random() * this.maxOverdueTTL);
|
|
227
|
-
entry.timeout = setTimeout(() => this.revalidateUrl(url), Math.max(0, timeUntilRevalidation));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// no timeout necessary
|
|
231
|
-
entry.timeout = null;
|
|
80
|
+
return this.pubSub.publish(urls);
|
|
232
81
|
}
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
82
|
+
invalidateAll() {
|
|
83
|
+
const urls = new Set(this.storage.keys());
|
|
84
|
+
this.storage.clear();
|
|
85
|
+
this.tagIndex.clear();
|
|
86
|
+
return this.pubSub.publish(urls);
|
|
239
87
|
}
|
|
240
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-fetch-client.d.ts","sourceRoot":"","sources":["../src/create-fetch-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,IAAI,QAAQ,EAChB,SAAS,EACT,MAAM,EACN,YAAY,EACb,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"create-fetch-client.d.ts","sourceRoot":"","sources":["../src/create-fetch-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,IAAI,QAAQ,EAChB,SAAS,EACT,MAAM,EACN,YAAY,EACb,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,mBAAmB,CAAC;AAO9D,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;AAID,wBAAgB,iBAAiB,CAC/B,MAAM,SAAS,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,EACxD,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,OAAY,GAkGpB,MAAM,CAAC,MAAM,CAAC,CAChC"}
|
|
@@ -1,80 +1,54 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { INVALIDATIONS_ROUTE, TAGS_HEADER } from "@toapi/common";
|
|
2
|
+
import { buildObservable } from "./build-observable.js";
|
|
2
3
|
import { Cache } from "./cache.js";
|
|
3
4
|
import { handleResponse } from "./handle-response.js";
|
|
5
|
+
import { listenForInvalidations } from "./invalidation-stream.js";
|
|
6
|
+
import { PubSub } from "./pub-sub.js";
|
|
4
7
|
const globalFetch = fetch;
|
|
5
|
-
|
|
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
|
-
}
|
|
8
|
+
const DEFAULT_MIN_TTL = 100;
|
|
32
9
|
export function createFetchClient(apiUrl, options = {}) {
|
|
33
10
|
const fetch = options.fetch ?? globalFetch;
|
|
11
|
+
const minTTL = options?.minTTL ?? DEFAULT_MIN_TTL;
|
|
12
|
+
const pubSub = new PubSub({
|
|
13
|
+
minTTL,
|
|
14
|
+
});
|
|
34
15
|
const cache = new Cache({
|
|
35
|
-
minTTL: options.minTTL,
|
|
36
16
|
maxOverdueTTL: options.maxOverdueTTL,
|
|
37
17
|
logger: options.logger,
|
|
18
|
+
pubSub,
|
|
19
|
+
minTTL,
|
|
38
20
|
});
|
|
39
21
|
const invalidationsUrl = options.invalidationsUrl === false
|
|
40
22
|
? null
|
|
41
23
|
: (options.invalidationsUrl ?? apiUrl + INVALIDATIONS_ROUTE);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
}
|
|
24
|
+
listenForInvalidations({
|
|
25
|
+
fetch,
|
|
26
|
+
onInvalidate: (tags) => cache.invalidateTags(tags),
|
|
27
|
+
onConnect: () => cache.invalidateAll(),
|
|
28
|
+
logger: options.logger,
|
|
29
|
+
invalidationsUrl,
|
|
30
|
+
});
|
|
63
31
|
function load(url, init = {}) {
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
32
|
+
return buildObservable({
|
|
33
|
+
fetch,
|
|
34
|
+
url,
|
|
35
|
+
init: {
|
|
36
|
+
method: "GET",
|
|
37
|
+
...init,
|
|
38
|
+
},
|
|
39
|
+
pubSub,
|
|
40
|
+
cache,
|
|
41
|
+
});
|
|
68
42
|
}
|
|
69
43
|
async function revalidate(url) {
|
|
70
|
-
await cache.
|
|
44
|
+
await cache.invalidateUrl(url);
|
|
71
45
|
}
|
|
72
46
|
function mutate(method, url, data, init = {}) {
|
|
73
47
|
const headers = new Headers(init.headers);
|
|
74
48
|
if (!(data instanceof FormData) && !headers.has("Content-Type")) {
|
|
75
49
|
headers.set("Content-Type", "application/json");
|
|
76
50
|
}
|
|
77
|
-
const
|
|
51
|
+
const response = fetch(url, {
|
|
78
52
|
method,
|
|
79
53
|
body: typeof data === "undefined"
|
|
80
54
|
? undefined
|
|
@@ -84,14 +58,18 @@ export function createFetchClient(apiUrl, options = {}) {
|
|
|
84
58
|
...init,
|
|
85
59
|
headers,
|
|
86
60
|
});
|
|
87
|
-
const
|
|
88
|
-
return Object.assign(res
|
|
89
|
-
.then((res) => handleResponse(res))
|
|
90
|
-
.catch(async (error) => {
|
|
61
|
+
const body = response.then(handleResponse).catch(async (error) => {
|
|
91
62
|
await options?.logger?.error?.(error);
|
|
92
63
|
throw error;
|
|
93
|
-
})
|
|
94
|
-
|
|
64
|
+
});
|
|
65
|
+
const revalidated = response.then(async (res) => {
|
|
66
|
+
await Promise.all([
|
|
67
|
+
body,
|
|
68
|
+
cache.invalidateTags(res.headers.get(TAGS_HEADER)?.split(" ") ?? []),
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
return Object.assign(body, {
|
|
72
|
+
revalidated,
|
|
95
73
|
});
|
|
96
74
|
}
|
|
97
75
|
return new Proxy(() => { }, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-metadata.d.ts","sourceRoot":"","sources":["../src/extract-metadata.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAM5D"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EXPIRES_AT_HEADER, TAGS_HEADER } from "@toapi/common";
|
|
2
|
+
export function extractMetadata(response) {
|
|
3
|
+
const expiresAtHeader = response.headers.get(EXPIRES_AT_HEADER);
|
|
4
|
+
return {
|
|
5
|
+
tags: new Set(response.headers.get(TAGS_HEADER)?.split(" ") ?? []),
|
|
6
|
+
expiresAt: expiresAtHeader ? parseInt(expiresAtHeader, 10) : undefined,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Logger } from "@toapi/common";
|
|
2
|
+
interface Options {
|
|
3
|
+
fetch: (url: string, init: RequestInit) => Promise<Response>;
|
|
4
|
+
invalidationsUrl: string | null;
|
|
5
|
+
onInvalidate(tags: string[]): Promise<void>;
|
|
6
|
+
onConnect(): Promise<void>;
|
|
7
|
+
logger?: Logger;
|
|
8
|
+
}
|
|
9
|
+
export declare function listenForInvalidations(options: Options): void;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=invalidation-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalidation-stream.d.ts","sourceRoot":"","sources":["../src/invalidation-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAEvB,UAAU,OAAO;IACf,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QAoCtD"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { INVALIDATION_POST_EVENT, CONNECT_POST_EVENT, } from "@toapi/common";
|
|
2
|
+
export function listenForInvalidations(options) {
|
|
3
|
+
const { onInvalidate, onConnect, logger } = options;
|
|
4
|
+
const warn = logger?.warn ?? console.warn;
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
navigator.serviceWorker.addEventListener("message", async (event) => {
|
|
7
|
+
if (typeof event.data === "object" &&
|
|
8
|
+
event.data !== null &&
|
|
9
|
+
"type" in event.data) {
|
|
10
|
+
switch (event.data.type) {
|
|
11
|
+
case INVALIDATION_POST_EVENT: {
|
|
12
|
+
try {
|
|
13
|
+
await onInvalidate(event.data.tags);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
warn(`TApi: Failed to revalidate tags: received invalid post message ${JSON.stringify(event.data)}`);
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
case CONNECT_POST_EVENT: {
|
|
21
|
+
await onConnect();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if (!navigator.serviceWorker.controller) {
|
|
28
|
+
fetchInvalidationStream(options);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (typeof window !== "undefined") {
|
|
32
|
+
fetchInvalidationStream(options);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function fetchInvalidationStream({ fetch, onInvalidate, invalidationsUrl, onConnect, logger, }) {
|
|
36
|
+
if (!invalidationsUrl)
|
|
37
|
+
return;
|
|
38
|
+
const warn = logger?.warn ?? console.warn;
|
|
39
|
+
const info = logger?.info ?? console.info;
|
|
40
|
+
const MAX_ATTEMPTS = 500;
|
|
41
|
+
for (let retry = 0; retry < MAX_ATTEMPTS; retry++) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(invalidationsUrl, {});
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
throw `${res.status} ${res.statusText}`;
|
|
46
|
+
if (!res.body) {
|
|
47
|
+
throw `Invalidation stream response has no body`;
|
|
48
|
+
}
|
|
49
|
+
// reset retry counter
|
|
50
|
+
retry = 0;
|
|
51
|
+
// invalidate everything in the cache, it might have gone stale while we were not listening
|
|
52
|
+
await onConnect();
|
|
53
|
+
info("Invalidations stream connection established");
|
|
54
|
+
let buffer = "";
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
for await (const chunk of res.body) {
|
|
57
|
+
buffer += decoder.decode(chunk);
|
|
58
|
+
const lines = buffer.split("\n");
|
|
59
|
+
buffer = lines.pop() ?? "";
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const rawTags = line.trim();
|
|
62
|
+
if (!rawTags)
|
|
63
|
+
continue;
|
|
64
|
+
await onInvalidate(rawTags.split(" "));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const formattedError = error instanceof Error
|
|
70
|
+
? `${error.name} ${error.message}`
|
|
71
|
+
: String(error);
|
|
72
|
+
warn(`Reconnecting to invalidation stream after error: ${formattedError}`);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
// error or stream ended, retry with exponential backoff
|
|
76
|
+
await new Promise((resolve) =>
|
|
77
|
+
// so the retry interval is roughly
|
|
78
|
+
// 0.55s, 0.61s, 0.66s, 0.73s
|
|
79
|
+
// growing exponentially up to 1.9hours and then stays constant
|
|
80
|
+
// for about 63 days before it throws an error
|
|
81
|
+
setTimeout(resolve, 500 * Math.pow(1.1, Math.min(retry, 100))));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw new Error("Failed to reconnect to invalidation stream");
|
|
85
|
+
}
|
package/dist/pub-sub.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
type Subscription = (urls: Set<string>) => Promise<void>;
|
|
2
|
+
interface Options {
|
|
3
|
+
minTTL: number;
|
|
4
|
+
}
|
|
1
5
|
export declare class PubSub {
|
|
2
6
|
private subscriptions;
|
|
3
|
-
private
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
subscribe(
|
|
8
|
-
publish(
|
|
9
|
-
has(url: string): boolean;
|
|
7
|
+
private debounceTimeouts;
|
|
8
|
+
private requestedUrls;
|
|
9
|
+
private minTTL;
|
|
10
|
+
constructor(options: Options);
|
|
11
|
+
subscribe(callback: Subscription): () => void;
|
|
12
|
+
publish(urls: Set<string>): Promise<void>;
|
|
10
13
|
}
|
|
14
|
+
export {};
|
|
11
15
|
//# sourceMappingURL=pub-sub.d.ts.map
|
package/dist/pub-sub.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pub-sub.d.ts","sourceRoot":"","sources":["../src/pub-sub.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"pub-sub.d.ts","sourceRoot":"","sources":["../src/pub-sub.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEzD,UAAU,OAAO;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,gBAAgB,CAAoD;IAC5E,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,OAAO;IAI5B,SAAS,CAAC,QAAQ,EAAE,YAAY;IAO1B,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC;CAkChC"}
|
package/dist/pub-sub.js
CHANGED
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
export class PubSub {
|
|
2
|
-
subscriptions = new
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
subscriptions = new Set();
|
|
3
|
+
debounceTimeouts = new Map();
|
|
4
|
+
requestedUrls = new Set();
|
|
5
|
+
minTTL;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.minTTL = options.minTTL;
|
|
6
8
|
}
|
|
7
|
-
subscribe(
|
|
8
|
-
|
|
9
|
-
if (!urlSubscriptions) {
|
|
10
|
-
urlSubscriptions = new Set();
|
|
11
|
-
this.subscriptions.set(url, urlSubscriptions);
|
|
12
|
-
}
|
|
13
|
-
urlSubscriptions.add(callback);
|
|
9
|
+
subscribe(callback) {
|
|
10
|
+
this.subscriptions.add(callback);
|
|
14
11
|
return () => {
|
|
15
|
-
|
|
16
|
-
if (urlSubscriptions.size === 0) {
|
|
17
|
-
this.subscriptions.delete(url);
|
|
18
|
-
this.onClear(url);
|
|
19
|
-
}
|
|
12
|
+
this.subscriptions.delete(callback);
|
|
20
13
|
};
|
|
21
14
|
}
|
|
22
|
-
publish(
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
async publish(urls) {
|
|
16
|
+
for (const url of urls) {
|
|
17
|
+
if (this.debounceTimeouts.has(url)) {
|
|
18
|
+
// debounced, mark as requested and ignore now
|
|
19
|
+
this.requestedUrls.add(url);
|
|
20
|
+
urls.delete(url);
|
|
27
21
|
}
|
|
28
22
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
const timeout = setTimeout(() => Promise.all(Array.from(urls).map((url) => {
|
|
24
|
+
if (this.requestedUrls.has(url)) {
|
|
25
|
+
this.debounceTimeouts.delete(url);
|
|
26
|
+
// needs to be refetched
|
|
27
|
+
return this.publish(urls);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// debounce timeout over
|
|
31
|
+
this.debounceTimeouts.delete(url);
|
|
32
|
+
}
|
|
33
|
+
})), this.minTTL);
|
|
34
|
+
for (const url of urls) {
|
|
35
|
+
this.debounceTimeouts.set(url, timeout);
|
|
36
|
+
}
|
|
37
|
+
await Promise.all(this.subscriptions.values().map((callback) => callback(urls)));
|
|
32
38
|
}
|
|
33
39
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Cache, CacheEntry } from "./cache.js";
|
|
2
|
+
interface Options {
|
|
3
|
+
fetch(url: string, init: RequestInit): Promise<Response>;
|
|
4
|
+
url: string;
|
|
5
|
+
cache: Cache;
|
|
6
|
+
init: RequestInit;
|
|
7
|
+
}
|
|
8
|
+
export declare function request(options: Options): CacheEntry;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEpD,UAAU,OAAO;IACf,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAqBpD"}
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { extractMetadata } from "./extract-metadata.js";
|
|
2
|
+
import { handleResponse } from "./handle-response.js";
|
|
3
|
+
export function request(options) {
|
|
4
|
+
const { fetch, url, cache, init } = options;
|
|
5
|
+
const cached = cache.get(url);
|
|
6
|
+
if (cached)
|
|
7
|
+
return cached;
|
|
8
|
+
const response = fetch(url, init);
|
|
9
|
+
const data = response.then(handleResponse);
|
|
10
|
+
const meta = response.then(extractMetadata);
|
|
11
|
+
cache.set(url, {
|
|
12
|
+
response,
|
|
13
|
+
data,
|
|
14
|
+
meta,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
response,
|
|
18
|
+
data,
|
|
19
|
+
meta,
|
|
20
|
+
};
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toapi/client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "Michel Smola",
|
|
6
6
|
"email": "michel.smola@farbenmeer.de"
|
|
@@ -20,25 +20,25 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@toapi/common": "^1.
|
|
23
|
+
"@toapi/common": "^1.3.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^25.0.3",
|
|
27
|
-
"vitest": "^
|
|
27
|
+
"vitest": "^5.0.0",
|
|
28
28
|
"zod": "^4.0.17",
|
|
29
|
-
"@toapi/server": "^1.
|
|
29
|
+
"@toapi/server": "^1.3.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"typescript": "^5 || ^6.0.0",
|
|
32
|
+
"typescript": "^5 || ^6.0.0 || ^7.0.0",
|
|
33
33
|
"zod": "^4.0.17"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/
|
|
37
|
+
"url": "git+https://github.com/toapi-js/toapi.git"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc --noEmit false",
|
|
41
41
|
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
42
|
-
"test": "vitest"
|
|
42
|
+
"test": "vitest run"
|
|
43
43
|
}
|
|
44
44
|
}
|