@antelopejs/dms 0.5.4 → 0.5.5
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
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.5.5
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.5.4...v0.5.5)
|
|
6
|
+
|
|
7
|
+
### 🩹 Fixes
|
|
8
|
+
|
|
9
|
+
- **layout:** Hold the in-flight modules listing fetch on the DMS app ([#121](https://github.com/AntelopeJS/dms/pull/121))
|
|
10
|
+
|
|
11
|
+
### ❤️ Contributors
|
|
12
|
+
|
|
13
|
+
- Alessandro Aloisio
|
|
14
|
+
|
|
3
15
|
## v0.5.4
|
|
4
16
|
|
|
5
17
|
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.5.3...v0.5.4)
|
|
@@ -6,6 +6,7 @@ type ModulesListingEntry = ModuleInfo & {
|
|
|
6
6
|
const STATE_KEY_MODULES = "dms-modulesListing";
|
|
7
7
|
const STATE_KEY_LOADING = "dms-modulesListingLoading";
|
|
8
8
|
const STATE_KEY_ERROR = "dms-modulesListingError";
|
|
9
|
+
const IN_FLIGHT_MODULES_LISTING_KEY = "__dmsInFlightModulesListing";
|
|
9
10
|
const ENDPOINT_MODULES_LISTING = "/dms/modules-listing";
|
|
10
11
|
const ERROR_FORBIDDEN_MESSAGE = "Forbidden: only owners can list modules";
|
|
11
12
|
const ERROR_UNKNOWN_MESSAGE = "Unknown error loading modules listing";
|
|
@@ -26,8 +27,6 @@ function extractErrorMessage(error: unknown): string {
|
|
|
26
27
|
return ERROR_UNKNOWN_MESSAGE;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
let inFlightLoadingPromise: Promise<void> | null = null;
|
|
30
|
-
|
|
31
30
|
export const useModulesListing = () => {
|
|
32
31
|
const modules = useDmsState<ModulesListingEntry[] | undefined>(
|
|
33
32
|
STATE_KEY_MODULES,
|
|
@@ -36,6 +35,21 @@ export const useModulesListing = () => {
|
|
|
36
35
|
const isLoading = useDmsState<boolean>(STATE_KEY_LOADING, () => false);
|
|
37
36
|
const loadingError = useDmsState<string | null>(STATE_KEY_ERROR, () => null);
|
|
38
37
|
|
|
38
|
+
// Held on the DMS app, like the in-flight fetches of useSiteLayout: every
|
|
39
|
+
// caller writes the same listing state, so they share one fetch. Not module
|
|
40
|
+
// scope, which the server shares across the requests it renders at once: a
|
|
41
|
+
// second render would wait on the first one's fetch, whose answer lands in
|
|
42
|
+
// the first request's state, and render an empty listing.
|
|
43
|
+
const dmsApp = useDmsApp() as unknown as Record<PropertyKey, unknown>;
|
|
44
|
+
const inFlightLoadingPromise = {
|
|
45
|
+
get value(): Promise<void> | null {
|
|
46
|
+
return (dmsApp[IN_FLIGHT_MODULES_LISTING_KEY] as Promise<void>) ?? null;
|
|
47
|
+
},
|
|
48
|
+
set value(promise: Promise<void> | null) {
|
|
49
|
+
dmsApp[IN_FLIGHT_MODULES_LISTING_KEY] = promise;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
39
53
|
function fetchModulesListing(): Promise<void> {
|
|
40
54
|
const { $authFetch } = useAuthFetch();
|
|
41
55
|
isLoading.value = true;
|
|
@@ -51,16 +65,16 @@ export const useModulesListing = () => {
|
|
|
51
65
|
})
|
|
52
66
|
.finally(() => {
|
|
53
67
|
isLoading.value = false;
|
|
54
|
-
inFlightLoadingPromise = null;
|
|
68
|
+
inFlightLoadingPromise.value = null;
|
|
55
69
|
});
|
|
56
70
|
|
|
57
|
-
inFlightLoadingPromise = promise;
|
|
71
|
+
inFlightLoadingPromise.value = promise;
|
|
58
72
|
return promise;
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
async function loadModulesListing(): Promise<void> {
|
|
62
|
-
if (inFlightLoadingPromise) {
|
|
63
|
-
await inFlightLoadingPromise;
|
|
76
|
+
if (inFlightLoadingPromise.value) {
|
|
77
|
+
await inFlightLoadingPromise.value;
|
|
64
78
|
return;
|
|
65
79
|
}
|
|
66
80
|
if (modules.value !== undefined) {
|
|
@@ -70,8 +84,8 @@ export const useModulesListing = () => {
|
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
async function refresh(): Promise<void> {
|
|
73
|
-
if (inFlightLoadingPromise) {
|
|
74
|
-
await inFlightLoadingPromise;
|
|
87
|
+
if (inFlightLoadingPromise.value) {
|
|
88
|
+
await inFlightLoadingPromise.value;
|
|
75
89
|
}
|
|
76
90
|
modules.value = undefined;
|
|
77
91
|
loadingError.value = null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The modules listing as a server runs it: every request it renders has its
|
|
3
|
+
* own DMS app and its own state, and two renders of the modules page can be in
|
|
4
|
+
* flight at once. Each one must fetch and render its own listing.
|
|
5
|
+
*/
|
|
6
|
+
import { ref, type Ref } from "vue";
|
|
7
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
8
|
+
import { useModulesListing } from "../layers/dms-layout/app/composables/page/useModulesListing";
|
|
9
|
+
|
|
10
|
+
interface Render {
|
|
11
|
+
app: Record<PropertyKey, unknown>;
|
|
12
|
+
state: Map<string, Ref>;
|
|
13
|
+
authFetch: ReturnType<typeof vi.fn>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ordersModule = {
|
|
17
|
+
id: "orders",
|
|
18
|
+
title: "Orders",
|
|
19
|
+
description: "",
|
|
20
|
+
icon: "i-lucide-package",
|
|
21
|
+
hasAccess: true,
|
|
22
|
+
landingSlug: "/modules/orders/list",
|
|
23
|
+
};
|
|
24
|
+
const billingModule = {
|
|
25
|
+
id: "billing",
|
|
26
|
+
title: "Billing",
|
|
27
|
+
description: "",
|
|
28
|
+
icon: "i-lucide-receipt",
|
|
29
|
+
hasAccess: true,
|
|
30
|
+
landingSlug: "/modules/billing/invoices",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let current: Render;
|
|
34
|
+
|
|
35
|
+
function createRender(): Render {
|
|
36
|
+
return { app: {}, state: new Map(), authFetch: vi.fn() };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// What the renderer resolves from the component being set up: the app and
|
|
40
|
+
// the state of the request that component belongs to.
|
|
41
|
+
function inRender<T>(render: Render, callback: () => T): T {
|
|
42
|
+
current = render;
|
|
43
|
+
return callback();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deferred<T>() {
|
|
47
|
+
let resolve!: (value: T) => void;
|
|
48
|
+
const promise = new Promise<T>((settle) => {
|
|
49
|
+
resolve = settle;
|
|
50
|
+
});
|
|
51
|
+
return { promise, resolve };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.stubGlobal("useDmsApp", () => current.app);
|
|
56
|
+
vi.stubGlobal("useDmsState", (key: string, initial: () => unknown) => {
|
|
57
|
+
if (!current.state.has(key)) current.state.set(key, ref(initial()));
|
|
58
|
+
return current.state.get(key);
|
|
59
|
+
});
|
|
60
|
+
vi.stubGlobal("useAuthFetch", () => ({ $authFetch: current.authFetch }));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
vi.unstubAllGlobals();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("fetches the listing of a render that starts while another render's fetch is in flight", async () => {
|
|
68
|
+
const first = createRender();
|
|
69
|
+
const second = createRender();
|
|
70
|
+
const firstResponse = deferred<unknown[]>();
|
|
71
|
+
first.authFetch.mockReturnValueOnce(firstResponse.promise);
|
|
72
|
+
second.authFetch.mockResolvedValueOnce([billingModule]);
|
|
73
|
+
|
|
74
|
+
const firstListing = inRender(first, () => useModulesListing());
|
|
75
|
+
const firstLoad = inRender(first, () => firstListing.loadModulesListing());
|
|
76
|
+
const secondListing = inRender(second, () => useModulesListing());
|
|
77
|
+
const secondLoad = inRender(second, () => secondListing.loadModulesListing());
|
|
78
|
+
firstResponse.resolve([ordersModule]);
|
|
79
|
+
await Promise.all([firstLoad, secondLoad]);
|
|
80
|
+
|
|
81
|
+
expect(secondListing.modules.value).toEqual([billingModule]);
|
|
82
|
+
expect(second.authFetch).toHaveBeenCalledOnce();
|
|
83
|
+
expect(firstListing.modules.value).toEqual([ordersModule]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("still shares one fetch between the callers of one render", async () => {
|
|
87
|
+
const render = createRender();
|
|
88
|
+
const response = deferred<unknown[]>();
|
|
89
|
+
render.authFetch.mockReturnValueOnce(response.promise);
|
|
90
|
+
|
|
91
|
+
const [page, widget] = inRender(render, () => [
|
|
92
|
+
useModulesListing(),
|
|
93
|
+
useModulesListing(),
|
|
94
|
+
]);
|
|
95
|
+
const loads = inRender(render, () => [
|
|
96
|
+
page.loadModulesListing(),
|
|
97
|
+
widget.loadModulesListing(),
|
|
98
|
+
]);
|
|
99
|
+
response.resolve([ordersModule]);
|
|
100
|
+
await Promise.all(loads);
|
|
101
|
+
|
|
102
|
+
expect(render.authFetch).toHaveBeenCalledOnce();
|
|
103
|
+
expect(widget.modules.value).toEqual([ordersModule]);
|
|
104
|
+
});
|