Skip to content

Commit

Permalink
WIP resource management in store
Browse files Browse the repository at this point in the history
  • Loading branch information
kulmann committed Oct 13, 2021
1 parent 1bbe2fe commit 892d47f
Show file tree
Hide file tree
Showing 21 changed files with 335 additions and 15 deletions.
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
10 changes: 8 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 @@ -123,7 +125,7 @@ export default {
navItems,
quickActions,
translations,
ready({ router: runtimeRouter, store: runtimeStore }) {
ready({ router: runtimeRouter, store: runtimeStore, sdk: runtimeSdk }) {
patchRouter(runtimeRouter)
Registry.filterSearch = new FilterSearch(runtimeStore, runtimeRouter)
Registry.sdkSearch = new SDKSearch(runtimeStore, runtimeRouter)
Expand All @@ -132,10 +134,14 @@ export default {
// registry that does not rely on call order, aka first register "on" and only after emit.
bus.emit('app.search.register.provider', Registry.filterSearch)
bus.emit('app.search.register.provider', Registry.sdkSearch)

// initialize services
archiverService.initialize(
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'
65 changes: 65 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,65 @@
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)
store.registerModule(identifier, resource)
store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](true)
store.mutations[`Files/${identifier}/${ResourceMutationType.SET_PATH}`](path)

// announce new view & path in `listing` store module
store.mutations[`Files/listing/${ListingMutationType.SET_ACTIVE_VIEW}`]({ 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?
store.mutations[`Files/${identifier}/${ResourceMutationType.SET_LOADING}`](false)
})
store.mutations[`Files/${identifier}/${ResourceMutationType.SET_PARENT}`](resourceNode.parent)
store.mutations[`Files/${identifier}/${ResourceMutationType.SET_CHILDREN}`](
resourceNode.children
)

// exit `loading` state
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,17 @@
import { ResourceLoader, ResourceNode } from './resourceLoader'
import { buildResource } from '../../../helpers/resources'

export class FavoritesLoader implements ResourceLoader {
async loadResources(
sdk: OwnCloud,
path: string,
davProperties: DavProperty[]
): Promise<ResourceNode> {
let resources = await this.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,18 @@
import { ResourceLoader, ResourceNode } from './resourceLoader'
import { buildResource } from '../../../helpers/resources'

export class PersonalLoader implements ResourceLoader {
async loadResources(
sdk: OwnCloud,
path: string,
davProperties: DavProperty[]
): Promise<ResourceNode> {
let resources = await this.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 = {
[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

0 comments on commit 892d47f

Please sign in to comment.