@fonderie/client 0.3.0 → 0.5.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.
@@ -5,16 +5,42 @@
5
5
  ## @fonderie/client
6
6
 
7
7
  ```ts
8
+ interface IClientAuthConfig {
9
+ getRefreshToken?: () => string | undefined;
10
+ onTokensChanged?: (tokens: ITokens) => void;
11
+ onAuthError?: () => void;
12
+ }
13
+
8
14
  interface IFonderieClientOptions {
9
15
  baseUrl: string;
10
16
  accessToken?: string;
11
17
  workspaceId?: string;
18
+ cache?: ICache;
19
+ auth?: IClientAuthConfig;
12
20
  }
13
21
 
14
22
  interface IRequestConfig {
15
23
  workspaceId?: string;
24
+ token?: string;
25
+ cache?: number | false;
26
+ bust?: boolean;
27
+ invalidate?: string[];
28
+ }
29
+
30
+ interface ICache {
31
+ get<T>(key: string): T | undefined;
32
+ set<T>(key: string, value: T, ttlMs: number): void;
33
+ dedupe<T>(key: string, fn: () => Promise<T>): Promise<T>;
34
+ invalidate(fragment: string): void;
35
+ clear(): void;
16
36
  }
17
37
 
38
+ interface IMemoryCacheOptions {
39
+ defaultTtlMs?: number;
40
+ }
41
+
42
+ function createMemoryCache(opts?: IMemoryCacheOptions): ICache & { defaultTtlMs: number; }
43
+
18
44
  new FonderieClient(opts: IFonderieClientOptions): FonderieClient
19
45
  .auth: AuthClient
20
46
  .billing: BillingClient
@@ -23,8 +49,9 @@ new FonderieClient(opts: IFonderieClientOptions): FonderieClient
23
49
  .webhooks: WebhooksClient
24
50
  .customers: CustomersClient
25
51
  .setAccessToken(token: string | undefined): void
52
+ .clearCache(): void
26
53
  .setWorkspaceId(workspaceId: string | undefined): void
27
- .request<T = unknown>(opts: { method: string; path: string; body?: unknown; workspaceId?: string | undefined; }): Promise<IApiResponse<T>>
54
+ .request<T = unknown>(opts: { method: string; path: string; body?: unknown; token?: string | undefined; workspaceId?: string | undefined; cache?: number | false | undefined; bust?: boolean | undefined; invalidate?: string[] | undefined; }): Promise<...>
28
55
  .get<T = unknown>(path: string, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
29
56
  .post<T = unknown>(path: string, body?: unknown, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
30
57
  .put<T = unknown>(path: string, body?: unknown, config?: IRequestConfig | undefined): Promise<IApiResponse<T>>
package/dist/index.cjs CHANGED
@@ -29,10 +29,48 @@ __export(index_exports, {
29
29
  FonderieApiError: () => FonderieApiError,
30
30
  FonderieClient: () => FonderieClient,
31
31
  WebhooksClient: () => WebhooksClient,
32
- WorkspacesClient: () => WorkspacesClient
32
+ WorkspacesClient: () => WorkspacesClient,
33
+ createMemoryCache: () => createMemoryCache
33
34
  });
34
35
  module.exports = __toCommonJS(index_exports);
35
36
 
37
+ // src/cache.ts
38
+ function createMemoryCache(opts = {}) {
39
+ const store = /* @__PURE__ */ new Map();
40
+ const inflight = /* @__PURE__ */ new Map();
41
+ const defaultTtlMs = opts.defaultTtlMs ?? 6e4;
42
+ return {
43
+ defaultTtlMs,
44
+ get(key) {
45
+ const entry = store.get(key);
46
+ if (!entry) return void 0;
47
+ if (Date.now() > entry.expiresAt) {
48
+ store.delete(key);
49
+ return void 0;
50
+ }
51
+ return entry.value;
52
+ },
53
+ set(key, value, ttlMs) {
54
+ store.set(key, { value, expiresAt: Date.now() + ttlMs });
55
+ },
56
+ dedupe(key, fn) {
57
+ const existing = inflight.get(key);
58
+ if (existing) return existing;
59
+ const req = fn().finally(() => inflight.delete(key));
60
+ inflight.set(key, req);
61
+ return req;
62
+ },
63
+ invalidate(fragment) {
64
+ for (const key of store.keys()) if (key.includes(fragment)) store.delete(key);
65
+ for (const key of inflight.keys()) if (key.includes(fragment)) inflight.delete(key);
66
+ },
67
+ clear() {
68
+ store.clear();
69
+ inflight.clear();
70
+ }
71
+ };
72
+ }
73
+
36
74
  // src/http.ts
37
75
  var FonderieApiError = class extends Error {
38
76
  constructor(reason, explanation, status, details) {
@@ -49,24 +87,52 @@ var FonderieApiError = class extends Error {
49
87
  details;
50
88
  };
51
89
  var HttpClient = class {
52
- constructor(baseUrl) {
90
+ constructor(baseUrl, deps = {}) {
53
91
  this.baseUrl = baseUrl;
92
+ this.cache = deps.cache;
93
+ this.defaultTtlMs = deps.defaultTtlMs ?? 6e4;
94
+ this.refresh = deps.refresh;
54
95
  }
55
96
  baseUrl;
97
+ cache;
98
+ defaultTtlMs;
99
+ refresh;
56
100
  async request(opts) {
57
- const headers = {
58
- "Content-Type": "application/json"
59
- };
101
+ const method = opts.method.toUpperCase();
102
+ const cache = this.cache;
103
+ if (cache && method === "GET" && opts.cache !== false) {
104
+ const key = `GET ${opts.path}::ws=${opts.workspaceId ?? ""}`;
105
+ if (!opts.bust) {
106
+ const hit = cache.get(key);
107
+ if (hit !== void 0) return hit;
108
+ }
109
+ const ttl = typeof opts.cache === "number" ? opts.cache : this.defaultTtlMs;
110
+ return cache.dedupe(key, async () => {
111
+ const data2 = await this.exec(opts);
112
+ cache.set(key, data2, ttl);
113
+ return data2;
114
+ });
115
+ }
116
+ const data = await this.exec(opts);
117
+ if (cache && method !== "GET") {
118
+ const resource = opts.path.split("?")[0]?.split("/").filter(Boolean)[0];
119
+ if (resource) cache.invalidate(`/${resource}`);
120
+ for (const fragment of opts.invalidate ?? []) cache.invalidate(fragment);
121
+ }
122
+ return data;
123
+ }
124
+ async exec(opts, retried = false) {
125
+ const headers = { "Content-Type": "application/json" };
60
126
  if (opts.token) headers["Authorization"] = `Bearer ${opts.token}`;
61
127
  if (opts.cookie) headers["Cookie"] = opts.cookie;
62
128
  if (opts.workspaceId) headers["X-Workspace-ID"] = opts.workspaceId;
63
- const fetchInit = {
64
- method: opts.method,
65
- headers,
66
- credentials: "include"
67
- };
129
+ const fetchInit = { method: opts.method, headers, credentials: "include" };
68
130
  if (opts.body !== void 0) fetchInit.body = JSON.stringify(opts.body);
69
131
  const res = await fetch(`${this.baseUrl}${opts.path}`, fetchInit);
132
+ if (res.status === 401 && this.refresh && !retried && !opts.path.startsWith("/auth/")) {
133
+ const newToken = await this.refresh();
134
+ if (newToken) return this.exec({ ...opts, token: newToken }, true);
135
+ }
70
136
  if (res.status === 204) {
71
137
  if (!res.ok) throw new FonderieApiError("unknown", res.statusText, res.status);
72
138
  return void 0;
@@ -1064,10 +1130,19 @@ var FonderieClient = class {
1064
1130
  http;
1065
1131
  tokens;
1066
1132
  workspaceId;
1133
+ cache;
1134
+ authConfig;
1135
+ refreshing = null;
1067
1136
  constructor(opts) {
1068
- this.http = new HttpClient(opts.baseUrl);
1069
1137
  this.tokens = new TokenStore(opts.accessToken);
1070
1138
  this.workspaceId = opts.workspaceId;
1139
+ this.cache = opts.cache;
1140
+ this.authConfig = opts.auth;
1141
+ this.http = new HttpClient(opts.baseUrl, {
1142
+ cache: opts.cache,
1143
+ defaultTtlMs: opts.cache?.defaultTtlMs,
1144
+ refresh: opts.auth ? () => this.doRefresh() : void 0
1145
+ });
1071
1146
  this.auth = new AuthClient(this.http, this.tokens);
1072
1147
  this.billing = new BillingClient(this.http, this.tokens);
1073
1148
  this.workspaces = new WorkspacesClient(this.http, this.tokens);
@@ -1075,10 +1150,42 @@ var FonderieClient = class {
1075
1150
  this.webhooks = new WebhooksClient(this.http, this.tokens);
1076
1151
  this.customers = new CustomersClient(this.http, this.tokens);
1077
1152
  }
1153
+ // Single-flight refresh: POST /auth/refresh with the app-supplied refresh
1154
+ // token, store the new access token, notify the app, return it for the retry.
1155
+ doRefresh() {
1156
+ if (this.refreshing) return this.refreshing;
1157
+ this.refreshing = (async () => {
1158
+ const refreshToken = this.authConfig?.getRefreshToken?.();
1159
+ if (!refreshToken) {
1160
+ this.authConfig?.onAuthError?.();
1161
+ return void 0;
1162
+ }
1163
+ try {
1164
+ const { result } = await this.auth.refreshTokens(refreshToken);
1165
+ const tokens = result.tokens;
1166
+ this.tokens.set(tokens.access);
1167
+ this.authConfig?.onTokensChanged?.(tokens);
1168
+ return tokens.access;
1169
+ } catch {
1170
+ this.tokens.set(void 0);
1171
+ this.authConfig?.onAuthError?.();
1172
+ return void 0;
1173
+ } finally {
1174
+ this.refreshing = null;
1175
+ }
1176
+ })();
1177
+ return this.refreshing;
1178
+ }
1078
1179
  // The JWT used to authenticate every request — the typed modules and the
1079
- // generic transport share one token store. Pass undefined to sign out.
1180
+ // generic transport share one token store. Passing undefined signs out and
1181
+ // clears the cache (so no session's data survives a logout).
1080
1182
  setAccessToken(token) {
1081
1183
  this.tokens.set(token);
1184
+ if (!token) this.clearCache();
1185
+ }
1186
+ // Drop all cached responses (e.g. on switching accounts).
1187
+ clearCache() {
1188
+ this.cache?.clear();
1082
1189
  }
1083
1190
  // Default X-Workspace-ID for the generic transport, also propagated to the
1084
1191
  // workspace-scoped modules so one call configures the whole client.
@@ -1098,24 +1205,34 @@ var FonderieClient = class {
1098
1205
  method: opts.method,
1099
1206
  path: opts.path,
1100
1207
  body: opts.body,
1101
- token: this.tokens.get(),
1102
- workspaceId: opts.workspaceId ?? this.workspaceId
1208
+ token: opts.token ?? this.tokens.get(),
1209
+ workspaceId: opts.workspaceId ?? this.workspaceId,
1210
+ cache: opts.cache,
1211
+ bust: opts.bust,
1212
+ invalidate: opts.invalidate
1103
1213
  });
1104
1214
  }
1105
1215
  get(path, config) {
1106
- return this.request({ method: "GET", path, workspaceId: config?.workspaceId });
1216
+ return this.request({
1217
+ method: "GET",
1218
+ path,
1219
+ token: config?.token,
1220
+ workspaceId: config?.workspaceId,
1221
+ cache: config?.cache,
1222
+ bust: config?.bust
1223
+ });
1107
1224
  }
1108
1225
  post(path, body, config) {
1109
- return this.request({ method: "POST", path, body, workspaceId: config?.workspaceId });
1226
+ return this.request({ method: "POST", path, body, token: config?.token, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
1110
1227
  }
1111
1228
  put(path, body, config) {
1112
- return this.request({ method: "PUT", path, body, workspaceId: config?.workspaceId });
1229
+ return this.request({ method: "PUT", path, body, token: config?.token, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
1113
1230
  }
1114
1231
  patch(path, body, config) {
1115
- return this.request({ method: "PATCH", path, body, workspaceId: config?.workspaceId });
1232
+ return this.request({ method: "PATCH", path, body, token: config?.token, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
1116
1233
  }
1117
1234
  delete(path, config) {
1118
- return this.request({ method: "DELETE", path, workspaceId: config?.workspaceId });
1235
+ return this.request({ method: "DELETE", path, token: config?.token, workspaceId: config?.workspaceId, invalidate: config?.invalidate });
1119
1236
  }
1120
1237
  };
1121
1238
 
@@ -1299,6 +1416,7 @@ var CourierAdminClient = class {
1299
1416
  FonderieApiError,
1300
1417
  FonderieClient,
1301
1418
  WebhooksClient,
1302
- WorkspacesClient
1419
+ WorkspacesClient,
1420
+ createMemoryCache
1303
1421
  });
1304
1422
  //# sourceMappingURL=index.cjs.map