Lines 1-33
Link Here
|
1 |
import HttpClient from "./http-client.js"; |
1 |
import HttpClient from "./http-client.js"; |
2 |
|
2 |
|
3 |
import ArticleRequestAPIClient from "./article-request-api-client.js"; |
3 |
/** |
4 |
import AVAPIClient from "./authorised-values-api-client.js"; |
4 |
* @template {object} T |
5 |
import CataloguingAPIClient from "./cataloguing-api-client.js"; |
5 |
* @typedef {new (...args: unknown[]) => T} ClientConstructor |
6 |
import CirculationAPIClient from "./circulation-api-client.js"; |
6 |
*/ |
7 |
import ClubAPIClient from "./club-api-client.js"; |
7 |
|
8 |
import CoverImageAPIClient from "./cover-image-api-client.js"; |
8 |
/** |
9 |
import LocalizationAPIClient from "./localization-api-client.js"; |
9 |
* @template {object} T |
10 |
import PatronAPIClient from "./patron-api-client.js"; |
10 |
* @typedef {ClientConstructor<T> | { default: ClientConstructor<T> }} ClientModule |
11 |
import PatronListAPIClient from "./patron-list-api-client.js"; |
11 |
*/ |
12 |
import RecallAPIClient from "./recall-api-client.js"; |
12 |
|
13 |
import SysprefAPIClient from "./system-preferences-api-client.js"; |
13 |
/** |
14 |
import TicketAPIClient from "./ticket-api-client.js"; |
14 |
* Determines whether a value can safely be treated as an object for property |
15 |
import AcquisitionAPIClient from "./acquisition-api-client.js"; |
15 |
* access (including functions, which are callable objects in JS). |
16 |
import DefaultAPIClient from "./default-api-client.js"; |
16 |
* |
|
|
17 |
* @param {unknown} value |
18 |
* @returns {value is object | Function} |
19 |
*/ |
20 |
const isObjectLike = value => |
21 |
(typeof value === "object" && value !== null) || |
22 |
typeof value === "function"; |
23 |
|
24 |
/** |
25 |
* Lazily instantiates an API client module the first time a consumer actually |
26 |
* uses it. Callers still interact with `APIClient.foo` synchronously, but under |
27 |
* the hood a proxy defers the dynamic `import()` until a method is invoked or a |
28 |
* promise chain is attached. This keeps existing call sites unchanged while |
29 |
* preventing every specialised client from being fetched on initial page load. |
30 |
* |
31 |
* @template {object} T |
32 |
* @param {() => Promise<ClientModule<T> | ClientConstructor<T>>} loader dynamic importer for the client module |
33 |
* @returns {T} proxy exposing the API client interface with lazy loading |
34 |
*/ |
35 |
const createClientProxy = loader => { |
36 |
/** @type {Promise<T> | undefined} */ |
37 |
let instancePromise; |
38 |
|
39 |
/** |
40 |
* Extracts the client constructor from a dynamic import namespace. |
41 |
* |
42 |
* @param {ClientModule<T> | ClientConstructor<T>} namespace |
43 |
* @returns {ClientConstructor<T>} |
44 |
*/ |
45 |
const resolveClientConstructor = namespace => { |
46 |
if (typeof namespace === "function") { |
47 |
return /** @type {ClientConstructor<T>} */ (namespace); |
48 |
} |
49 |
|
50 |
if (isObjectLike(namespace)) { |
51 |
const maybeDefault = Reflect.get( |
52 |
/** @type {object} */ (namespace), |
53 |
"default" |
54 |
); |
55 |
if (typeof maybeDefault === "function") { |
56 |
return /** @type {ClientConstructor<T>} */ (maybeDefault); |
57 |
} |
58 |
} |
59 |
throw new TypeError("API client module did not export a constructor"); |
60 |
}; |
61 |
|
62 |
/** |
63 |
* Resolves (or re-resolves after failure) the underlying client instance. |
64 |
* |
65 |
* @returns {Promise<T>} promise resolving to the concrete client |
66 |
*/ |
67 |
const loadInstance = () => { |
68 |
if (!instancePromise) { |
69 |
instancePromise = loader() |
70 |
.then(resolveClientConstructor) |
71 |
.then(Client => new Client(HttpClient)) |
72 |
.catch(error => { |
73 |
instancePromise = undefined; |
74 |
throw error; |
75 |
}); |
76 |
} |
77 |
return instancePromise; |
78 |
}; |
79 |
|
80 |
/** |
81 |
* Creates a proxy layer that defers property access and function calls |
82 |
* until the client instance is available while keeping the existing call |
83 |
* structure intact (including promise chaining support). |
84 |
* |
85 |
* @param {(client: T) => unknown} accessor resolver for the current target |
86 |
* @param {(client: T) => unknown} [parentAccessor=accessor] context resolver |
87 |
* @returns {unknown} proxy forwarding operations to the resolved target |
88 |
*/ |
89 |
const createProxy = (accessor, parentAccessor = accessor) => { |
90 |
/** |
91 |
* Forwards promise chaining when consumers treat the proxy like a promise. |
92 |
* |
93 |
* @param {(value: unknown) => unknown} onFulfilled |
94 |
* @param {(reason: unknown) => unknown} [onRejected] |
95 |
* @returns {Promise<unknown>} |
96 |
*/ |
97 |
const handleThen = (onFulfilled, onRejected) => |
98 |
loadInstance() |
99 |
.then(client => accessor(client)) |
100 |
.then(onFulfilled, onRejected); |
101 |
|
102 |
/** |
103 |
* Propagates errors when consumers attach a catch handler to the proxy. |
104 |
* |
105 |
* @param {(reason: unknown) => unknown} onRejected |
106 |
* @returns {Promise<unknown>} |
107 |
*/ |
108 |
const handleCatch = onRejected => |
109 |
loadInstance() |
110 |
.then(client => accessor(client)) |
111 |
.catch(onRejected); |
112 |
|
113 |
/** |
114 |
* Executes finally handlers while preserving the resolved value chain. |
115 |
* |
116 |
* @param {() => unknown} onFinally |
117 |
* @returns {Promise<unknown>} |
118 |
*/ |
119 |
const handleFinally = onFinally => |
120 |
loadInstance() |
121 |
.then(client => accessor(client)) |
122 |
.finally(onFinally); |
123 |
|
124 |
/** |
125 |
* Returns a proxy that represents a nested property on the client. |
126 |
* |
127 |
* @param {PropertyKey} prop |
128 |
* @returns {unknown} |
129 |
*/ |
130 |
const forwardProperty = prop => |
131 |
createProxy(client => { |
132 |
const target = accessor(client); |
133 |
if (!isObjectLike(target)) { |
134 |
return undefined; |
135 |
} |
136 |
return Reflect.get(/** @type {object} */ (target), prop); |
137 |
}, accessor); |
138 |
|
139 |
/** |
140 |
* Invokes a method on the resolved client while keeping the original |
141 |
* `this` binding semantics. |
142 |
* |
143 |
* @param {unknown} thisArg |
144 |
* @param {unknown[]} argArray |
145 |
* @returns {Promise<unknown>} |
146 |
*/ |
147 |
const invokeTarget = (thisArg, argArray) => |
148 |
loadInstance().then(client => { |
149 |
const target = accessor(client); |
150 |
if (typeof target !== "function") { |
151 |
throw new TypeError("API client property is not callable"); |
152 |
} |
153 |
const context = parentAccessor |
154 |
? parentAccessor(client) |
155 |
: (thisArg ?? undefined); |
156 |
return target.apply(context, argArray); |
157 |
}); |
158 |
|
159 |
return new Proxy(function () {}, { |
160 |
get(_, prop) { |
161 |
if (prop === "then") { |
162 |
return handleThen; |
163 |
} |
164 |
if (prop === "catch") { |
165 |
return handleCatch; |
166 |
} |
167 |
if (prop === "finally") { |
168 |
return handleFinally; |
169 |
} |
170 |
|
171 |
return forwardProperty(prop); |
172 |
}, |
173 |
apply(_, thisArg, args) { |
174 |
return invokeTarget(thisArg, /** @type {unknown[]} */ (args)); |
175 |
}, |
176 |
}); |
177 |
}; |
178 |
|
179 |
return /** @type {T} */ (createProxy(client => client)); |
180 |
}; |
17 |
|
181 |
|
18 |
export const APIClient = { |
182 |
export const APIClient = { |
19 |
article_request: new ArticleRequestAPIClient(HttpClient), |
183 |
article_request: createClientProxy( |
20 |
authorised_values: new AVAPIClient(HttpClient), |
184 |
() => import("./article-request-api-client.js") |
21 |
acquisition: new AcquisitionAPIClient(HttpClient), |
185 |
), |
22 |
cataloguing: new CataloguingAPIClient(HttpClient), |
186 |
authorised_values: createClientProxy( |
23 |
circulation: new CirculationAPIClient(HttpClient), |
187 |
() => import("./authorised-values-api-client.js") |
24 |
club: new ClubAPIClient(HttpClient), |
188 |
), |
25 |
cover_image: new CoverImageAPIClient(HttpClient), |
189 |
acquisition: createClientProxy(() => import("./acquisition-api-client.js")), |
26 |
localization: new LocalizationAPIClient(HttpClient), |
190 |
cataloguing: createClientProxy(() => import("./cataloguing-api-client.js")), |
27 |
patron: new PatronAPIClient(HttpClient), |
191 |
circulation: createClientProxy(() => import("./circulation-api-client.js")), |
28 |
patron_list: new PatronListAPIClient(HttpClient), |
192 |
club: createClientProxy(() => import("./club-api-client.js")), |
29 |
recall: new RecallAPIClient(HttpClient), |
193 |
cover_image: createClientProxy(() => import("./cover-image-api-client.js")), |
30 |
sysprefs: new SysprefAPIClient(HttpClient), |
194 |
localization: createClientProxy( |
31 |
ticket: new TicketAPIClient(HttpClient), |
195 |
() => import("./localization-api-client.js") |
32 |
default: new DefaultAPIClient(HttpClient), |
196 |
), |
|
|
197 |
patron: createClientProxy(() => import("./patron-api-client.js")), |
198 |
patron_list: createClientProxy(() => import("./patron-list-api-client.js")), |
199 |
recall: createClientProxy(() => import("./recall-api-client.js")), |
200 |
sysprefs: createClientProxy( |
201 |
() => import("./system-preferences-api-client.js") |
202 |
), |
203 |
ticket: createClientProxy(() => import("./ticket-api-client.js")), |
204 |
default: createClientProxy(() => import("./default-api-client.js")), |
33 |
}; |
205 |
}; |
|
|
206 |
|
207 |
export default APIClient; |