@xen-orchestra/web-core 0.34.0 → 0.35.0

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.
@@ -1,8 +1,14 @@
1
+ import {
2
+ useSseStore,
3
+ type THandleDelete,
4
+ type THandlePost,
5
+ type THandleWatching,
6
+ } from '@core/packages/remote-resource/sse.store'
1
7
  import type { ResourceContext, UseRemoteResource } from '@core/packages/remote-resource/types.ts'
2
8
  import type { VoidFunction } from '@core/types/utility.type.ts'
3
9
  import { ifElse } from '@core/utils/if-else.utils.ts'
4
10
  import { type MaybeRef, noop, useTimeoutPoll } from '@vueuse/core'
5
- import { merge } from 'lodash-es'
11
+ import { merge, remove } from 'lodash-es'
6
12
  import readNDJSONStream from 'ndjson-readablestream'
7
13
  import {
8
14
  computed,
@@ -46,6 +52,28 @@ export function defineRemoteResource<TData, TState extends object, TArgs extends
46
52
  stream?: boolean
47
53
  }): UseRemoteResource<TState, TArgs>
48
54
 
55
+ export function defineRemoteResource<
56
+ TData,
57
+ TState extends object = { data: Ref<TData> },
58
+ TArgs extends any[] = [],
59
+ >(config: {
60
+ url: string | ((...args: TArgs) => string)
61
+ initialData: () => TData
62
+ state?: (data: Ref<NoInfer<TData>>, context: ResourceContext<TArgs>) => TState
63
+ onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
64
+ onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
65
+ stream?: boolean
66
+ watchCollection: {
67
+ collectionId: string
68
+ resource: string // reactivity only on XAPI XO record for now
69
+ getIdentifier: (obj: unknown) => string
70
+ handleDelete: THandleDelete
71
+ handlePost: THandlePost
72
+ handleWatching: THandleWatching
73
+ predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => boolean
74
+ }
75
+ }): UseRemoteResource<TState, TArgs>
76
+
49
77
  export function defineRemoteResource<
50
78
  TData,
51
79
  TState extends object = { data: Ref<TData> },
@@ -55,9 +83,19 @@ export function defineRemoteResource<
55
83
  initialData?: () => TData
56
84
  state?: (data: Ref<TData>, context: ResourceContext<TArgs>) => TState
57
85
  onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
86
+ onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
58
87
  cacheExpirationMs?: number | false
59
88
  pollingIntervalMs?: number | false
60
89
  stream?: boolean
90
+ watchCollection?: {
91
+ collectionId: string
92
+ resource: string // reactivity only on XAPI XO record for now
93
+ getIdentifier: (obj: unknown) => string
94
+ handleDelete: THandleDelete
95
+ handlePost: THandlePost
96
+ handleWatching: THandleWatching
97
+ predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => boolean
98
+ }
61
99
  }) {
62
100
  const cache = new Map<
63
101
  string,
@@ -83,22 +121,56 @@ export function defineRemoteResource<
83
121
 
84
122
  const pollingInterval = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS
85
123
 
124
+ const removeData = (data: TData[], dataToRemove: any) => {
125
+ const getIdentifier = config.watchCollection?.getIdentifier ?? JSON.stringify
126
+
127
+ remove(data, item => {
128
+ if (typeof item === 'object') {
129
+ return getIdentifier(item) === getIdentifier(dataToRemove)
130
+ }
131
+
132
+ return item === dataToRemove
133
+ })
134
+ }
135
+
86
136
  const onDataReceived =
87
137
  config.onDataReceived ??
88
- ((data: Ref<TData>, receivedData: any) => {
89
- if (!config.stream || data.value === undefined) {
138
+ ((data: Ref<TData>, receivedData: any, args?: ResourceContext<TArgs>) => {
139
+ // allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
140
+ if (config.watchCollection?.predicate?.(receivedData, args) === false) {
141
+ return
142
+ }
143
+ if (data.value === undefined || (Array.isArray(data.value) && Array.isArray(receivedData))) {
90
144
  data.value = receivedData
91
145
  return
92
146
  }
93
147
 
94
- if (Array.isArray(data.value) && Array.isArray(receivedData)) {
95
- data.value.push(...receivedData)
148
+ if (Array.isArray(data.value)) {
149
+ removeData(data.value, receivedData)
150
+ data.value.push(receivedData)
96
151
  return
97
152
  }
98
153
 
99
154
  merge(data.value, receivedData)
100
155
  })
101
156
 
157
+ const onDataRemoved =
158
+ config.onDataRemoved ??
159
+ ((data: Ref<TData>, receivedData: any, args?: ResourceContext<TArgs>) => {
160
+ // allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
161
+ if (!config.watchCollection?.predicate?.(receivedData, args)) {
162
+ return
163
+ }
164
+
165
+ // for now only support `onDataRemoved` when watching XapiXoRecord collection
166
+ if (Array.isArray(data.value) && !Array.isArray(receivedData)) {
167
+ removeData(data.value, receivedData)
168
+ return
169
+ }
170
+
171
+ console.warn('onDataRemoved received but unhandled for:', receivedData)
172
+ })
173
+
102
174
  function subscribeToUrl(url: string) {
103
175
  const entry = cache.get(url)
104
176
 
@@ -184,7 +256,23 @@ export function defineRemoteResource<
184
256
  let pause: VoidFunction = noop
185
257
  let resume: VoidFunction = execute
186
258
 
187
- if (pollingInterval !== false) {
259
+ if (config.watchCollection !== undefined) {
260
+ const { collectionId, resource, handleDelete, handlePost, handleWatching } = config.watchCollection
261
+ const { watch, unwatch } = useSseStore()
262
+
263
+ pause = () => unwatch({ collectionId, resource, handleDelete })
264
+ resume = async function () {
265
+ await execute()
266
+ await watch({
267
+ collectionId,
268
+ handleWatching,
269
+ handlePost,
270
+ resource,
271
+ onDataReceived: receivedData => onDataReceived(data, receivedData, context),
272
+ onDataRemoved: receivedData => onDataRemoved(data, receivedData, context),
273
+ })
274
+ }
275
+ } else if (pollingInterval !== false) {
188
276
  const timeoutPoll = useTimeoutPoll(execute, pollingInterval, {
189
277
  immediateCallback: true,
190
278
  immediate: false,
@@ -0,0 +1,140 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, watch as watchVue } from 'vue'
3
+
4
+ type EventFn = (object: unknown) => void
5
+ export type THandlePost = (sseId: string) => Promise<string>
6
+ export type THandleDelete = (sseId: string, subscriptionId: string) => Promise<void>
7
+ export type THandleWatching = (
8
+ updateSseId: (id: string) => void,
9
+ getConfigByResource: (resource: string) =>
10
+ | {
11
+ subscriptionId: string
12
+ configs: Record<
13
+ string,
14
+ {
15
+ add: (object: unknown) => void
16
+ update: (object: unknown) => void
17
+ remove: (object: unknown) => void
18
+ }
19
+ >
20
+ }
21
+ | undefined
22
+ ) => void
23
+
24
+ export const useSseStore = defineStore('sse', () => {
25
+ const sse = ref<{ id?: string; isWatching: boolean }>({ isWatching: false })
26
+ const configsByResource: Map<
27
+ string,
28
+ {
29
+ subscriptionId: string
30
+ configs: Record<
31
+ string,
32
+ {
33
+ add: EventFn
34
+ update: EventFn
35
+ remove: EventFn
36
+ }
37
+ >
38
+ }
39
+ > = new Map()
40
+
41
+ function updateSseId(id: string) {
42
+ sse.value.id = id
43
+ }
44
+
45
+ function getConfigsByResource(resource: string) {
46
+ return configsByResource.get(resource)
47
+ }
48
+
49
+ function initializeWatcher(handleWatching: THandleWatching) {
50
+ return new Promise((resolve, reject) => {
51
+ if (sse.value.id !== undefined) {
52
+ return reject(new Error('SSE already initialized'))
53
+ }
54
+
55
+ watchVue(
56
+ sse,
57
+ value => {
58
+ if (value.id !== undefined) {
59
+ resolve(undefined)
60
+ }
61
+ },
62
+ { deep: true }
63
+ )
64
+
65
+ if (!sse.value.isWatching) {
66
+ sse.value.isWatching = true
67
+ handleWatching(updateSseId, getConfigsByResource)
68
+ }
69
+ })
70
+ }
71
+
72
+ async function watch({
73
+ collectionId,
74
+ resource,
75
+ onDataReceived,
76
+ onDataRemoved,
77
+ handlePost,
78
+ handleWatching,
79
+ }: {
80
+ collectionId: string
81
+ resource: string
82
+ onDataReceived: EventFn
83
+ onDataRemoved: EventFn
84
+ handlePost: THandlePost
85
+ handleWatching: THandleWatching
86
+ }) {
87
+ if (sse.value.id === undefined) {
88
+ await initializeWatcher(handleWatching)
89
+ }
90
+
91
+ const id = await handlePost(sse.value.id!)
92
+
93
+ const { subscriptionId, configs } = configsByResource.get(resource) ?? {}
94
+
95
+ if (subscriptionId !== undefined && id !== subscriptionId) {
96
+ throw new Error(`Previous subscription ID: ${subscriptionId} and new one: ${id} are not the same`)
97
+ }
98
+
99
+ configsByResource.set(resource, {
100
+ subscriptionId: id,
101
+ configs: {
102
+ ...configs,
103
+ [collectionId]: {
104
+ add: onDataReceived,
105
+ update: onDataReceived,
106
+ remove: onDataRemoved,
107
+ },
108
+ },
109
+ })
110
+ }
111
+
112
+ async function unwatch({
113
+ collectionId,
114
+ resource,
115
+ handleDelete,
116
+ }: {
117
+ collectionId: string
118
+ resource: string
119
+ handleDelete: THandleDelete
120
+ }) {
121
+ if (sse.value.id === undefined || !configsByResource.has(resource)) {
122
+ return
123
+ }
124
+
125
+ const { configs, subscriptionId } = configsByResource.get(resource)!
126
+ delete configs[collectionId]
127
+
128
+ if (Object.keys(configs).length === 0) {
129
+ await handleDelete(sse.value.id, subscriptionId)
130
+ configsByResource.delete(resource)
131
+ } else {
132
+ configsByResource.set(resource, {
133
+ subscriptionId,
134
+ configs,
135
+ })
136
+ }
137
+ }
138
+
139
+ return { watch, unwatch }
140
+ })
@@ -1,3 +1,5 @@
1
+ import type { XapiXoRecord, XoAlarm } from '@vates/types/'
2
+
1
3
  export type MaybeArray<T> = T | T[]
2
4
 
3
5
  export type VoidFunction = () => void
@@ -18,3 +20,7 @@ export type KeyOfByValue<T, TValue> =
18
20
  : never
19
21
 
20
22
  export type ArrayFilterPredicate<T> = (value: T, index: number, array: T[]) => boolean
23
+
24
+ export type GetRecordByType<TType extends XapiXoRecord['type'] | 'alarm'> = TType extends 'alarm'
25
+ ? XoAlarm
26
+ : Extract<XapiXoRecord, { type: TType }>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xen-orchestra/web-core",
3
3
  "type": "module",
4
- "version": "0.34.0",
4
+ "version": "0.35.0",
5
5
  "private": false,
6
6
  "exports": {
7
7
  "./*": {