Compare commits

...

1 Commits

Author SHA1 Message Date
Thierry
57a57f4391 feat(lite): rework subscriptions 2023-08-03 16:21:49 +02:00
15 changed files with 568 additions and 410 deletions

View File

@@ -4,6 +4,53 @@ All collections of `XenApiRecord` are stored inside the `xapiCollectionStore`.
To retrieve a collection, invoke `useXapiCollectionStore().get(type)`.
## TL;DR - How to extend a subscription
_**Note:** Once the extension grows in complexity, it's recommended to create a dedicated file for it (e.g. `host.extension.ts` for `host.store.ts`)._
```typescript
type MyExtension1 = Extension<{ propA: string }>;
type MyExtension2 = Extension<{ propB: string }, { withB: true }>;
type Extensions = [
XenApiRecordExtension<XenApiHost>, // If needed
DeferExtension, // If needed
MyExtension1,
MyExtension2
];
export const useHostStore = defineStore("host", () => {
const hostCollection = useXapiCollectionStore().get("console");
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const originalSubscription = hostCollection.subscribe(options);
const myExtension1: PartialSubscription<MyExtension1> = {
propA: "Hello",
};
const myExtension2: PartialSubscription<MyExtension2> | undefined =
options?.withB
? {
propB: "World",
}
: undefined;
return {
...originalSubscription,
...myExtension1,
...myExtension2,
};
};
return {
...hostCollection,
subscribe,
};
});
```
## Accessing a collection
In order to use a collection, you'll need to subscribe to it.
@@ -40,71 +87,102 @@ export const useConsoleStore = defineStore("console", () =>
To extend the base Subscription, you'll need to override the `subscribe` method.
For that, you can use the `createSubscribe<RawObjectType, Extensions>((options) => { /* ... */})` helper.
### Define the extensions
Subscription extensions are defined as `(object | [object, RequiredOptions])[]`.
Subscription extensions are defined as a simple extension (`Extension<object>`) or as a conditional
extension (`Extension<object, object>`).
When using a tuple (`[object, RequiredOptions]`), the corresponding `object` type will be added to the subscription if
the `RequiredOptions` for that tuple are present in the options passed to `subscribe`.
When using a conditional extension, the corresponding `object` type will be added to the subscription only if
the the options passed to `subscribe(options)` do match the second argument or `Extension`.
There is two existing extensions:
- `XenApiRecordExtension<T extends XenApiRecord>`: a simple extension which defined all the base
properties and methods (`records`, `getByOpaqueRef`, `getByUuid`, etc.)
- `DeferExtension`: a conditional extension which add the `start` and `isStarted` properties if the
`immediate` option is set to `false`.
```typescript
// Always present extension
type DefaultExtension = {
type PropABExtension = Extension<{
propA: string;
propB: ComputedRef<number>;
};
}>;
// Conditional extension 1
type FirstConditionalExtension = [
type PropCExtension = Extension<
{ propC: ComputedRef<string> }, // <- This signature will be added
{ optC: string } // <- if this condition is met
];
>;
// Conditional extension 2
type SecondConditionalExtension = [
type PropDExtension = Extension<
{ propD: () => void }, // <- This signature will be added
{ optD: number } // <- if this condition is met
];
>;
// Create the extensions array
type Extensions = [
DefaultExtension,
FirstConditionalExtension,
SecondConditionalExtension
XenApiRecordExtension<XenApiHost>,
DeferExtension,
PropABExtension,
PropCExtension,
PropDExtension
];
```
### Define the subscription
### Define the `subscribe` method
You can then create the `subscribe` function with the help of `Options` and `Subscription` helper types.
This will allow to get correct completion and type checking for the `options` argument, and to get the correct return
type based on passed options.
```typescript
const subscribe = <O extends Options<Extensions>>(options?: O) => {
return {
// ...
} as Subscription<Extensions, O>;
};
```
### Extend the subscription
The `PartialSubscription` type will help to define and check the data to add to subscription for each extension.
```typescript
export const useConsoleStore = defineStore("console", () => {
const consoleCollection = useXapiCollectionStore().get("console");
const subscribe = createSubscribe<"console", Extensions>((options) => {
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const originalSubscription = consoleCollection.subscribe(options);
const extendedSubscription = {
const propABSubscription: PartialSubscription<PropABExtension> = {
propA: "Some string",
propB: computed(() => 42),
};
const propCSubscription = options?.optC !== undefined && {
propC: computed(() => "Some other string"),
};
const propCSubscription: PartialSubscription<PropCExtension> | undefined =
options?.optC !== undefined
? {
propC: computed(() => "Some other string"),
}
: undefined;
const propDSubscription = options?.optD !== undefined && {
propD: () => console.log("Hello"),
};
const propDSubscription: PartialSubscription<PropDExtension> | undefined =
options?.optD !== undefined
? {
propD: () => console.log("Hello"),
}
: undefined;
return {
...originalSubscription,
...extendedSubscription,
...propABSubscription,
...propCSubscription,
...propDSubscription,
};
});
};
return {
...consoleCollection,
@@ -125,20 +203,18 @@ type Options = {
### Use the subscription
In each case, all the default properties (`records`, `getByUuid`, etc.) will be present.
```typescript
const store = useConsoleStore();
// No options (propA and propB will be present)
const subscription = store.subscribe();
// No options (Contains common properties: `propA`, `propB`, `records`, `getByUuid`, etc.)
const subscription1 = store.subscribe();
// optC option (propA, propB and propC will be present)
const subscription = store.subscribe({ optC: "Hello" });
// optC option (Contains common properties + `propC`)
const subscription2 = store.subscribe({ optC: "Hello" });
// optD option (propA, propB and propD will be present)
const subscription = store.subscribe({ optD: 12 });
// optD option (Contains common properties + `propD`)
const subscription3 = store.subscribe({ optD: 12 });
// optC and optD options (propA, propB, propC and propD will be present)
const subscription = store.subscribe({ optC: "Hello", optD: 12 });
// optC and optD options (Contains common properties + `propC` + `propD`)
const subscription4 = store.subscribe({ optC: "Hello", optD: 12 });
```

View File

@@ -5,9 +5,10 @@ import type {
XenApiVm,
VM_OPERATION,
RawObjectType,
XenApiHostMetrics,
} from "@/libs/xen-api";
import type { Filter } from "@/types/filter";
import type { Subscription } from "@/types/xapi-collection";
import type { XenApiRecordSubscription } from "@/types/subscription";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import { faFont, faHashtag, faList } from "@fortawesome/free-solid-svg-icons";
import { utcParse } from "d3-time-format";
@@ -116,14 +117,14 @@ export function getStatsLength(stats?: object | any[]) {
export function isHostRunning(
host: XenApiHost,
hostMetricsSubscription: Subscription<"host_metrics", object>
hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics>
) {
return hostMetricsSubscription.getByOpaqueRef(host.metrics)?.live === true;
}
export function getHostMemory(
host: XenApiHost,
hostMetricsSubscription: Subscription<"host_metrics", object>
hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics>
) {
const hostMetrics = hostMetricsSubscription.getByOpaqueRef(host.metrics);

View File

@@ -1,5 +1,4 @@
import { buildXoObject, parseDateTime } from "@/libs/utils";
import type { RawTypeToRecord } from "@/types/xapi-collection";
import { JSONRPCClient } from "json-rpc-2.0";
import { castArray } from "lodash-es";
@@ -175,6 +174,45 @@ export interface XenApiMessage<T extends RawObjectType = RawObjectType>
timestamp: string;
}
export type XenApiAlarmType =
| "cpu_usage"
| "disk_usage"
| "fs_usage"
| "log_fs_usage"
| "mem_usage"
| "memory_free_kib"
| "network_usage"
| "physical_utilisation"
| "sr_io_throughput_total_per_host";
export interface XenApiAlarm extends XenApiMessage {
level: number;
triggerLevel: number;
type: XenApiAlarmType;
}
export type RawTypeToRecord<T extends RawObjectType> = T extends "SR"
? XenApiSr
: T extends "VM"
? XenApiVm
: T extends "VM_guest_metrics"
? XenApiVmGuestMetrics
: T extends "VM_metrics"
? XenApiVmMetrics
: T extends "console"
? XenApiConsole
: T extends "host"
? XenApiHost
: T extends "host_metrics"
? XenApiHostMetrics
: T extends "message"
? XenApiMessage
: T extends "pool"
? XenApiPool
: T extends "task"
? XenApiTask
: never;
type WatchCallbackResult = {
id: string;
class: ObjectType;

View File

@@ -1,27 +1,33 @@
import type { XenApiAlarm } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { createSubscribe } from "@/types/xapi-collection";
import type {
DeferExtension,
Options,
Subscription,
XenApiRecordExtension,
} from "@/types/subscription";
import { defineStore } from "pinia";
import { computed } from "vue";
type Extensions = [XenApiRecordExtension<XenApiAlarm>, DeferExtension];
export const useAlarmStore = defineStore("alarm", () => {
const messageCollection = useXapiCollectionStore().get("message");
const subscribe = createSubscribe<"message", []>((options) => {
const originalSubscription = messageCollection.subscribe(options);
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const subscription = messageCollection.subscribe(options);
const extendedSubscription = {
records: computed(() =>
originalSubscription.records.value.filter(
(record) => record.name === "alarm"
)
subscription.records.value.filter((record) => record.name === "alarm")
),
};
return {
...originalSubscription,
...subscription,
...extendedSubscription,
};
});
} as Subscription<Extensions, O>;
};
return {
...messageCollection,

View File

@@ -0,0 +1,88 @@
import { isHostRunning } from "@/libs/utils";
import type {
GRANULARITY,
HostStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { XenApiHost, XenApiHostMetrics } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
Extension,
XenApiRecordExtension,
XenApiRecordSubscription,
} from "@/types/subscription";
import type { PartialSubscription } from "@/types/subscription";
import { computed } from "vue";
import type { ComputedRef } from "vue";
type GetStatsExtension = Extension<{
getStats: (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<HostStats> | undefined> | undefined;
}>;
type RunningHostsExtension = Extension<
{ runningHosts: ComputedRef<XenApiHost[]> },
{ hostMetricsSubscription: XenApiRecordSubscription<XenApiHostMetrics> }
>;
export type HostExtensions = [
XenApiRecordExtension<XenApiHost>,
GetStatsExtension,
RunningHostsExtension
];
export const getStatsSubscription = (
hostSubscription: XenApiRecordSubscription<XenApiHost>
): PartialSubscription<GetStatsExtension> => {
const xenApiStore = useXenApiStore();
return {
getStats: (
hostUuid,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const host = hostSubscription.getByUuid(hostUuid);
if (host === undefined) {
throw new Error(`Host ${hostUuid} could not be found.`);
}
const xapiStats = xenApiStore.isConnected
? xenApiStore.getXapiStats()
: undefined;
return xapiStats?._getAndUpdateStats<HostStats>({
abortSignal,
host,
ignoreExpired,
uuid: host.uuid,
granularity,
});
},
};
};
export const runningHostsSubscription = (
hostSubscription: XenApiRecordSubscription<XenApiHost>,
hostMetricsSubscription:
| XenApiRecordSubscription<XenApiHostMetrics>
| undefined
): PartialSubscription<RunningHostsExtension> | undefined => {
if (hostMetricsSubscription === undefined) {
return undefined;
}
return {
runningHosts: computed(() =>
hostSubscription.records.value.filter((host) =>
isHostRunning(host, hostMetricsSubscription)
)
),
};
};

View File

@@ -1,88 +1,28 @@
import { isHostRunning, sortRecordsByNameLabel } from "@/libs/utils";
import type {
GRANULARITY,
HostStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { XenApiHost } from "@/libs/xen-api";
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { HostExtensions } from "@/stores/host.extension";
import {
getStatsSubscription,
runningHostsSubscription,
} from "@/stores/host.extension";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import type { Subscription } from "@/types/xapi-collection";
import { createSubscribe } from "@/types/xapi-collection";
import type { Options, Subscription } from "@/types/subscription";
import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type GetStats = (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<HostStats> | undefined> | undefined;
type GetStatsExtension = {
getStats: GetStats;
};
type RunningHostsExtension = [
{ runningHosts: ComputedRef<XenApiHost[]> },
{ hostMetricsSubscription: Subscription<"host_metrics", any> }
];
type Extensions = [GetStatsExtension, RunningHostsExtension];
export const useHostStore = defineStore("host", () => {
const xenApiStore = useXenApiStore();
const hostCollection = useXapiCollectionStore().get("host");
hostCollection.setSort(sortRecordsByNameLabel);
const subscribe = createSubscribe<"host", Extensions>((options) => {
const originalSubscription = hostCollection.subscribe(options);
const subscribe = <O extends Options<HostExtensions>>(options?: O) => {
const subscription = hostCollection.subscribe(options);
const { hostMetricsSubscription } = options ?? {};
const getStats: GetStats = (
hostUuid,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const host = originalSubscription.getByUuid(hostUuid);
if (host === undefined) {
throw new Error(`Host ${hostUuid} could not be found.`);
}
const xapiStats = xenApiStore.isConnected
? xenApiStore.getXapiStats()
: undefined;
return xapiStats?._getAndUpdateStats<HostStats>({
abortSignal,
host,
ignoreExpired,
uuid: host.uuid,
granularity,
});
};
const extendedSubscription = {
getStats,
};
const hostMetricsSubscription = options?.hostMetricsSubscription;
const runningHostsSubscription = hostMetricsSubscription !== undefined && {
runningHosts: computed(() =>
originalSubscription.records.value.filter((host) =>
isHostRunning(host, hostMetricsSubscription)
)
),
};
return {
...originalSubscription,
...extendedSubscription,
...runningHostsSubscription,
};
});
...subscription,
...getStatsSubscription(subscription),
...runningHostsSubscription(subscription, hostMetricsSubscription),
} as Subscription<HostExtensions, O>;
};
return {
...hostCollection,

View File

@@ -1,31 +1,36 @@
import { getFirst } from "@/libs/utils";
import type { XenApiPool } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { createSubscribe } from "@/types/xapi-collection";
import type {
Extension,
Options,
PartialSubscription,
Subscription,
XenApiRecordExtension,
} from "@/types/subscription";
import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type PoolExtension = {
type PoolExtension = Extension<{
pool: ComputedRef<XenApiPool | undefined>;
};
}>;
type Extensions = [PoolExtension];
type Extensions = [XenApiRecordExtension<XenApiPool>, PoolExtension];
export const usePoolStore = defineStore("pool", () => {
const poolCollection = useXapiCollectionStore().get("pool");
const subscribe = <O extends Options<Extensions>>(options?: O) => {
const subscription = poolCollection.subscribe(options);
const subscribe = createSubscribe<"pool", Extensions>((options) => {
const originalSubscription = poolCollection.subscribe(options);
const extendedSubscription = {
pool: computed(() => getFirst(originalSubscription.records.value)),
const extendedSubscription: PartialSubscription<PoolExtension> = {
pool: computed(() => getFirst(subscription.records.value)),
};
return {
...originalSubscription,
...subscription,
...extendedSubscription,
};
});
} as Subscription<Extensions, O>;
};
return {
...poolCollection,

View File

@@ -0,0 +1,56 @@
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
import useCollectionFilter from "@/composables/collection-filter.composable";
import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
import type {
Extension,
PartialSubscription,
XenApiRecordExtension,
XenApiRecordSubscription,
} from "@/types/subscription";
import type { ComputedRef, Ref } from "vue";
type AdditionalTasksExtension = Extension<{
pendingTasks: ComputedRef<XenApiTask[]>;
finishedTasks: Ref<XenApiTask[]>;
}>;
export type TaskExtensions = [
XenApiRecordExtension<XenApiTask>,
AdditionalTasksExtension
];
export const additionalTasksSubscription = (
taskSubscription: XenApiRecordSubscription<XenApiTask>
): PartialSubscription<AdditionalTasksExtension> => {
const { compareFn } = useCollectionSorter<XenApiTask>({
initialSorts: ["-created"],
});
const { predicate } = useCollectionFilter({
initialFilters: [
"!name_label:|(SR.scan host.call_plugin)",
"status:pending",
],
});
const sortedTasks = useSortedCollection(taskSubscription.records, compareFn);
return {
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
finishedTasks: useArrayRemovedItemsHistory(
sortedTasks,
(task) => task.uuid,
{
limit: 50,
onRemove: (tasks) =>
tasks.map((task) => ({
...task,
finished: new Date().toISOString(),
})),
}
),
};
};

View File

@@ -1,64 +1,22 @@
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
import useCollectionFilter from "@/composables/collection-filter.composable";
import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
import {
additionalTasksSubscription,
type TaskExtensions,
} from "@/stores/task.extension";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { createSubscribe } from "@/types/xapi-collection";
import type { Options, Subscription } from "@/types/subscription";
import { defineStore } from "pinia";
import type { ComputedRef, Ref } from "vue";
type PendingTasksExtension = {
pendingTasks: ComputedRef<XenApiTask[]>;
};
type FinishedTasksExtension = {
finishedTasks: Ref<XenApiTask[]>;
};
type Extensions = [PendingTasksExtension, FinishedTasksExtension];
export const useTaskStore = defineStore("task", () => {
const tasksCollection = useXapiCollectionStore().get("task");
const subscribe = createSubscribe<"task", Extensions>(() => {
const subscription = tasksCollection.subscribe();
const { compareFn } = useCollectionSorter<XenApiTask>({
initialSorts: ["-created"],
});
const sortedTasks = useSortedCollection(subscription.records, compareFn);
const { predicate } = useCollectionFilter({
initialFilters: [
"!name_label:|(SR.scan host.call_plugin)",
"status:pending",
],
});
const extendedSubscription = {
pendingTasks: useFilteredCollection<XenApiTask>(sortedTasks, predicate),
finishedTasks: useArrayRemovedItemsHistory(
sortedTasks,
(task) => task.uuid,
{
limit: 50,
onRemove: (tasks) =>
tasks.map((task) => ({
...task,
finished: new Date().toISOString(),
})),
}
),
};
const subscribe = <O extends Options<TaskExtensions>>(options?: O) => {
const subscription = tasksCollection.subscribe(options);
return {
...subscription,
...extendedSubscription,
};
});
...additionalTasksSubscription(subscription),
} as Subscription<TaskExtensions, O>;
};
return { ...tasksCollection, subscribe };
});

View File

@@ -0,0 +1,108 @@
import type {
GRANULARITY,
VmStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import { POWER_STATE, type XenApiHost, type XenApiVm } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
Extension,
PartialSubscription,
XenApiRecordExtension,
XenApiRecordSubscription,
} from "@/types/subscription";
import { computed, type ComputedRef } from "vue";
type RecordsByHostRefExtension = Extension<{
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
}>;
type RunningVmsExtension = Extension<{
runningVms: ComputedRef<XenApiVm[]>;
}>;
type GetStatsExtension = Extension<
{
getStats: (
id: XenApiVm["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<VmStats> | undefined> | undefined;
},
{ hostSubscription: XenApiRecordSubscription<XenApiHost> }
>;
export type VmExtensions = [
XenApiRecordExtension<XenApiVm>,
RecordsByHostRefExtension,
RunningVmsExtension,
GetStatsExtension
];
export const recordsByHostRefSubscription = (
vmSubscription: XenApiRecordSubscription<XenApiVm>
): PartialSubscription<RecordsByHostRefExtension> => ({
recordsByHostRef: computed(() => {
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
vmSubscription.records.value.forEach((vm) => {
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
vmsByHostOpaqueRef.set(vm.resident_on, []);
}
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
});
return vmsByHostOpaqueRef;
}),
});
export const runningVmsSubscription = (
vmSubscription: XenApiRecordSubscription<XenApiVm>
): PartialSubscription<RunningVmsExtension> => ({
runningVms: computed(() =>
vmSubscription.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
});
export const getStatsSubscription = (
vmSubscription: XenApiRecordSubscription<XenApiVm>,
hostSubscription: XenApiRecordSubscription<XenApiHost> | undefined
): PartialSubscription<GetStatsExtension> | undefined => {
if (hostSubscription === undefined) {
return;
}
return {
getStats: (id, granularity, ignoreExpired = false, { abortSignal }) => {
const xenApiStore = useXenApiStore();
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = vmSubscription.getByUuid(id);
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
}
const host = hostSubscription.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return xenApiStore.getXapiStats()._getAndUpdateStats<VmStats>({
abortSignal,
host,
ignoreExpired,
uuid: vm.uuid,
granularity,
});
},
};
};

View File

@@ -1,36 +1,13 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import type {
GRANULARITY,
VmStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { POWER_STATE } from "@/libs/xen-api";
import {
getStatsSubscription,
recordsByHostRefSubscription,
runningVmsSubscription,
type VmExtensions,
} from "@/stores/vm.extension";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createSubscribe, type Subscription } from "@/types/xapi-collection";
import type { Options, Subscription } from "@/types/subscription";
import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type GetStats = (
id: XenApiVm["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<VmStats> | undefined> | undefined;
type DefaultExtension = {
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
runningVms: ComputedRef<XenApiVm[]>;
};
type GetStatsExtension = [
{
getStats: GetStats;
},
{ hostSubscription: Subscription<"host", object> }
];
type Extensions = [DefaultExtension, GetStatsExtension];
export const useVmStore = defineStore("vm", () => {
const vmCollection = useXapiCollectionStore().get("VM");
@@ -41,82 +18,16 @@ export const useVmStore = defineStore("vm", () => {
vmCollection.setSort(sortRecordsByNameLabel);
const subscribe = createSubscribe<"VM", Extensions>((options) => {
const originalSubscription = vmCollection.subscribe(options);
const extendedSubscription = {
recordsByHostRef: computed(() => {
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
originalSubscription.records.value.forEach((vm) => {
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
vmsByHostOpaqueRef.set(vm.resident_on, []);
}
vmsByHostOpaqueRef.get(vm.resident_on)?.push(vm);
});
return vmsByHostOpaqueRef;
}),
runningVms: computed(() =>
originalSubscription.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
};
const hostSubscription = options?.hostSubscription;
const getStatsSubscription:
| {
getStats: GetStats;
}
| undefined =
hostSubscription !== undefined
? {
getStats: (
id,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const xenApiStore = useXenApiStore();
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = originalSubscription.getByUuid(id);
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
}
const host = hostSubscription.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(
`VM ${id} is halted or host could not be found.`
);
}
return xenApiStore.getXapiStats()._getAndUpdateStats<VmStats>({
abortSignal,
host,
ignoreExpired,
uuid: vm.uuid,
granularity,
});
},
}
: undefined;
const subscribe = <O extends Options<VmExtensions>>(options?: O) => {
const subscription = vmCollection.subscribe(options);
return {
...originalSubscription,
...extendedSubscription,
...getStatsSubscription,
};
});
...subscription,
...recordsByHostRefSubscription(subscription),
...runningVmsSubscription(subscription),
...getStatsSubscription(subscription, options?.hostSubscription),
} as Subscription<VmExtensions, O>;
};
return {
...vmCollection,

View File

@@ -1,10 +1,11 @@
import type { RawObjectType } from "@/libs/xen-api";
import type { RawObjectType, RawTypeToRecord } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
RawTypeToRecord,
SubscribeOptions,
DeferExtension,
Options,
Subscription,
} from "@/types/xapi-collection";
XenApiRecordExtension,
} from "@/types/subscription";
import { tryOnUnmounted, whenever } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, readonly, ref } from "vue";
@@ -14,7 +15,7 @@ export const useXapiCollectionStore = defineStore("xapiCollection", () => {
function get<T extends RawObjectType>(
type: T
): ReturnType<typeof createXapiCollection<T, RawTypeToRecord<T>>> {
): ReturnType<typeof createXapiCollection<T>> {
if (!collections.value.has(type)) {
collections.value.set(type, createXapiCollection(type));
}
@@ -27,7 +28,7 @@ export const useXapiCollectionStore = defineStore("xapiCollection", () => {
const createXapiCollection = <
T extends RawObjectType,
R extends RawTypeToRecord<T>
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
>(
type: T
) => {
@@ -106,9 +107,11 @@ const createXapiCollection = <
() => fetchAll()
);
function subscribe<O extends SubscribeOptions<any>>(
type Extensions = [XenApiRecordExtension<R>, DeferExtension];
function subscribe<O extends Options<Extensions>>(
options?: O
): Subscription<T, O> {
): Subscription<Extensions, O> {
const id = Symbol();
tryOnUnmounted(() => {
@@ -131,14 +134,14 @@ const createXapiCollection = <
if (options?.immediate !== false) {
start();
return subscription as unknown as Subscription<T, O>;
return subscription as Subscription<Extensions, O>;
}
return {
...subscription,
start,
isStarted: computed(() => subscriptions.value.has(id)),
} as unknown as Subscription<T, O>;
} as Subscription<Extensions, O>;
}
const unsubscribe = (id: symbol) => subscriptions.value.delete(id);

View File

@@ -0,0 +1,74 @@
import type { XenApiRecord } from "@/libs/xen-api";
import type { ComputedRef, Ref } from "vue";
type SimpleExtension<Value extends object> = { type: "simple"; value: Value };
type ConditionalExtension<Value extends object, Condition extends object> = {
type: "conditional";
value: Value;
condition: Condition;
};
type UnpackExtension<E, Options> = E extends SimpleExtension<infer Value>
? Value
: E extends ConditionalExtension<infer Value, infer Condition>
? Options extends Condition
? Value
: object
: object;
export type Extension<
Value extends object,
Condition extends object | undefined = undefined
> = Condition extends object
? ConditionalExtension<Value, Condition>
: SimpleExtension<Value>;
export type Options<Extensions extends any[]> = Extensions extends [
infer First,
...infer Rest
]
? First extends ConditionalExtension<any, infer Condition>
? Rest extends any[]
? Partial<Condition> & Options<Rest>
: Partial<Condition>
: Rest extends any[]
? Options<Rest>
: object
: object;
export type Subscription<
Extensions extends any[],
Options extends object
> = Extensions extends [infer First, ...infer Rest]
? UnpackExtension<First, Options> & Subscription<Rest, Options>
: object;
export type PartialSubscription<E> = E extends SimpleExtension<infer Value>
? Value
: E extends ConditionalExtension<infer Value, any>
? Value
: never;
export type XenApiRecordExtension<T extends XenApiRecord<any>> = Extension<{
records: ComputedRef<T[]>;
getByOpaqueRef: (opaqueRef: T["$ref"]) => T | undefined;
getByUuid: (uuid: T["uuid"]) => T | undefined;
hasUuid: (uuid: T["uuid"]) => boolean;
isReady: Readonly<Ref<boolean>>;
isFetching: Readonly<Ref<boolean>>;
isReloading: ComputedRef<boolean>;
hasError: ComputedRef<boolean>;
lastError: Readonly<Ref<string | undefined>>;
}>;
export type DeferExtension = Extension<
{
start: () => void;
isStarted: ComputedRef<boolean>;
},
{ immediate: false }
>;
export type XenApiRecordSubscription<T extends XenApiRecord<any>> =
PartialSubscription<XenApiRecordExtension<T>>;

View File

@@ -1,108 +0,0 @@
import type {
RawObjectType,
XenApiConsole,
XenApiHost,
XenApiHostMetrics,
XenApiMessage,
XenApiPool,
XenApiSr,
XenApiTask,
XenApiVm,
XenApiVmGuestMetrics,
XenApiVmMetrics,
} from "@/libs/xen-api";
import type { ComputedRef, Ref } from "vue";
type DefaultExtension<
T extends RawObjectType,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
> = {
records: ComputedRef<R[]>;
getByOpaqueRef: (opaqueRef: R["$ref"]) => R | undefined;
getByUuid: (uuid: R["uuid"]) => R | undefined;
hasUuid: (uuid: R["uuid"]) => boolean;
isReady: Readonly<Ref<boolean>>;
isFetching: Readonly<Ref<boolean>>;
isReloading: ComputedRef<boolean>;
hasError: ComputedRef<boolean>;
lastError: Readonly<Ref<string | undefined>>;
};
type DeferExtension = [
{
start: () => void;
isStarted: ComputedRef<boolean>;
},
{ immediate: false }
];
type DefaultExtensions<T extends RawObjectType> = [
DefaultExtension<T>,
DeferExtension
];
type GenerateSubscribeOptions<Extensions extends any[]> = Extensions extends [
infer FirstExtension,
...infer RestExtension
]
? FirstExtension extends [object, infer FirstCondition]
? FirstCondition & GenerateSubscribeOptions<RestExtension>
: GenerateSubscribeOptions<RestExtension>
: object;
export type SubscribeOptions<Extensions extends any[]> = Partial<
GenerateSubscribeOptions<Extensions> &
GenerateSubscribeOptions<DefaultExtensions<any>>
>;
type GenerateSubscription<
Options extends object,
Extensions extends any[]
> = Extensions extends [infer FirstExtension, ...infer RestExtension]
? FirstExtension extends [infer FirstObject, infer FirstCondition]
? Options extends FirstCondition
? FirstObject & GenerateSubscription<Options, RestExtension>
: GenerateSubscription<Options, RestExtension>
: FirstExtension & GenerateSubscription<Options, RestExtension>
: object;
export type Subscription<
T extends RawObjectType,
Options extends object,
Extensions extends any[] = []
> = GenerateSubscription<Options, Extensions> &
GenerateSubscription<Options, DefaultExtensions<T>>;
export function createSubscribe<
T extends RawObjectType,
Extensions extends any[],
Options extends object = SubscribeOptions<Extensions>
>(builder: (options?: Options) => Subscription<T, Options, Extensions>) {
return function subscribe<O extends Options>(
options?: O
): Subscription<T, O, Extensions> {
return builder(options);
};
}
export type RawTypeToRecord<T extends RawObjectType> = T extends "SR"
? XenApiSr
: T extends "VM"
? XenApiVm
: T extends "VM_guest_metrics"
? XenApiVmGuestMetrics
: T extends "VM_metrics"
? XenApiVmMetrics
: T extends "console"
? XenApiConsole
: T extends "host"
? XenApiHost
: T extends "host_metrics"
? XenApiHostMetrics
: T extends "message"
? XenApiMessage
: T extends "pool"
? XenApiPool
: T extends "task"
? XenApiTask
: never;

View File

@@ -19,7 +19,9 @@ import { useTaskStore } from "@/stores/task.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
const { pendingTasks, finishedTasks, isReady, hasError } = useTaskStore().subscribe();
const { pendingTasks, finishedTasks, isReady, hasError } =
useTaskStore().subscribe();
const { t } = useI18n();
const titleStore = usePageTitleStore();