@faable/sdk-base 1.2.0 → 1.4.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.
@@ -1,6 +1,12 @@
1
1
  import type { AuthInterface } from "./types/AuthInterface.js";
2
2
  import type { Fetcher, FetcherCreateParams } from "./fetcher/Fetcher.js";
3
3
  import type { Paginator, PaginatorOptions } from "./helpers/paginator.js";
4
+ export type RequestObserver = (info: {
5
+ method: string;
6
+ url: string;
7
+ status: number;
8
+ durationMs: number;
9
+ }) => void;
4
10
  export type ApiParams = {
5
11
  baseURL?: string;
6
12
  fetcher?: FetcherCreateParams;
@@ -22,6 +28,13 @@ export type ApiParams = {
22
28
  etagCache?: boolean | {
23
29
  maxEntries?: number;
24
30
  };
31
+ /**
32
+ * Pluggable per-response observer. Called once per completed HTTP response
33
+ * with `{ method, url, status, durationMs }`. Use it to collect metrics
34
+ * (calls per endpoint, `304` ETag-hit rate, latency, …). Errors thrown by the
35
+ * observer are swallowed so instrumentation can never break a request.
36
+ */
37
+ requestObserver?: RequestObserver;
25
38
  };
26
39
  export declare abstract class FaableApi<Params extends ApiParams = ApiParams> {
27
40
  fetcher: Fetcher;
@@ -1 +1 @@
1
- {"version":3,"file":"FaableApi.d.ts","sourceRoot":"","sources":["../src/FaableApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1E,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,IAAI,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C,CAAC;AAEF,8BAAsB,SAAS,CAAC,MAAM,SAAS,SAAS,GAAG,SAAS;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC;IAC/B,SAAS,aAAa,MAAM,CAAC,EAAE,MAAM;IAMrC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,QAAQ,MAAM,IAAI,EAAE,MAAM,KAAK,GAAG,EAAE,MAAM,GAAG,GAAG,EACtE,IAAI,EAAE,CAAC,EACP,MAAM,CAAC,EAAE,MAAM,GACd,YAAY,CAAC,CAAC,CAAC;CAInB"}
1
+ {"version":3,"file":"FaableApi.d.ts","sourceRoot":"","sources":["../src/FaableApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAQ1E,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,KAAK,IAAI,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,IAAI,CAAC,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,CAAC;AAEF,8BAAsB,SAAS,CAAC,MAAM,SAAS,SAAS,GAAG,SAAS;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC;IAC/B,SAAS,aAAa,MAAM,CAAC,EAAE,MAAM;IAMrC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,QAAQ,MAAM,IAAI,EAAE,MAAM,KAAK,GAAG,EAAE,MAAM,GAAG,GAAG,EACtE,IAAI,EAAE,CAAC,EACP,MAAM,CAAC,EAAE,MAAM,GACd,YAAY,CAAC,CAAC,CAAC;CAInB"}
@@ -1 +1 @@
1
- {"version":3,"file":"fetcher_axios.d.ts","sourceRoot":"","sources":["../../src/fetcher/fetcher_axios.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAiBjD,eAAO,MAAM,aAAa,YAAY,SAAS,KAAQ,OA0HtD,CAAC"}
1
+ {"version":3,"file":"fetcher_axios.d.ts","sourceRoot":"","sources":["../../src/fetcher/fetcher_axios.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAiBjD,eAAO,MAAM,aAAa,YAAY,SAAS,KAAQ,OAmJtD,CAAC"}
@@ -22,6 +22,9 @@ export const fetcher_axios = (params = {}) => {
22
22
  instance.interceptors.request.use(async (req) => {
23
23
  const auth_data = params.auth && (await params.auth.hook());
24
24
  req.headers.set(auth_data?.headers || {});
25
+ // Stamp a start time so the response interceptor can report latency to the
26
+ // pluggable observer.
27
+ req.__startTime = Date.now();
25
28
  return req;
26
29
  });
27
30
  // Add base interceptor
@@ -30,6 +33,21 @@ export const fetcher_axios = (params = {}) => {
30
33
  const queryparams = new URLSearchParams(res.config.params);
31
34
  console.log(`[${res.status}] ${res.config.url}${queryparams.size > 0 ? `?${queryparams.toString()}` : ""}`);
32
35
  }
36
+ // Pluggable per-response hook (metrics, etc.). Never let it break a request.
37
+ if (params.requestObserver) {
38
+ try {
39
+ const start = res.config.__startTime;
40
+ params.requestObserver({
41
+ method: (res.config.method ?? "get").toUpperCase(),
42
+ url: res.config.url ?? "",
43
+ status: res.status,
44
+ durationMs: start ? Date.now() - start : 0,
45
+ });
46
+ }
47
+ catch {
48
+ // ignore observer errors
49
+ }
50
+ }
33
51
  return res;
34
52
  }, handleErrorInterceptor);
35
53
  // Opt-in in-memory ETag cache (per fetcher instance). Keyed by url + params
@@ -54,38 +72,41 @@ export const fetcher_axios = (params = {}) => {
54
72
  etagCache.delete(etagCache.keys().next().value);
55
73
  }
56
74
  };
75
+ // Conditional GET against the cache. Replays the stored ETag as
76
+ // `If-None-Match`; a `304` serves the cached parsed body, a `200` refreshes
77
+ // it. Shared by `get` and the GET path of `request` so paginated lists
78
+ // (the paginator calls `request`) cache page-by-page too — each page is a
79
+ // distinct (url + params incl. cursor) key, and the API folds a per-resource
80
+ // generation counter into every page's ETag, so any write invalidates all
81
+ // pages (server-driven, never stale).
82
+ const etagGet = async (cfg) => {
83
+ const key = etagKey(cfg.url ?? "", cfg.params);
84
+ const cached = etagCache.get(key);
85
+ const res = await instance.request({
86
+ ...cfg,
87
+ headers: {
88
+ ...cfg.headers,
89
+ ...(cached ? { "If-None-Match": cached.etag } : {}),
90
+ },
91
+ // Accept 304 so axios resolves (instead of routing it through the
92
+ // error interceptor) and we can serve the cached body.
93
+ validateStatus: (s) => (s >= 200 && s < 300) || s === 304,
94
+ });
95
+ if (res.status === 304 && cached) {
96
+ etagTouch(key, cached); // mark as recently used
97
+ return cached.data;
98
+ }
99
+ const etag = res.headers?.etag;
100
+ if (etag) {
101
+ etagTouch(key, { etag, data: res.data });
102
+ }
103
+ return res.data;
104
+ };
57
105
  return {
58
106
  get: async (url, config) => {
59
- if (!etagEnabled) {
60
- const res = await instance.request({
61
- method: "GET",
62
- url,
63
- ...config,
64
- });
65
- return res.data;
66
- }
67
- const key = etagKey(url, config?.params);
68
- const cached = etagCache.get(key);
69
- const res = await instance.request({
70
- method: "GET",
71
- url,
72
- ...config,
73
- headers: {
74
- ...config?.headers,
75
- ...(cached ? { "If-None-Match": cached.etag } : {}),
76
- },
77
- // Accept 304 so axios resolves (instead of routing it through the
78
- // error interceptor) and we can serve the cached body.
79
- validateStatus: (s) => (s >= 200 && s < 300) || s === 304,
80
- });
81
- if (res.status === 304 && cached) {
82
- etagTouch(key, cached); // mark as recently used
83
- return cached.data;
84
- }
85
- const etag = res.headers?.etag;
86
- if (etag) {
87
- etagTouch(key, { etag, data: res.data });
88
- }
107
+ if (etagEnabled)
108
+ return etagGet({ method: "GET", url, ...config });
109
+ const res = await instance.request({ method: "GET", url, ...config });
89
110
  return res.data;
90
111
  },
91
112
  delete: async (url, config) => {
@@ -109,6 +130,12 @@ export const fetcher_axios = (params = {}) => {
109
130
  return res.data;
110
131
  },
111
132
  request: async (params) => {
133
+ // The paginator issues its page requests through `request` (method
134
+ // omitted → axios defaults to GET), so route GETs through the ETag cache
135
+ // too — otherwise lists would never be cached.
136
+ const method = (params.method ?? "GET").toString().toUpperCase();
137
+ if (etagEnabled && method === "GET")
138
+ return etagGet(params);
112
139
  const res = await instance.request(params);
113
140
  return res.data;
114
141
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/sdk-base",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "author": "Marc Pomar <marc@faable.com>",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",