@mionjs/client 0.8.6 → 0.8.8
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.
- package/.dist/cjs/index.d.ts +1 -0
- package/.dist/cjs/index.d.ts.map +1 -0
- package/.dist/cjs/src/client.d.ts +1 -0
- package/.dist/cjs/src/client.d.ts.map +1 -0
- package/.dist/cjs/src/constants.d.ts +1 -0
- package/.dist/cjs/src/constants.d.ts.map +1 -0
- package/.dist/cjs/src/lib/clientMethodsMetadata.d.ts +1 -0
- package/.dist/cjs/src/lib/clientMethodsMetadata.d.ts.map +1 -0
- package/.dist/cjs/src/lib/fetchRemoteMethodsMetadata.d.ts +1 -0
- package/.dist/cjs/src/lib/fetchRemoteMethodsMetadata.d.ts.map +1 -0
- package/.dist/cjs/src/lib/handlersRegistry.d.ts +1 -0
- package/.dist/cjs/src/lib/handlersRegistry.d.ts.map +1 -0
- package/.dist/cjs/src/lib/serializer.d.ts +1 -0
- package/.dist/cjs/src/lib/serializer.d.ts.map +1 -0
- package/.dist/cjs/src/lib/storage.d.ts +1 -0
- package/.dist/cjs/src/lib/storage.d.ts.map +1 -0
- package/.dist/cjs/src/lib/testUtils.d.ts +1 -0
- package/.dist/cjs/src/lib/testUtils.d.ts.map +1 -0
- package/.dist/cjs/src/lib/typedEvent.d.ts +1 -0
- package/.dist/cjs/src/lib/typedEvent.d.ts.map +1 -0
- package/.dist/cjs/src/lib/validation.d.ts +1 -0
- package/.dist/cjs/src/lib/validation.d.ts.map +1 -0
- package/.dist/cjs/src/request.d.ts +1 -0
- package/.dist/cjs/src/request.d.ts.map +1 -0
- package/.dist/cjs/src/routesFlow.d.ts +1 -0
- package/.dist/cjs/src/routesFlow.d.ts.map +1 -0
- package/.dist/cjs/src/subRequest.d.ts +1 -0
- package/.dist/cjs/src/subRequest.d.ts.map +1 -0
- package/.dist/cjs/src/types.d.ts +1 -0
- package/.dist/cjs/src/types.d.ts.map +1 -0
- package/.dist/esm/index.d.ts +1 -0
- package/.dist/esm/index.d.ts.map +1 -0
- package/.dist/esm/src/client.d.ts +1 -0
- package/.dist/esm/src/client.d.ts.map +1 -0
- package/.dist/esm/src/constants.d.ts +1 -0
- package/.dist/esm/src/constants.d.ts.map +1 -0
- package/.dist/esm/src/lib/clientMethodsMetadata.d.ts +1 -0
- package/.dist/esm/src/lib/clientMethodsMetadata.d.ts.map +1 -0
- package/.dist/esm/src/lib/fetchRemoteMethodsMetadata.d.ts +1 -0
- package/.dist/esm/src/lib/fetchRemoteMethodsMetadata.d.ts.map +1 -0
- package/.dist/esm/src/lib/handlersRegistry.d.ts +1 -0
- package/.dist/esm/src/lib/handlersRegistry.d.ts.map +1 -0
- package/.dist/esm/src/lib/serializer.d.ts +1 -0
- package/.dist/esm/src/lib/serializer.d.ts.map +1 -0
- package/.dist/esm/src/lib/storage.d.ts +1 -0
- package/.dist/esm/src/lib/storage.d.ts.map +1 -0
- package/.dist/esm/src/lib/testUtils.d.ts +1 -0
- package/.dist/esm/src/lib/testUtils.d.ts.map +1 -0
- package/.dist/esm/src/lib/typedEvent.d.ts +1 -0
- package/.dist/esm/src/lib/typedEvent.d.ts.map +1 -0
- package/.dist/esm/src/lib/validation.d.ts +1 -0
- package/.dist/esm/src/lib/validation.d.ts.map +1 -0
- package/.dist/esm/src/request.d.ts +1 -0
- package/.dist/esm/src/request.d.ts.map +1 -0
- package/.dist/esm/src/routesFlow.d.ts +1 -0
- package/.dist/esm/src/routesFlow.d.ts.map +1 -0
- package/.dist/esm/src/subRequest.d.ts +1 -0
- package/.dist/esm/src/subRequest.d.ts.map +1 -0
- package/.dist/esm/src/types.d.ts +1 -0
- package/.dist/esm/src/types.d.ts.map +1 -0
- package/index.ts +19 -0
- package/package.json +12 -8
- package/src/client.ts +290 -0
- package/src/constants.ts +37 -0
- package/src/lib/clientMethodsMetadata.ts +187 -0
- package/src/lib/fetchRemoteMethodsMetadata.ts +44 -0
- package/src/lib/handlersRegistry.ts +101 -0
- package/src/lib/serializer.ts +245 -0
- package/src/lib/storage.ts +57 -0
- package/src/lib/testUtils.ts +16 -0
- package/src/lib/typedEvent.ts +57 -0
- package/src/lib/validation.ts +72 -0
- package/src/request.ts +563 -0
- package/src/routesFlow.ts +101 -0
- package/src/subRequest.ts +111 -0
- package/src/types.ts +237 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* ########
|
|
2
|
+
* 2023 mion
|
|
3
|
+
* Author: Ma-jerez
|
|
4
|
+
* License: MIT
|
|
5
|
+
* The software is provided "as is", without warranty of any kind.
|
|
6
|
+
* ######## */
|
|
7
|
+
|
|
8
|
+
import {DEFAULT_PREFILL_OPTIONS} from './constants.ts';
|
|
9
|
+
import {
|
|
10
|
+
ClientOptions,
|
|
11
|
+
MiddlewareSubRequest,
|
|
12
|
+
InitClientOptions,
|
|
13
|
+
RouteSubRequest,
|
|
14
|
+
SubRequest,
|
|
15
|
+
RequestErrors,
|
|
16
|
+
ClientRoutes,
|
|
17
|
+
ClientMiddleFns,
|
|
18
|
+
Result,
|
|
19
|
+
WorkflowResult,
|
|
20
|
+
} from './types.ts';
|
|
21
|
+
import type {RemoteApi} from '@mionjs/router';
|
|
22
|
+
import {registerErrorDeserializers} from '@mionjs/core';
|
|
23
|
+
import {getRouterItemId} from '@mionjs/core';
|
|
24
|
+
import {MionClientRequest} from './request.ts';
|
|
25
|
+
import type {RunTypeError} from '@mionjs/core';
|
|
26
|
+
import {HandlersRegistry} from './lib/handlersRegistry.ts';
|
|
27
|
+
import {MionSubRequest, findSubRequestError} from './subRequest.ts';
|
|
28
|
+
|
|
29
|
+
export function initClient<RM extends RemoteApi>(
|
|
30
|
+
options: InitClientOptions
|
|
31
|
+
): {client: MionClient; routes: ClientRoutes<RM>; middleFns: ClientMiddleFns<RM>} {
|
|
32
|
+
registerErrorDeserializers();
|
|
33
|
+
const clientOptions = {
|
|
34
|
+
...DEFAULT_PREFILL_OPTIONS,
|
|
35
|
+
...options,
|
|
36
|
+
};
|
|
37
|
+
const client = new MionClient(clientOptions);
|
|
38
|
+
const rootProxy = new MethodProxy([], client, clientOptions);
|
|
39
|
+
return {
|
|
40
|
+
client,
|
|
41
|
+
routes: rootProxy.proxy as ClientRoutes<RM>,
|
|
42
|
+
middleFns: rootProxy.proxy as ClientMiddleFns<RM>,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class MionClient {
|
|
47
|
+
/** Shared registry for persistent middleFn error handlers */
|
|
48
|
+
readonly handlersRegistry = new HandlersRegistry();
|
|
49
|
+
|
|
50
|
+
/** In-memory cache for prefilled middleFn subrequests (keyed by baseURL:middleFnId) */
|
|
51
|
+
readonly prefilledMiddleFnsCache = new Map<string, SubRequest<any>>();
|
|
52
|
+
|
|
53
|
+
/** Tracks in-flight prefill operations to avoid race conditions */
|
|
54
|
+
private pendingPrefills: Promise<void>[] = [];
|
|
55
|
+
|
|
56
|
+
private globalAbortController = new AbortController();
|
|
57
|
+
private get globalSignal(): AbortSignal {
|
|
58
|
+
return this.globalAbortController.signal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
constructor(private clientOptions: ClientOptions) {}
|
|
62
|
+
|
|
63
|
+
/** Aborts all in-flight requests. New requests after this call work normally. */
|
|
64
|
+
abort(): void {
|
|
65
|
+
this.globalAbortController.abort();
|
|
66
|
+
this.globalAbortController = new AbortController();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Composes a single AbortSignal from global, per-request, and timeout signals */
|
|
70
|
+
private composeSignal(signal?: AbortSignal, timeout?: number): AbortSignal {
|
|
71
|
+
const signals: AbortSignal[] = [this.globalSignal];
|
|
72
|
+
if (signal) signals.push(signal);
|
|
73
|
+
const effectiveTimeout = timeout ?? this.clientOptions.timeout;
|
|
74
|
+
if (effectiveTimeout !== undefined) signals.push(AbortSignal.timeout(effectiveTimeout));
|
|
75
|
+
return AbortSignal.any(signals);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Executes a route call with optional workflow routes and middleFns */
|
|
79
|
+
execute(
|
|
80
|
+
routeSubRequest?: RouteSubRequest<any>,
|
|
81
|
+
workflowSubRequests?: RouteSubRequest<any>[],
|
|
82
|
+
middleFnsRecord?: Record<string, MiddlewareSubRequest<any>>,
|
|
83
|
+
signal?: AbortSignal,
|
|
84
|
+
timeout?: number
|
|
85
|
+
): Promise<any> {
|
|
86
|
+
return this.executeRequest(routeSubRequest, workflowSubRequests, middleFnsRecord, signal, timeout);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async executeRequest<Routes extends RouteSubRequest<any>[], H extends Record<string, MiddlewareSubRequest<any>>>(
|
|
90
|
+
routeSubRequest: RouteSubRequest<any> | undefined,
|
|
91
|
+
workflowSubRequests: Routes | undefined,
|
|
92
|
+
middleFnsRecord: H | undefined,
|
|
93
|
+
signal?: AbortSignal,
|
|
94
|
+
timeout?: number
|
|
95
|
+
): Promise<any> {
|
|
96
|
+
// Capture the signal before any async work so abort() during prefill await is respected
|
|
97
|
+
const composedSignal = this.composeSignal(signal, timeout);
|
|
98
|
+
|
|
99
|
+
// Wait for any in-flight prefill operations to complete before executing the request
|
|
100
|
+
if (this.pendingPrefills.length > 0) await Promise.allSettled([...this.pendingPrefills]);
|
|
101
|
+
|
|
102
|
+
const middleFnSubRequests = middleFnsRecord ? Object.values(middleFnsRecord) : [];
|
|
103
|
+
const request = new MionClientRequest(
|
|
104
|
+
this.clientOptions,
|
|
105
|
+
this.prefilledMiddleFnsCache,
|
|
106
|
+
routeSubRequest,
|
|
107
|
+
middleFnSubRequests,
|
|
108
|
+
workflowSubRequests,
|
|
109
|
+
composedSignal
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await request.call();
|
|
114
|
+
const routeIds = this.getRouteIds(routeSubRequest, workflowSubRequests);
|
|
115
|
+
const allMiddleFns = this.getAllMiddleFnsFromRequest(request, routeIds);
|
|
116
|
+
this.processMiddleFnsResponses(allMiddleFns, undefined);
|
|
117
|
+
return this.buildResult(routeSubRequest, workflowSubRequests, middleFnsRecord || allMiddleFns, undefined);
|
|
118
|
+
} catch (errors: any) {
|
|
119
|
+
const routeIds = this.getRouteIds(routeSubRequest, workflowSubRequests);
|
|
120
|
+
const allMiddleFns = this.getAllMiddleFnsFromRequest(request, routeIds);
|
|
121
|
+
this.processMiddleFnsResponses(allMiddleFns, errors);
|
|
122
|
+
return this.buildResult(routeSubRequest, workflowSubRequests, middleFnsRecord || allMiddleFns, errors);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Get route IDs from single route or routesFlow routes */
|
|
127
|
+
private getRouteIds(
|
|
128
|
+
routeSubRequest: RouteSubRequest<any> | undefined,
|
|
129
|
+
workflowSubRequests: RouteSubRequest<any>[] | undefined
|
|
130
|
+
): Set<string> {
|
|
131
|
+
const routeIds = new Set<string>();
|
|
132
|
+
if (routeSubRequest) routeIds.add(routeSubRequest.id);
|
|
133
|
+
if (workflowSubRequests) workflowSubRequests.forEach((sr) => routeIds.add(sr.id));
|
|
134
|
+
return routeIds;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Get all middleFns from the request's subRequestList, excluding the route(s) */
|
|
138
|
+
private getAllMiddleFnsFromRequest(
|
|
139
|
+
request: MionClientRequest<any, any>,
|
|
140
|
+
excludedIds: Set<string>
|
|
141
|
+
): MiddlewareSubRequest<any>[] {
|
|
142
|
+
return Object.entries(request.subRequestList)
|
|
143
|
+
.filter(([id]) => !excludedIds.has(id))
|
|
144
|
+
.map(([, subRequest]) => subRequest as MiddlewareSubRequest<any>);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Process all middleFn responses - call success or error handlers for each middleFn individually */
|
|
148
|
+
private processMiddleFnsResponses(middleFnSubRequests: MiddlewareSubRequest<any>[], errors: RequestErrors | undefined): void {
|
|
149
|
+
for (const middleFn of middleFnSubRequests) {
|
|
150
|
+
const middleFnError = errors?.get(middleFn.id);
|
|
151
|
+
if (middleFnError) {
|
|
152
|
+
this.handlersRegistry.executeHandler(middleFn.id, middleFnError);
|
|
153
|
+
} else if (middleFn.resolvedValue !== undefined) {
|
|
154
|
+
this.handlersRegistry.executeSuccessHandler(middleFn.id, middleFn.resolvedValue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Build the result 4-tuple from the request results. middleFns can be a named record or an array of subrequests */
|
|
160
|
+
private buildResult<Routes extends RouteSubRequest<any>[], H extends Record<string, MiddlewareSubRequest<any>>>(
|
|
161
|
+
routeSubRequest: RouteSubRequest<any> | undefined,
|
|
162
|
+
workflowSubRequests: Routes | undefined,
|
|
163
|
+
middleFns: H | MiddlewareSubRequest<any>[],
|
|
164
|
+
errors: RequestErrors | undefined
|
|
165
|
+
): WorkflowResult<Routes, H> | Result<any, any> {
|
|
166
|
+
const middleFnsResults = {} as Record<string, any>;
|
|
167
|
+
const middleFnsErrors = {} as Record<string, any>;
|
|
168
|
+
const processedIds = new Set<string>();
|
|
169
|
+
|
|
170
|
+
let routeResultPart: any;
|
|
171
|
+
let routeErrorPart: any;
|
|
172
|
+
|
|
173
|
+
if (routeSubRequest) {
|
|
174
|
+
processedIds.add(routeSubRequest.id);
|
|
175
|
+
const routeError =
|
|
176
|
+
errors?.get(routeSubRequest.id) || (errors ? findSubRequestError(routeSubRequest, errors) : undefined);
|
|
177
|
+
routeResultPart = routeError ? undefined : routeSubRequest.resolvedValue;
|
|
178
|
+
routeErrorPart = routeError;
|
|
179
|
+
} else if (workflowSubRequests) {
|
|
180
|
+
const routeResults: (any | undefined)[] = [];
|
|
181
|
+
const routeErrors: (any | undefined)[] = [];
|
|
182
|
+
for (const routeSubRequest of workflowSubRequests) {
|
|
183
|
+
processedIds.add(routeSubRequest.id);
|
|
184
|
+
const routeError = errors?.get(routeSubRequest.id);
|
|
185
|
+
if (routeError) {
|
|
186
|
+
routeResults.push(undefined);
|
|
187
|
+
routeErrors.push(routeError);
|
|
188
|
+
} else {
|
|
189
|
+
routeResults.push(routeSubRequest.resolvedValue);
|
|
190
|
+
routeErrors.push(undefined);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const hasAnyResult = routeResults.some((r) => r !== undefined);
|
|
194
|
+
const hasAnyError = routeErrors.some((e) => e !== undefined);
|
|
195
|
+
routeResultPart = hasAnyResult ? routeResults : undefined;
|
|
196
|
+
routeErrorPart = hasAnyError ? routeErrors : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// middleFns can be a named record (from callWithMiddleFns/routesFlow) or an array (from executeCall)
|
|
200
|
+
if (Array.isArray(middleFns)) {
|
|
201
|
+
// Array of subrequests - use IDs as keys
|
|
202
|
+
for (const middleFn of middleFns) {
|
|
203
|
+
processedIds.add(middleFn.id);
|
|
204
|
+
const middleFnError = errors?.get(middleFn.id);
|
|
205
|
+
if (middleFnError) {
|
|
206
|
+
middleFnsErrors[middleFn.id] = middleFnError;
|
|
207
|
+
} else if (middleFn.resolvedValue !== undefined) {
|
|
208
|
+
middleFnsResults[middleFn.id] = middleFn.resolvedValue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
// Named record - use names as keys
|
|
213
|
+
for (const [name, middleFn] of Object.entries(middleFns)) {
|
|
214
|
+
processedIds.add(middleFn.id);
|
|
215
|
+
const middleFnError = errors?.get(middleFn.id);
|
|
216
|
+
if (middleFnError) {
|
|
217
|
+
middleFnsErrors[name] = middleFnError;
|
|
218
|
+
} else if (middleFn.resolvedValue !== undefined) {
|
|
219
|
+
middleFnsResults[name] = middleFn.resolvedValue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (errors) {
|
|
225
|
+
for (const [id, error] of errors) {
|
|
226
|
+
if (!processedIds.has(id)) {
|
|
227
|
+
middleFnsErrors[id] = error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return [routeResultPart, routeErrorPart, middleFnsResults, middleFnsErrors] as any;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
typeErrors<List extends SubRequest<any>[]>(...subRequest: List): Promise<RunTypeError[]> {
|
|
236
|
+
const request = new MionClientRequest(this.clientOptions, this.prefilledMiddleFnsCache);
|
|
237
|
+
return request.validateParams(subRequest);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
prefill<List extends MiddlewareSubRequest<any>[]>(...subRequest: List): Promise<void> {
|
|
241
|
+
const request = new MionClientRequest(this.clientOptions, this.prefilledMiddleFnsCache);
|
|
242
|
+
const promise = request.prefill(subRequest);
|
|
243
|
+
this.pendingPrefills.push(promise);
|
|
244
|
+
promise.finally(() => {
|
|
245
|
+
const index = this.pendingPrefills.indexOf(promise);
|
|
246
|
+
if (index >= 0) this.pendingPrefills.splice(index, 1);
|
|
247
|
+
});
|
|
248
|
+
return promise;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
removePrefill<List extends MiddlewareSubRequest<any>[]>(...subRequest: List): Promise<void> {
|
|
252
|
+
const request = new MionClientRequest(this.clientOptions, this.prefilledMiddleFnsCache);
|
|
253
|
+
return request.removePrefill(subRequest);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Clear all error handlers from the registry and abort in-flight requests */
|
|
257
|
+
destroy(): void {
|
|
258
|
+
this.abort();
|
|
259
|
+
this.handlersRegistry.clearAll();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
class MethodProxy {
|
|
264
|
+
propsProxies: Record<string, MethodProxy> = {};
|
|
265
|
+
handler = {
|
|
266
|
+
apply: (_target: any, _thisArg: any, argArray?: any): RouteSubRequest<any> & MiddlewareSubRequest<any> => {
|
|
267
|
+
const handlerId = getRouterItemId(this.parentProps);
|
|
268
|
+
return new MionSubRequest(this.parentProps, handlerId, argArray, this.client);
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
get: (_target: any, prop: string): typeof Proxy => {
|
|
272
|
+
const existing = this.propsProxies[prop];
|
|
273
|
+
if (existing) return existing.proxy;
|
|
274
|
+
const newMethodProxy = new MethodProxy([...this.parentProps, prop], this.client, this.clientOptions);
|
|
275
|
+
this.propsProxies[prop] = newMethodProxy;
|
|
276
|
+
return newMethodProxy.proxy;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
proxy: typeof Proxy;
|
|
281
|
+
|
|
282
|
+
constructor(
|
|
283
|
+
public parentProps: string[],
|
|
284
|
+
private client: MionClient,
|
|
285
|
+
private clientOptions: ClientOptions
|
|
286
|
+
) {
|
|
287
|
+
const target = () => null;
|
|
288
|
+
this.proxy = new Proxy(target, this.handler);
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* ########
|
|
2
|
+
* 2022 mion
|
|
3
|
+
* Author: Ma-jerez
|
|
4
|
+
* License: MIT
|
|
5
|
+
* The software is provided "as is", without warranty of any kind.
|
|
6
|
+
* ######## */
|
|
7
|
+
|
|
8
|
+
import {ClientOptions} from './types.ts';
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_PREFILL_OPTIONS: ClientOptions = {
|
|
11
|
+
baseURL: '',
|
|
12
|
+
fetchOptions: {
|
|
13
|
+
method: 'PUT',
|
|
14
|
+
headers: {'Content-Type': 'application/json'},
|
|
15
|
+
},
|
|
16
|
+
/** Prefix for all routes, i.e: api/v1 */
|
|
17
|
+
basePath: '',
|
|
18
|
+
/** Suffix for all routes, i.e: .json */
|
|
19
|
+
suffix: '',
|
|
20
|
+
/** Enables automatic parameter validation */
|
|
21
|
+
validateParams: true,
|
|
22
|
+
/** Set true to automatically generate and id for every error */
|
|
23
|
+
autoGenerateErrorId: false,
|
|
24
|
+
/** Default serializer mode - stringifyJson as default native serializer */
|
|
25
|
+
serializer: 'stringifyJson',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Maximum safe URL length for GET requests with ?data= query param */
|
|
29
|
+
export const MAX_GET_URL_LENGTH = 4096;
|
|
30
|
+
|
|
31
|
+
export const STORAGE_KEY = 'mion:client';
|
|
32
|
+
|
|
33
|
+
/** RoutesFlow route key - matches router constant */
|
|
34
|
+
export const ROUTES_FLOW_KEY = 'mion-routes-flow';
|
|
35
|
+
|
|
36
|
+
/** RoutesFlow route path - matches router constant */
|
|
37
|
+
export const ROUTES_FLOW_PATH = `/${ROUTES_FLOW_KEY}`;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* ########
|
|
2
|
+
* 2023 mion
|
|
3
|
+
* Author: Ma-jerez
|
|
4
|
+
* License: MIT
|
|
5
|
+
* The software is provided "as is", without warranty of any kind.
|
|
6
|
+
* ######## */
|
|
7
|
+
|
|
8
|
+
import {isRpcError, addRoutesToCache} from '@mionjs/core';
|
|
9
|
+
import {MION_ROUTES} from '@mionjs/core';
|
|
10
|
+
import {ClientOptions, SubRequest} from '../types.ts';
|
|
11
|
+
import type {
|
|
12
|
+
JitCompiledFnData,
|
|
13
|
+
MethodsCache,
|
|
14
|
+
MethodWithOptions,
|
|
15
|
+
PureFunctionData,
|
|
16
|
+
SerializableMethodsData,
|
|
17
|
+
PureFnsDataCache,
|
|
18
|
+
} from '@mionjs/core';
|
|
19
|
+
import {routesCache, addSerializedJitCaches} from '@mionjs/core';
|
|
20
|
+
import {STORAGE_KEY} from '../constants.ts';
|
|
21
|
+
import {getStorage} from './storage.ts';
|
|
22
|
+
|
|
23
|
+
const METHOD_DATA_PREFIX = `${STORAGE_KEY}:method-data:`;
|
|
24
|
+
const JIT_FN_PREFIX = `${STORAGE_KEY}:jit-fn:`;
|
|
25
|
+
const PURE_FN_PREFIX = `${STORAGE_KEY}:pure-fn:`;
|
|
26
|
+
|
|
27
|
+
type MetadataRouteKey = typeof MION_ROUTES.methodsMetadata | typeof MION_ROUTES.methodsMetadataById;
|
|
28
|
+
|
|
29
|
+
/** Extracts raw metadata from a parsed response body, unwraps the JIT union discriminator, and processes it. */
|
|
30
|
+
export function extractAndProcessMetadata(routeKey: MetadataRouteKey, parsedBody: any, options: ClientOptions): void {
|
|
31
|
+
if (typeof parsedBody !== 'object' || !(routeKey in parsedBody)) return;
|
|
32
|
+
const rawMetadata = parsedBody[routeKey];
|
|
33
|
+
delete parsedBody[routeKey];
|
|
34
|
+
if (!rawMetadata) return;
|
|
35
|
+
const metadataValue = Array.isArray(rawMetadata) ? rawMetadata[1] : rawMetadata;
|
|
36
|
+
if (metadataValue && !isRpcError(metadataValue) && metadataValue.methods) {
|
|
37
|
+
processMethodsMetadata(metadataValue as SerializableMethodsData, options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Processes metadata from an optimistic response and caches it */
|
|
42
|
+
function processMethodsMetadata(serializableMethodsData: SerializableMethodsData, options: ClientOptions): void {
|
|
43
|
+
storeDependencies(serializableMethodsData.deps, serializableMethodsData.purFnDeps, options);
|
|
44
|
+
storeMethodsMetadata(serializableMethodsData.methods, options);
|
|
45
|
+
addToCaches(serializableMethodsData);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getSerializedMethodDataKey(methodId: string, options: ClientOptions) {
|
|
49
|
+
return `${METHOD_DATA_PREFIX}${methodId}:${options.baseURL}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getJitCompiledFnKey(jitFnHash: string, options: ClientOptions) {
|
|
53
|
+
return `${JIT_FN_PREFIX}${jitFnHash}:${options.baseURL}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getJitPureFnKey(namespace: string, pureFnHash: string, options: ClientOptions) {
|
|
57
|
+
return `${PURE_FN_PREFIX}${namespace}:${pureFnHash}:${options.baseURL}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Stores JIT compiled functions and pure functions globally in localStorage */
|
|
61
|
+
export function storeDependencies(deps: Record<string, JitCompiledFnData>, pureFnDeps: PureFnsDataCache, options: ClientOptions) {
|
|
62
|
+
Object.entries(deps).forEach(([hash, jitFnData]: [string, JitCompiledFnData]) => {
|
|
63
|
+
const key = getJitCompiledFnKey(hash, options);
|
|
64
|
+
try {
|
|
65
|
+
getStorage().setItem(key, JSON.stringify(jitFnData));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn(`Failed to store JIT function dependency ${hash}:`, error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Store namespaced pure functions
|
|
72
|
+
Object.entries(pureFnDeps).forEach(([namespace, nsPureFns]) => {
|
|
73
|
+
Object.entries(nsPureFns).forEach(([fnHash, pureFnData]: [string, PureFunctionData]) => {
|
|
74
|
+
const key = getJitPureFnKey(namespace, fnHash, options);
|
|
75
|
+
try {
|
|
76
|
+
getStorage().setItem(key, JSON.stringify(pureFnData));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.warn(`Failed to store pure function dependency ${namespace}::${fnHash}:`, error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Stores method metadata in localStorage using the new storage format */
|
|
85
|
+
export function storeMethodsMetadata(methods: MethodsCache, options: ClientOptions) {
|
|
86
|
+
Object.entries(methods).forEach(([methodId, methodData]) => {
|
|
87
|
+
const key = getSerializedMethodDataKey(methodId, options);
|
|
88
|
+
try {
|
|
89
|
+
getStorage().setItem(key, JSON.stringify(methodData));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(`Failed to store method metadata ${methodId}:`, error);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Restores all JIT compiled functions and pure functions from localStorage and deserializes them */
|
|
97
|
+
export function restoreAllDependencies(options: ClientOptions) {
|
|
98
|
+
const deps: Record<string, JitCompiledFnData> = {};
|
|
99
|
+
const pureFnDeps: PureFnsDataCache = {};
|
|
100
|
+
const baseURLSuffix = `:${options.baseURL}`;
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < getStorage().length; i++) {
|
|
103
|
+
const key = getStorage().key(i);
|
|
104
|
+
if (key?.startsWith(JIT_FN_PREFIX) && key.endsWith(baseURLSuffix)) {
|
|
105
|
+
try {
|
|
106
|
+
const data = getStorage().getItem(key);
|
|
107
|
+
if (data) {
|
|
108
|
+
const parsedData = JSON.parse(data);
|
|
109
|
+
deps[parsedData.jitFnHash] = parsedData;
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(`Failed to restore JIT function from key ${key}:`, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < getStorage().length; i++) {
|
|
118
|
+
const key = getStorage().key(i);
|
|
119
|
+
if (key?.startsWith(PURE_FN_PREFIX) && key.endsWith(baseURLSuffix)) {
|
|
120
|
+
try {
|
|
121
|
+
const data = getStorage().getItem(key);
|
|
122
|
+
if (data) {
|
|
123
|
+
const parsedData = JSON.parse(data);
|
|
124
|
+
// Extract namespace from key: "mion:pure-fn:namespace:fnHash:baseURL"
|
|
125
|
+
const inner = key.slice(PURE_FN_PREFIX.length, key.length - baseURLSuffix.length);
|
|
126
|
+
const namespace = inner.split(':')[0] || parsedData.namespace;
|
|
127
|
+
if (!pureFnDeps[namespace]) pureFnDeps[namespace] = {};
|
|
128
|
+
pureFnDeps[namespace][parsedData.fnName] = parsedData;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn(`Failed to restore pure function from key ${key}:`, error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Object.keys(deps).length > 0 || Object.keys(pureFnDeps).length > 0) {
|
|
137
|
+
addSerializedJitCaches(deps, pureFnDeps);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Creates a SubRequest for the metadata middleware to piggyback on an optimistic request */
|
|
142
|
+
export function createMetadataSubRequest(methodIds: string[]): SubRequest<any> {
|
|
143
|
+
return {
|
|
144
|
+
pointer: [MION_ROUTES.methodsMetadata],
|
|
145
|
+
id: MION_ROUTES.methodsMetadata,
|
|
146
|
+
isResolved: false,
|
|
147
|
+
params: [methodIds],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Restores method metadata from localStorage using the new storage format */
|
|
152
|
+
export function restoreFromLocalStorage(methodIds: string[], options: ClientOptions) {
|
|
153
|
+
restoreAllDependencies(options);
|
|
154
|
+
|
|
155
|
+
const methods: MethodsCache = {};
|
|
156
|
+
let anyMethodsRestored = false;
|
|
157
|
+
|
|
158
|
+
methodIds.forEach((id) => {
|
|
159
|
+
if (routesCache.hasMetadata(id)) return;
|
|
160
|
+
const methodKey = getSerializedMethodDataKey(id, options);
|
|
161
|
+
const methodMetaJson = getStorage().getItem(methodKey);
|
|
162
|
+
if (methodMetaJson) {
|
|
163
|
+
try {
|
|
164
|
+
const methodMeta: MethodWithOptions = JSON.parse(methodMetaJson);
|
|
165
|
+
methods[id] = methodMeta;
|
|
166
|
+
anyMethodsRestored = true;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn(`Failed to restore method metadata for ${id}:`, error);
|
|
169
|
+
getStorage().removeItem(methodKey);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (anyMethodsRestored) {
|
|
175
|
+
const serializableMethodsData: SerializableMethodsData = {
|
|
176
|
+
methods,
|
|
177
|
+
deps: {},
|
|
178
|
+
purFnDeps: {},
|
|
179
|
+
};
|
|
180
|
+
addToCaches(serializableMethodsData);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function addToCaches(serializableMethodsData: SerializableMethodsData) {
|
|
185
|
+
addSerializedJitCaches(serializableMethodsData.deps, serializableMethodsData.purFnDeps);
|
|
186
|
+
addRoutesToCache(serializableMethodsData.methods);
|
|
187
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* ########
|
|
2
|
+
* 2023 mion
|
|
3
|
+
* Author: Ma-jerez
|
|
4
|
+
* License: MIT
|
|
5
|
+
* The software is provided "as is", without warranty of any kind.
|
|
6
|
+
* ######## */
|
|
7
|
+
|
|
8
|
+
import {isRpcError, MION_ROUTES, getRoutePath, routesCache} from '@mionjs/core';
|
|
9
|
+
import {ClientOptions, RequestBody} from '../types.ts';
|
|
10
|
+
import {restoreFromLocalStorage} from './clientMethodsMetadata.ts';
|
|
11
|
+
import {deserializeResponseBody} from './serializer.ts';
|
|
12
|
+
|
|
13
|
+
/** Manually calls mionGetRemoteMethodsInfoById to get Remote Api Metadata */
|
|
14
|
+
export async function fetchRemoteMethodsMetadata(
|
|
15
|
+
methodIds: string[],
|
|
16
|
+
options: ClientOptions,
|
|
17
|
+
signal?: AbortSignal
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
restoreFromLocalStorage(methodIds, options);
|
|
20
|
+
const missingAfterLocal = methodIds.filter((path) => !routesCache.hasMetadata(path));
|
|
21
|
+
if (!missingAfterLocal.length) return;
|
|
22
|
+
const body: RequestBody = {
|
|
23
|
+
[MION_ROUTES.methodsMetadataById]: [missingAfterLocal],
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
const path = getRoutePath([MION_ROUTES.methodsMetadataById], options);
|
|
27
|
+
const url = new URL(path, options.baseURL);
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {'Content-Type': 'application/json'},
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
signal,
|
|
33
|
+
});
|
|
34
|
+
const deserialized = await deserializeResponseBody(response, options);
|
|
35
|
+
const platformError = deserialized[MION_ROUTES.platformError];
|
|
36
|
+
if (isRpcError(platformError)) throw platformError;
|
|
37
|
+
const stillMissing = missingAfterLocal.filter((id) => !routesCache.hasMetadata(id));
|
|
38
|
+
if (stillMissing.length) throw new Error(`Failed to fetch metadata for: ${stillMissing.join(', ')}`);
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
// Preserve abort/timeout DOMException so the caller's onError can classify it correctly
|
|
41
|
+
if (signal?.aborted) throw error;
|
|
42
|
+
throw new Error(`Error fetching validation and serialization metadata: ${error?.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* ########
|
|
2
|
+
* 2025 mion
|
|
3
|
+
* Author: Ma-jerez
|
|
4
|
+
* License: MIT
|
|
5
|
+
* The software is provided "as is", without warranty of any kind.
|
|
6
|
+
* ######## */
|
|
7
|
+
|
|
8
|
+
import type {RpcError} from '@mionjs/core';
|
|
9
|
+
import type {ErrorHandler, SuccessHandler} from '../types.ts';
|
|
10
|
+
|
|
11
|
+
/** Central registry for persistent middleFn handlers (both success and error) */
|
|
12
|
+
export class HandlersRegistry {
|
|
13
|
+
private errorHandlers: Map<string, Map<string, ErrorHandler<any>>> = new Map();
|
|
14
|
+
private successHandlers: Map<string, SuccessHandler<any>> = new Map();
|
|
15
|
+
|
|
16
|
+
/** Register an error handler for a specific handler (middleFn) and error type */
|
|
17
|
+
register(handlerId: string, errorType: string, handler: ErrorHandler<any>): void {
|
|
18
|
+
let handlerMap = this.errorHandlers.get(handlerId);
|
|
19
|
+
if (!handlerMap) {
|
|
20
|
+
handlerMap = new Map();
|
|
21
|
+
this.errorHandlers.set(handlerId, handlerMap);
|
|
22
|
+
}
|
|
23
|
+
handlerMap.set(errorType, handler);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Unregister an error handler for a specific handler and error type */
|
|
27
|
+
unregister(handlerId: string, errorType: string): void {
|
|
28
|
+
const handlerMap = this.errorHandlers.get(handlerId);
|
|
29
|
+
if (handlerMap) {
|
|
30
|
+
handlerMap.delete(errorType);
|
|
31
|
+
if (handlerMap.size === 0) this.errorHandlers.delete(handlerId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Check if an error handler exists for a specific handler ID and error type */
|
|
36
|
+
hasHandler(handlerId: string, errorType: string): boolean {
|
|
37
|
+
const handlerMap = this.errorHandlers.get(handlerId);
|
|
38
|
+
return handlerMap?.has(errorType) ?? false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Get and execute the handler for an error, if it exists */
|
|
42
|
+
executeHandler(handlerId: string, error: RpcError<string>): boolean {
|
|
43
|
+
const handlerMap = this.errorHandlers.get(handlerId);
|
|
44
|
+
if (!handlerMap) return false;
|
|
45
|
+
|
|
46
|
+
const handler = handlerMap.get(error.type);
|
|
47
|
+
if (!handler) return false;
|
|
48
|
+
|
|
49
|
+
handler(error);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Register a success handler for a specific middleFn */
|
|
54
|
+
registerSuccess(handlerId: string, handler: SuccessHandler<any>): void {
|
|
55
|
+
this.successHandlers.set(handlerId, handler);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Unregister a success handler for a specific middleFn */
|
|
59
|
+
unregisterSuccess(handlerId: string): void {
|
|
60
|
+
this.successHandlers.delete(handlerId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Check if a success handler exists for a specific handler ID */
|
|
64
|
+
hasSuccessHandler(handlerId: string): boolean {
|
|
65
|
+
return this.successHandlers.has(handlerId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Get and execute the success handler for a middleFn result, if it exists */
|
|
69
|
+
executeSuccessHandler(handlerId: string, result: any): boolean {
|
|
70
|
+
const handler = this.successHandlers.get(handlerId);
|
|
71
|
+
if (!handler) return false;
|
|
72
|
+
|
|
73
|
+
handler(result);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Clear all handlers (both error and success) for a specific handler ID */
|
|
78
|
+
clearHandlers(handlerId: string): void {
|
|
79
|
+
this.errorHandlers.delete(handlerId);
|
|
80
|
+
this.successHandlers.delete(handlerId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Clear all registered handlers (both error and success) */
|
|
84
|
+
clearAll(): void {
|
|
85
|
+
this.errorHandlers.clear();
|
|
86
|
+
this.successHandlers.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get all registered handler IDs */
|
|
90
|
+
getHandlerIds(): string[] {
|
|
91
|
+
const errorIds = Array.from(this.errorHandlers.keys());
|
|
92
|
+
const successIds = Array.from(this.successHandlers.keys());
|
|
93
|
+
return [...new Set([...errorIds, ...successIds])];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Get all error types registered for a handler ID */
|
|
97
|
+
getErrorTypes(handlerId: string): string[] {
|
|
98
|
+
const handlerMap = this.errorHandlers.get(handlerId);
|
|
99
|
+
return handlerMap ? Array.from(handlerMap.keys()) : [];
|
|
100
|
+
}
|
|
101
|
+
}
|