Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP resource management in store #5892

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Resource } from './resource'
import { Resource } from '../../types/resource'

export const isSameResource = (r1: Resource, r2: Resource): boolean => {
if (!r1 || !r2) return false
Expand Down
9 changes: 7 additions & 2 deletions packages/web-app-files/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import quickActionsImport from './quickActions'
import store from './store'
import { FilterSearch, SDKSearch } from './search'
import { bus } from 'web-pkg/src/instance'
import { archiverService, Registry } from './services'
import { archiverService, listingService, Registry } from './services'
import fileSideBars from './fileSideBars'
import routes from './routes'
import get from 'lodash-es/get'
import { PersonalLoader, FavoritesLoader } from './services/listing/loader'
import { View } from './types/view'

// just a dummy function to trick gettext tools
function $gettext(msg) {
Expand Down Expand Up @@ -90,7 +92,7 @@ export default {
navItems,
quickActions,
translations,
ready({ router: runtimeRouter, store: runtimeStore }) {
ready({ router: runtimeRouter, store: runtimeStore, sdk: runtimeSdk }) {
Registry.filterSearch = new FilterSearch(runtimeStore, runtimeRouter)
Registry.sdkSearch = new SDKSearch(runtimeStore, runtimeRouter)

Expand All @@ -104,5 +106,8 @@ export default {
runtimeStore.getters.configuration.server,
get(runtimeStore, 'getters.capabilities.files.archivers', [])
)
listingService.initialize(runtimeStore, runtimeSdk)
listingService.registerResourceLoader(View.Personal, new PersonalLoader())
listingService.registerResourceLoader(View.Favorites, new FavoritesLoader())
}
}
1 change: 1 addition & 0 deletions packages/web-app-files/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './archiver'
export * from './cache'
export * from './client'
export * from './listing'
export { default as Registry } from './registry'
2 changes: 2 additions & 0 deletions packages/web-app-files/src/services/listing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './listing'
export * from './loader/resourceLoader'
66 changes: 66 additions & 0 deletions packages/web-app-files/src/services/listing/listing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Store } from 'vuex'
import { View } from '../../types/view'
import { DavProperties, DavProperty } from 'web-pkg/src/constants'
import resource, { MutationType as ResourceMutationType } from '../../store/modules/resource'
import {
buildStoreModuleIdentifier,
MutationType as ListingMutationType
} from '../../store/modules/listing'
import OwnCloud from 'owncloud-sdk'
import { ResourceLoader } from './loader'
import { RuntimeError } from 'web-runtime/src/container/error'

export class ListingService {
store: Store<any>
sdk: OwnCloud
loaders: Map<View, ResourceLoader>

public initialize(store: Store<any>, sdk: OwnCloud): void {
this.store = store
this.sdk = sdk
this.loaders = new Map<View, ResourceLoader>()
}

public registerResourceLoader(view: View, loader: ResourceLoader) {
this.loaders.set(view, loader)
}

public async loadResources(
view: View,
path: string,
davProperties: DavProperty[] = DavProperties.Default
): Promise<void> {
if (!this.loaders.has(view)) {
throw new RuntimeError(`unknown view ${view} in listingService`)
}

// init store module for the view & path
const identifier = buildStoreModuleIdentifier(view, path)
this.store.registerModule(identifier, resource)
this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](true)
this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_PATH}`](path)

// announce new view & path in `listing` store module
this.store.mutations[`Files/listing/${ListingMutationType.SET_ACTIVE_VIEW_AND_PATH}`]({ view, path })

// load resources
const resourceNode = await this.loaders
.get(view)
.loadResources(this.sdk, path, davProperties)
.catch(e => {
console.error(e)
// TODO: set error state in store?
this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](false)
return Promise.reject(e)
})
this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_PARENT}`](resourceNode.parent)
this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_CHILDREN}`](
resourceNode.children
)

// exit `loading` state
this.store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](false)
}
}

export const listingService = new ListingService()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ResourceLoader, ResourceNode } from './resourceLoader'
import { buildResource } from '../../../helpers/resources'
import OwnCloud from 'owncloud-sdk'
import { DavProperty } from 'web-pkg/src/constants'

export class FavoritesLoader implements ResourceLoader {
async loadResources(
sdk: OwnCloud,
path: string,
davProperties: DavProperty[]
): Promise<ResourceNode> {
let resources = await sdk.files.getFavoriteFiles(davProperties)
resources = resources.map(buildResource)
return {
parent: null,
children: resources
}
}
}
3 changes: 3 additions & 0 deletions packages/web-app-files/src/services/listing/loader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './resourceLoader'
export * from './personalLoader'
export * from './favoritesLoader'
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ResourceLoader, ResourceNode } from './resourceLoader'
import { buildResource } from '../../../helpers/resources'
import OwnCloud from 'owncloud-sdk'
import { DavProperty } from 'web-pkg/src/constants'

export class PersonalLoader implements ResourceLoader {
async loadResources(
sdk: OwnCloud,
path: string,
davProperties: DavProperty[]
): Promise<ResourceNode> {
let resources = await sdk.files.list(path, 1, davProperties)
resources = resources.map(buildResource)
const currentFolder = resources.shift()
return {
parent: currentFolder,
children: resources
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Resource } from '../../../types/resource'
import { DavProperty } from 'web-pkg/src/constants'
import OwnCloud from 'owncloud-sdk'

export type ResourceNode = {
parent?: Resource
children: Resource[]
}

export interface ResourceLoader {
loadResources(sdk: OwnCloud, path: string, davProperties: DavProperty[]): Promise<ResourceNode>
}
8 changes: 5 additions & 3 deletions packages/web-app-files/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import state from './state'
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
import sidebarModule from './modules/sidebar'
import listingModule from './modules/listing'
import paginationModule from './modules/pagination'
import sidebarModule from './modules/sidebar'
const namespaced = true

export default {
Expand All @@ -13,7 +14,8 @@ export default {
actions,
mutations,
modules: {
sidebar: sidebarModule,
pagination: paginationModule
listing: listingModule,
pagination: paginationModule,
sidebar: sidebarModule
}
}
108 changes: 108 additions & 0 deletions packages/web-app-files/src/store/modules/listing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* problem statement:
* our `files` array in the vuex store of the files app is only capable of representing one state.
* multiple PROPFINDs for different folders, fired at similar times, will override each other in that state.
* the longest running propfind wins. Cancelling the requests is not solving all edge cases, as the
* PROPFINDs might finish at similar times so that cancellation is not happening anymore.
*
* idea for a solution:
* loading resources in the different views should be converted into a two step process, each:
* 1) set the activeView and activePath immediately after each navigation (SET_ACTIVE_VIEW, SET_ACTIVE_PATH)
* 2) do the PROPFIND, convert the response into resources, and set them in a dedicated folder store module.
* That store module needs to get registered dynamically. The PROPFIND and conversion might be long running.
* It's really important that the activePath is set as early as possible!
*
* the views then render the files of the store module that matches the current `activePath`.
* as a bonus, requests could be cancelled so that we avoid unnecessary store module creation.
*
* reasons for dynamically registered store modules: we need the reactivity of the individual files.
* Which comes built in with the store modules. Another approach would be to build a cache of recent
* propfind results (= response converted into a file listing). but that's not reactive by default.
*
* TODO: think about when to unregister the folder store modules. they might be big so removal should be happening as soon as not needed anymore.
* TODO: further think about different views: propfind, fetch incoming shares, fetch outgoing shares, trashbin
* TODO: the different `loadResources` methods from the views should live in a service, which then also takes care of creating the respective store module.
*/
import { GetterTree, MutationTree } from 'vuex'
import crypto from 'crypto'
import { Resource } from '../../types/resource'
import { RuntimeError } from 'web-runtime/src/container/error'

export const state = {
activeView: null,
activePath: null
}
export type State = typeof state

export enum MutationType {
SET_ACTIVE_VIEW_AND_PATH = 'SET_ACTIVE_VIEW_AND_PATH'
}
export type Mutations<State> = {
[MutationType.SET_ACTIVE_VIEW_AND_PATH](
state: State,
params: {
activeView: string
activePath: string
}
): void
}
export const mutations: MutationTree<State> & Mutations<State> = {
[MutationType.SET_ACTIVE_VIEW_AND_PATH](
state: State,
params: {
activeView: string
activePath: string
}
) {
state.activeView = params.activeView
state.activePath = params.activePath
}
}

export type Getters = {
isLoading(state: State, getters: Getters, rootState: any): boolean
getCurrentFolder(state: State, getters: Getters, rootState: any, rootGetters: any): Resource
getFiles(state: State, getters: Getters, rootState: any, rootGetters: any): Resource[]
}

export const getters: GetterTree<State, State> & Getters = {
isLoading(state, getters, rootState): boolean {
const identifier = buildStoreModuleIdentifier(state.activeView, state.activePath)
if (!hasStoreModule(state.activeView, state.activePath, rootState)) {
return false
}
return rootState[`Files/${identifier}/loading`]
},
getCurrentFolder(state, getters, rootState, rootGetters): Resource {
const identifier = buildStoreModuleIdentifier(state.activeView, state.activePath)
if (!hasStoreModule(state.activeView, state.activePath, rootState)) {
throw new RuntimeError('unknown resource')
}
return rootGetters[`Files/${identifier}/parent`]
},
getFiles(state, getters, rootState, rootGetters): Resource[] {
const identifier = buildStoreModuleIdentifier(state.activeView, state.activePath)
if (!hasStoreModule(state.activeView, state.activePath, rootState)) {
throw new RuntimeError('unknown resource')
}
return rootGetters[`Files/${identifier}/children`]
}
}

export const buildStoreModuleIdentifier = (activeView: string, activePath: string): string => {
const hash = crypto.createHash('sha256')
hash.update(activePath)
return `${activeView}_${hash.digest().toString('hex')}`
}

const hasStoreModule = (activeView: string, activePath: string, rootState: any): boolean => {
const identifier = buildStoreModuleIdentifier(activeView, activePath)
return !!rootState[`Files/${identifier}`]
}

export default {
namespaced: true,
state: () => state,
getters,
mutations
}
45 changes: 45 additions & 0 deletions packages/web-app-files/src/store/modules/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MutationTree } from 'vuex'
import { Resource } from '../../types/resource'

export const state = {
loading: true,
path: '',
parent: null,
children: []
}
export type State = typeof state

export enum MutationType {
SET_LOADING = 'SET_LOADING',
SET_PATH = 'SET_PATH',
SET_PARENT = 'SET_PARENT',
SET_CHILDREN = 'SET_CHILDREN'
}

export type Mutations<State> = {
[MutationType.SET_LOADING](state: State, loading: boolean): void
[MutationType.SET_PATH](state: State, path: string): void
[MutationType.SET_PARENT](state: State, parent: Resource): void
[MutationType.SET_CHILDREN](state: State, children: Resource[]): void
}

export const mutations: MutationTree<State> & Mutations<State> = {
[MutationType.SET_LOADING](state: State, loading: boolean): void {
state.loading = loading
},
[MutationType.SET_PATH](state: State, path: string): void {
state.path = path
},
[MutationType.SET_PARENT](state: State, parent: Resource): void {
state.parent = parent
},
[MutationType.SET_CHILDREN](state: State, children: Resource[]): void {
state.children = children
}
}

export default {
namespaced: true,
state: () => state,
mutations
}
9 changes: 9 additions & 0 deletions packages/web-app-files/src/types/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export abstract class View {
static readonly Personal: string = 'personal'
static readonly Favorites: string = 'favorites'
static readonly SharedWithMe: string = 'shared-with-me'
static readonly SharedWithOthers: string = 'shared-with-others'
static readonly SharedViaLink: string = 'shared-via-link'
static readonly PublicFiles: string = 'public-files'
static readonly TrashBin: string = 'trash-bin'
}
Loading