@async/framework 0.11.1 → 0.11.2
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/CHANGELOG.md +16 -0
- package/browser.js +184 -28
- package/browser.min.js +1 -1
- package/browser.ts +184 -28
- package/browser.umd.js +184 -28
- package/browser.umd.min.js +1 -1
- package/framework.ts +184 -28
- package/package.json +4 -3
- package/server.js +184 -28
package/framework.ts
CHANGED
|
@@ -188,6 +188,24 @@ const __asyncSignalModule = (() => {
|
|
|
188
188
|
};
|
|
189
189
|
},
|
|
190
190
|
|
|
191
|
+
_restore(snapshot = {}) {
|
|
192
|
+
if (!isAsyncSignalSnapshot(snapshot)) {
|
|
193
|
+
return state.set(snapshot);
|
|
194
|
+
}
|
|
195
|
+
if (activeAbort && !activeAbort.aborted) {
|
|
196
|
+
activeAbort.cancel(new Error(`Async signal "${registeredId}" restored from snapshot.`));
|
|
197
|
+
}
|
|
198
|
+
value = snapshot.value;
|
|
199
|
+
loading = Boolean(snapshot.loading);
|
|
200
|
+
error = snapshot.error ?? null;
|
|
201
|
+
status = typeof snapshot.status === "string" ? snapshot.status : inferStatus({ value, loading, error });
|
|
202
|
+
if (Number.isFinite(snapshot.version)) {
|
|
203
|
+
version = snapshot.version;
|
|
204
|
+
}
|
|
205
|
+
notify();
|
|
206
|
+
return state;
|
|
207
|
+
},
|
|
208
|
+
|
|
191
209
|
_bindRegistry(nextRegistry, nextId) {
|
|
192
210
|
registry = nextRegistry;
|
|
193
211
|
registeredId = nextId;
|
|
@@ -273,6 +291,27 @@ const __asyncSignalModule = (() => {
|
|
|
273
291
|
return Boolean(value?.[asyncSignalKind]);
|
|
274
292
|
}
|
|
275
293
|
|
|
294
|
+
function isAsyncSignalSnapshot(value) {
|
|
295
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return Object.hasOwn(value, "value")
|
|
299
|
+
&& (Object.hasOwn(value, "loading")
|
|
300
|
+
|| Object.hasOwn(value, "error")
|
|
301
|
+
|| Object.hasOwn(value, "status")
|
|
302
|
+
|| Object.hasOwn(value, "version"));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function inferStatus({ value, loading, error }) {
|
|
306
|
+
if (loading) {
|
|
307
|
+
return "loading";
|
|
308
|
+
}
|
|
309
|
+
if (error) {
|
|
310
|
+
return "error";
|
|
311
|
+
}
|
|
312
|
+
return value === undefined ? "idle" : "ready";
|
|
313
|
+
}
|
|
314
|
+
|
|
276
315
|
function attachCancel(signal, controller) {
|
|
277
316
|
Object.defineProperty(signal, "cancel", {
|
|
278
317
|
configurable: true,
|
|
@@ -1911,12 +1950,16 @@ const __componentModule = (() => {
|
|
|
1911
1950
|
});
|
|
1912
1951
|
|
|
1913
1952
|
const output = Component.call(context, props);
|
|
1953
|
+
if (output && typeof output.then === "function") {
|
|
1954
|
+
throw new TypeError(`Component "${componentName(Component)}" returned a Promise. Async components are not supported by synchronous renderComponent(). Use an async partial or handler instead.`);
|
|
1955
|
+
}
|
|
1914
1956
|
const html = renderScopedTemplate(output);
|
|
1915
1957
|
|
|
1916
1958
|
return {
|
|
1917
1959
|
html,
|
|
1918
1960
|
attach(target) {
|
|
1919
|
-
for (
|
|
1961
|
+
for (let index = 0; index < attachHooks.length; index += 1) {
|
|
1962
|
+
const hook = attachHooks[index];
|
|
1920
1963
|
runtime.scheduler?.enqueue("lifecycle", () => {
|
|
1921
1964
|
const cleanup = hook(target);
|
|
1922
1965
|
if (typeof cleanup === "function") {
|
|
@@ -1924,7 +1967,7 @@ const __componentModule = (() => {
|
|
|
1924
1967
|
}
|
|
1925
1968
|
}, {
|
|
1926
1969
|
scope,
|
|
1927
|
-
key: `attach:${
|
|
1970
|
+
key: `attach:${index}`
|
|
1928
1971
|
}) ?? runAttachHook(hook, target);
|
|
1929
1972
|
}
|
|
1930
1973
|
},
|
|
@@ -1932,8 +1975,12 @@ const __componentModule = (() => {
|
|
|
1932
1975
|
this.attach(target);
|
|
1933
1976
|
},
|
|
1934
1977
|
visible(target, observeVisible) {
|
|
1935
|
-
|
|
1936
|
-
|
|
1978
|
+
if (visibleHooks.length === 0) {
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const cleanup = observeVisible(target, () => {
|
|
1982
|
+
for (let index = 0; index < visibleHooks.length; index += 1) {
|
|
1983
|
+
const hook = visibleHooks[index];
|
|
1937
1984
|
runtime.scheduler?.enqueue("lifecycle", () => {
|
|
1938
1985
|
const hookCleanup = hook(target);
|
|
1939
1986
|
if (typeof hookCleanup === "function") {
|
|
@@ -1941,12 +1988,12 @@ const __componentModule = (() => {
|
|
|
1941
1988
|
}
|
|
1942
1989
|
}, {
|
|
1943
1990
|
scope,
|
|
1944
|
-
key: `visible:${
|
|
1991
|
+
key: `visible:${index}`
|
|
1945
1992
|
}) ?? runVisibleHook(hook, target);
|
|
1946
|
-
});
|
|
1947
|
-
if (typeof cleanup === "function") {
|
|
1948
|
-
cleanups.push(cleanup);
|
|
1949
1993
|
}
|
|
1994
|
+
});
|
|
1995
|
+
if (typeof cleanup === "function") {
|
|
1996
|
+
cleanups.push(cleanup);
|
|
1950
1997
|
}
|
|
1951
1998
|
},
|
|
1952
1999
|
cleanup() {
|
|
@@ -2202,11 +2249,12 @@ const __serverModule = (() => {
|
|
|
2202
2249
|
signal: context.abort
|
|
2203
2250
|
});
|
|
2204
2251
|
|
|
2252
|
+
assertTransportResponse(id, response);
|
|
2205
2253
|
if (!response.ok) {
|
|
2206
2254
|
throw new Error(`Server function "${id}" failed with ${response.status}.`);
|
|
2207
2255
|
}
|
|
2208
2256
|
|
|
2209
|
-
const result = await readServerResponse(response);
|
|
2257
|
+
const result = await readServerResponse(id, response);
|
|
2210
2258
|
await applyServerResult(result, runContext);
|
|
2211
2259
|
return markAppliedServerValue(unwrapServerResult(result));
|
|
2212
2260
|
}
|
|
@@ -2247,6 +2295,11 @@ const __serverModule = (() => {
|
|
|
2247
2295
|
return result;
|
|
2248
2296
|
}
|
|
2249
2297
|
|
|
2298
|
+
if (result.error) {
|
|
2299
|
+
markAppliedServerResult(result);
|
|
2300
|
+
throw toError(result.error);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2250
2303
|
if (result.signals && context.signals) {
|
|
2251
2304
|
for (const [path, value] of Object.entries(result.signals)) {
|
|
2252
2305
|
context.signals.set?.(path, value);
|
|
@@ -2265,15 +2318,7 @@ const __serverModule = (() => {
|
|
|
2265
2318
|
await context.router?.navigate?.(result.redirect);
|
|
2266
2319
|
}
|
|
2267
2320
|
|
|
2268
|
-
|
|
2269
|
-
throw toError(result.error);
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
Object.defineProperty(result, appliedServerResult, {
|
|
2273
|
-
configurable: true,
|
|
2274
|
-
enumerable: false,
|
|
2275
|
-
value: true
|
|
2276
|
-
});
|
|
2321
|
+
markAppliedServerResult(result);
|
|
2277
2322
|
|
|
2278
2323
|
return result;
|
|
2279
2324
|
}
|
|
@@ -2292,6 +2337,15 @@ const __serverModule = (() => {
|
|
|
2292
2337
|
return value;
|
|
2293
2338
|
}
|
|
2294
2339
|
|
|
2340
|
+
function markAppliedServerResult(result) {
|
|
2341
|
+
Object.defineProperty(result, appliedServerResult, {
|
|
2342
|
+
configurable: true,
|
|
2343
|
+
enumerable: false,
|
|
2344
|
+
value: true
|
|
2345
|
+
});
|
|
2346
|
+
return result;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2295
2349
|
function defaultInput(context = {}) {
|
|
2296
2350
|
const form = findForm(context);
|
|
2297
2351
|
if (form) {
|
|
@@ -2369,10 +2423,37 @@ const __serverModule = (() => {
|
|
|
2369
2423
|
return namespace([]);
|
|
2370
2424
|
}
|
|
2371
2425
|
|
|
2372
|
-
|
|
2426
|
+
function assertTransportResponse(id, response) {
|
|
2427
|
+
if (!response || typeof response !== "object") {
|
|
2428
|
+
throw new Error(`Server function "${id}" transport returned an invalid response: expected a fetch Response-like object.`);
|
|
2429
|
+
}
|
|
2430
|
+
if (typeof response.ok !== "boolean") {
|
|
2431
|
+
throw new Error(`Server function "${id}" transport returned an invalid response: missing boolean ok.`);
|
|
2432
|
+
}
|
|
2433
|
+
if (!response.headers || typeof response.headers.get !== "function") {
|
|
2434
|
+
throw new Error(`Server function "${id}" transport returned an invalid response: missing headers.get(name).`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
async function readServerResponse(id, response) {
|
|
2439
|
+
if (response.status === 204) {
|
|
2440
|
+
return { value: undefined };
|
|
2441
|
+
}
|
|
2373
2442
|
const type = response.headers.get("content-type") ?? "";
|
|
2374
2443
|
if (type.includes("application/json")) {
|
|
2375
|
-
|
|
2444
|
+
if (typeof response.json !== "function") {
|
|
2445
|
+
throw new Error(`Server function "${id}" transport returned an invalid response: missing json().`);
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
return await response.json();
|
|
2449
|
+
} catch (cause) {
|
|
2450
|
+
throw new Error(`Server function "${id}" returned invalid JSON: ${errorMessage(cause)}`, {
|
|
2451
|
+
cause
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (typeof response.text !== "function") {
|
|
2456
|
+
throw new Error(`Server function "${id}" transport returned an invalid response: missing text().`);
|
|
2376
2457
|
}
|
|
2377
2458
|
return { value: await response.text() };
|
|
2378
2459
|
}
|
|
@@ -2485,6 +2566,9 @@ const __serverModule = (() => {
|
|
|
2485
2566
|
if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
|
|
2486
2567
|
throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
|
|
2487
2568
|
}
|
|
2569
|
+
if (isUnsupportedJsonTransportObject(value, tag)) {
|
|
2570
|
+
throw new Error("Server proxy JSON transport does not support URLSearchParams, Headers, Request, Response, ReadableStream, ArrayBuffer, or typed array values yet.");
|
|
2571
|
+
}
|
|
2488
2572
|
if (Array.isArray(value)) {
|
|
2489
2573
|
for (const item of value) {
|
|
2490
2574
|
assertJsonTransportable(item, stack);
|
|
@@ -2498,6 +2582,16 @@ const __serverModule = (() => {
|
|
|
2498
2582
|
stack.delete(value);
|
|
2499
2583
|
}
|
|
2500
2584
|
|
|
2585
|
+
function isUnsupportedJsonTransportObject(value, tag = Object.prototype.toString.call(value)) {
|
|
2586
|
+
return tag === "[object URLSearchParams]"
|
|
2587
|
+
|| tag === "[object Headers]"
|
|
2588
|
+
|| tag === "[object Request]"
|
|
2589
|
+
|| tag === "[object Response]"
|
|
2590
|
+
|| tag === "[object ReadableStream]"
|
|
2591
|
+
|| tag === "[object ArrayBuffer]"
|
|
2592
|
+
|| ArrayBuffer.isView(value);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2501
2595
|
function joinEndpoint(endpoint, id) {
|
|
2502
2596
|
return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
|
|
2503
2597
|
}
|
|
@@ -2519,6 +2613,10 @@ const __serverModule = (() => {
|
|
|
2519
2613
|
return new Error(String(value));
|
|
2520
2614
|
}
|
|
2521
2615
|
|
|
2616
|
+
function errorMessage(error) {
|
|
2617
|
+
return error instanceof Error ? error.message : String(error);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2522
2620
|
function assertServerId(id) {
|
|
2523
2621
|
if (typeof id !== "string" || id.length === 0) {
|
|
2524
2622
|
throw new TypeError("Server function id must be a non-empty string.");
|
|
@@ -2751,7 +2849,8 @@ const __schedulerModule = (() => {
|
|
|
2751
2849
|
const phases = [...(options.phases ?? defaultPhases)];
|
|
2752
2850
|
const queues = new Map(phases.map((phase) => [phase, []]));
|
|
2753
2851
|
const keyedJobs = new Map();
|
|
2754
|
-
const
|
|
2852
|
+
const destroyedObjectScopes = new WeakSet();
|
|
2853
|
+
const destroyedPrimitiveScopes = new Set();
|
|
2755
2854
|
const objectScopeIds = new WeakMap();
|
|
2756
2855
|
const onError = typeof options.onError === "function" ? options.onError : undefined;
|
|
2757
2856
|
const maxDepth = options.maxDepth ?? 100;
|
|
@@ -2799,7 +2898,7 @@ const __schedulerModule = (() => {
|
|
|
2799
2898
|
throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
|
|
2800
2899
|
}
|
|
2801
2900
|
const scope = options.scope;
|
|
2802
|
-
if (
|
|
2901
|
+
if (isScopeDestroyed(scope)) {
|
|
2803
2902
|
return noop;
|
|
2804
2903
|
}
|
|
2805
2904
|
|
|
@@ -2903,14 +3002,29 @@ const __schedulerModule = (() => {
|
|
|
2903
3002
|
|
|
2904
3003
|
markScopeDestroyed(scope) {
|
|
2905
3004
|
if (scope !== undefined) {
|
|
2906
|
-
|
|
3005
|
+
if (isObjectScope(scope)) {
|
|
3006
|
+
destroyedObjectScopes.add(scope);
|
|
3007
|
+
} else {
|
|
3008
|
+
destroyedPrimitiveScopes.add(scope);
|
|
3009
|
+
}
|
|
2907
3010
|
api.cancelScope(scope);
|
|
2908
3011
|
}
|
|
2909
3012
|
return api;
|
|
2910
3013
|
},
|
|
2911
3014
|
|
|
3015
|
+
reviveScope(scope) {
|
|
3016
|
+
if (scope !== undefined) {
|
|
3017
|
+
if (isObjectScope(scope)) {
|
|
3018
|
+
destroyedObjectScopes.delete(scope);
|
|
3019
|
+
} else {
|
|
3020
|
+
destroyedPrimitiveScopes.delete(scope);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return api;
|
|
3024
|
+
},
|
|
3025
|
+
|
|
2912
3026
|
isScopeDestroyed(scope) {
|
|
2913
|
-
return
|
|
3027
|
+
return isScopeDestroyed(scope);
|
|
2914
3028
|
},
|
|
2915
3029
|
|
|
2916
3030
|
inspect() {
|
|
@@ -2922,7 +3036,7 @@ const __schedulerModule = (() => {
|
|
|
2922
3036
|
strategy,
|
|
2923
3037
|
phases: [...phases],
|
|
2924
3038
|
pending: counts,
|
|
2925
|
-
scopesDestroyed:
|
|
3039
|
+
scopesDestroyed: destroyedPrimitiveScopes.size,
|
|
2926
3040
|
flushing,
|
|
2927
3041
|
scheduled
|
|
2928
3042
|
};
|
|
@@ -2937,7 +3051,7 @@ const __schedulerModule = (() => {
|
|
|
2937
3051
|
queue.length = 0;
|
|
2938
3052
|
}
|
|
2939
3053
|
keyedJobs.clear();
|
|
2940
|
-
|
|
3054
|
+
destroyedPrimitiveScopes.clear();
|
|
2941
3055
|
}
|
|
2942
3056
|
};
|
|
2943
3057
|
|
|
@@ -2977,7 +3091,7 @@ const __schedulerModule = (() => {
|
|
|
2977
3091
|
if (job.key) {
|
|
2978
3092
|
keyedJobs.delete(job.key);
|
|
2979
3093
|
}
|
|
2980
|
-
if (job.canceled || (job.scope
|
|
3094
|
+
if (job.canceled || isScopeDestroyed(job.scope)) {
|
|
2981
3095
|
continue;
|
|
2982
3096
|
}
|
|
2983
3097
|
try {
|
|
@@ -3034,6 +3148,20 @@ const __schedulerModule = (() => {
|
|
|
3034
3148
|
}
|
|
3035
3149
|
return String(scope);
|
|
3036
3150
|
}
|
|
3151
|
+
|
|
3152
|
+
function isScopeDestroyed(scope) {
|
|
3153
|
+
if (scope === undefined) {
|
|
3154
|
+
return false;
|
|
3155
|
+
}
|
|
3156
|
+
if (isObjectScope(scope)) {
|
|
3157
|
+
return destroyedObjectScopes.has(scope);
|
|
3158
|
+
}
|
|
3159
|
+
return destroyedPrimitiveScopes.has(scope);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
function isObjectScope(scope) {
|
|
3164
|
+
return (typeof scope === "object" && scope !== null) || typeof scope === "function";
|
|
3037
3165
|
}
|
|
3038
3166
|
|
|
3039
3167
|
function scheduleMicrotask(fn) {
|
|
@@ -3094,6 +3222,7 @@ const __loaderModule = (() => {
|
|
|
3094
3222
|
|
|
3095
3223
|
scan(rootOrFragment = rootNode) {
|
|
3096
3224
|
assertActive();
|
|
3225
|
+
reviveScopes(rootOrFragment);
|
|
3097
3226
|
bindSignalAttributes(rootOrFragment);
|
|
3098
3227
|
bindClassAttributes(rootOrFragment);
|
|
3099
3228
|
bindEventAttributes(rootOrFragment);
|
|
@@ -3597,6 +3726,12 @@ const __loaderModule = (() => {
|
|
|
3597
3726
|
}
|
|
3598
3727
|
}
|
|
3599
3728
|
|
|
3729
|
+
function reviveScopes(scope) {
|
|
3730
|
+
for (const element of elementsIn(scope)) {
|
|
3731
|
+
schedulerInstance.reviveScope?.(element);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3600
3735
|
return api;
|
|
3601
3736
|
}
|
|
3602
3737
|
|
|
@@ -4160,7 +4295,10 @@ const __routerModule = (() => {
|
|
|
4160
4295
|
}
|
|
4161
4296
|
const matched = api.match(url);
|
|
4162
4297
|
if (matched?.route?.partial && partials?.resolve?.(matched.route.partial)) {
|
|
4163
|
-
return partials.render(matched.route.partial, matched.params,
|
|
4298
|
+
return partials.render(matched.route.partial, matched.params, {
|
|
4299
|
+
...contextFor(matched),
|
|
4300
|
+
prefetch: true
|
|
4301
|
+
});
|
|
4164
4302
|
}
|
|
4165
4303
|
return Promise.resolve(null);
|
|
4166
4304
|
},
|
|
@@ -5196,6 +5334,13 @@ const __appModule = (() => {
|
|
|
5196
5334
|
function setOrRegisterSignal(signals, path, value) {
|
|
5197
5335
|
const id = String(path).split(".")[0];
|
|
5198
5336
|
if (signals.has?.(id)) {
|
|
5337
|
+
if (path === id) {
|
|
5338
|
+
const entry = signals._entry?.(id);
|
|
5339
|
+
if (typeof entry?._restore === "function" && isAsyncSignalSnapshot(value)) {
|
|
5340
|
+
entry._restore(value);
|
|
5341
|
+
return;
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5199
5344
|
signals.set(path, value);
|
|
5200
5345
|
return;
|
|
5201
5346
|
}
|
|
@@ -5205,6 +5350,17 @@ const __appModule = (() => {
|
|
|
5205
5350
|
}
|
|
5206
5351
|
}
|
|
5207
5352
|
|
|
5353
|
+
function isAsyncSignalSnapshot(value) {
|
|
5354
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5355
|
+
return false;
|
|
5356
|
+
}
|
|
5357
|
+
return Object.hasOwn(value, "value")
|
|
5358
|
+
&& (Object.hasOwn(value, "loading")
|
|
5359
|
+
|| Object.hasOwn(value, "error")
|
|
5360
|
+
|| Object.hasOwn(value, "status")
|
|
5361
|
+
|| Object.hasOwn(value, "version"));
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5208
5364
|
function attachServerCache(server, cache) {
|
|
5209
5365
|
try {
|
|
5210
5366
|
server.cache = cache;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "No-build Loader app runtime with browser and server entrypoints, signals, command events, route partials, cache split, SSR activation, and streaming boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server.js",
|
|
@@ -115,8 +115,9 @@
|
|
|
115
115
|
"pack:check": "pnpm run bundle:check && npm pack --dry-run --ignore-scripts",
|
|
116
116
|
"pipeline:github:check": "async-pipeline github check",
|
|
117
117
|
"pipeline:github:generate": "async-pipeline github generate",
|
|
118
|
-
"pipeline:pages": "async-pipeline run
|
|
118
|
+
"pipeline:pages": "async-pipeline run-task docs.site",
|
|
119
119
|
"pipeline:publish": "async-pipeline run publish",
|
|
120
|
+
"pipeline:publish:github:release": "async-pipeline publish github release --package . --registry https://npm.pkg.github.com",
|
|
120
121
|
"pipeline:publish:npm": "async-pipeline publish npm --package .",
|
|
121
122
|
"pipeline:release-doctor": "async-pipeline run release-doctor",
|
|
122
123
|
"pipeline:release:doctor": "async-pipeline release doctor --package .",
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
"test": "node --test tests/*.test.js"
|
|
131
132
|
},
|
|
132
133
|
"devDependencies": {
|
|
133
|
-
"@async/pipeline": "0.
|
|
134
|
+
"@async/pipeline": "0.9.1",
|
|
134
135
|
"happy-dom": "20.10.5",
|
|
135
136
|
"terser": "5.48.0"
|
|
136
137
|
}
|