View | Details | Raw Unified | Return to bug 36674
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js (-28 / +202 lines)
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;
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js (-1 / +2 lines)
Lines 21-23 export const APIClient = { Link Here
21
    preservation: new PreservationAPIClient(HttpClient),
21
    preservation: new PreservationAPIClient(HttpClient),
22
    record_sources: new RecordSourcesAPIClient(HttpClient),
22
    record_sources: new RecordSourcesAPIClient(HttpClient),
23
};
23
};
24
- 
24
25
export default APIClient;

Return to bug 36674