|
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; |