-
Notifications
You must be signed in to change notification settings - Fork 59
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
Don't require local resources and getters to have globally unique names #1051
Open
matthew-white
wants to merge
2
commits into
master
Choose a base branch
from
local-resource-names
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,51 +9,26 @@ https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, | |
including this file, may be copied, modified, propagated, or distributed | ||
except according to the terms contained in the LICENSE file. | ||
*/ | ||
import { computed, inject, onUnmounted, provide, shallowReactive } from 'vue'; | ||
import { defineStore } from 'pinia'; | ||
import { computed, inject, onUnmounted, provide } from 'vue'; | ||
|
||
import createResources from './resources'; | ||
import { createResource, resourceView } from './resource'; | ||
|
||
const createState = () => | ||
shallowReactive({ data: null, awaitingResponse: false }); | ||
const patchMock = (f) => { f(); }; | ||
const defineResourceStore = false // eslint-disable-line no-constant-condition | ||
? (name) => defineStore(`requestData.${name}`, { state: createState }) | ||
: () => { | ||
const store = createState(); | ||
store.$patch = patchMock; | ||
return () => store; | ||
}; | ||
|
||
const composableKey = Symbol('requestData composable'); | ||
|
||
export const createRequestData = (container) => { | ||
// `resources` holds both app-wide resources and local resources. | ||
const resources = new Map(); | ||
/* `stores` holds all the Pinia stores that have ever been created for a | ||
resource. Even after a local resource is removed from `resources` (when the | ||
component is unmounted), the resource's underlying store is not removed from | ||
`stores`. That's because we will reuse the store if another local resource is | ||
created with the same name. Doing so minimizes the number of stores shown in | ||
Vue Devtools. */ | ||
const stores = new Map(); | ||
|
||
// Create app-wide resources. | ||
const requestData = { resources: new Set() }; | ||
const { pinia } = container; | ||
createResources(container, (name, setup) => { | ||
const useStore = defineResourceStore(name); | ||
stores.set(name, useStore); | ||
const resource = createResource(container, name, useStore(pinia), setup); | ||
resources.set(name, resource); | ||
if (name in requestData) throw new Error(`the name ${name} is in use`); | ||
const resource = createResource(container, name, setup); | ||
requestData[name] = resource; | ||
requestData.resources.add(resource); | ||
return resource; | ||
}); | ||
|
||
// Checking the collective state of multiple resources | ||
requestData.resourceStates = (resources) => ({ // eslint-disable-line no-shadow | ||
requestData.resourceStates = (resources) => ({ | ||
// Returns `true` if a group of resources is being initially loaded (not | ||
// refreshed). Returns `false` if there is a resource that is not loading | ||
// and also does not have data, because presumably its request resulted in | ||
|
@@ -64,83 +39,55 @@ export const createRequestData = (container) => { | |
dataExists: computed(() => resources.every(resource => resource.dataExists)) | ||
}); | ||
|
||
// Local resources | ||
// Add a mechanism to create local resources. Local resources do not have to | ||
// have names that are globally unique: different local resources can have the | ||
// same name at the same time. The localResources object keys by name, but it | ||
// is only used in testing and only in situations where that should be OK. | ||
const localResources = Object.create(null); | ||
const createLocalResource = (name, setup) => { | ||
// Even local resources cannot share a name at the same time. That's because | ||
// the name is used for the id of the store. | ||
if (resources.has(name)) throw new Error(`the name ${name} is in use`); | ||
if (!stores.has(name)) { | ||
const useStore = defineResourceStore(name); | ||
stores.set(name, useStore); | ||
} | ||
const useStore = stores.get(name); | ||
const resource = createResource(container, name, useStore(), setup); | ||
resources.set(name, resource); | ||
const resource = createResource(container, name, setup); | ||
// We provide the local resource; only descendant components will be able to | ||
// inject it. | ||
provide(`requestData.localResources.${name}`, resource); | ||
localResources[name] = resource; | ||
|
||
onUnmounted(() => { | ||
/* | ||
Resetting the resource will reset the state of the store, allowing the | ||
store to be reused if a local resource is created later with the same | ||
name. | ||
|
||
It's because we reset the resource that we use an `unmounted` hook rather | ||
than a beforeUnmount hook. If we used a beforeUnmount hook, then if the | ||
component used watchSyncEffect() with the resource, the watchSyncEffect() | ||
callback would be run after the resource was reset. That seems surprising, | ||
and accounting for that possibility could add complexity to the | ||
watchSyncEffect() callback and the component. (That said, using an | ||
`unmounted` hook does end up adding complexity to AsyncRoute.) | ||
*/ | ||
resource.reset(); | ||
resources.delete(name); | ||
// The resource is no longer needed, so we cancel any request still in | ||
// progress. We use onUnmounted() rather than onBeforeUnmount() in case | ||
// the component is watching resource.awaitingResponse synchronously for | ||
// some reason. We don't want to trigger a component watcher while the | ||
// component is being unmounted. | ||
resource.cancelRequest(); | ||
|
||
// Remove `resource` from localResources unless it has already been | ||
// replaced. | ||
if (localResources[name] === resource) delete localResources[name]; | ||
}); | ||
|
||
return resource; | ||
}; | ||
if (process.env.NODE_ENV === 'test') { | ||
// Normally, requestData only provides access to app-wide resources. | ||
// However, in testing, we sometimes need access outside components to local | ||
// resources. | ||
requestData.localResources = new Proxy({}, { | ||
get: (_, prop) => { | ||
const resource = resources.get(prop); | ||
return resource != null && !requestData.resources.has(resource) | ||
? resource | ||
: undefined; | ||
} | ||
}); | ||
} | ||
// Normally, requestData only provides access to app-wide resources. However, | ||
// we allow tests to access local resources in order to make assertions about | ||
// them. | ||
if (process.env.NODE_ENV === 'test') | ||
requestData.localResources = localResources; | ||
|
||
// Getters allow you to create a computed ref in one component, then access it | ||
// from useRequestData() in a descendant component. Getters are only really | ||
// needed for computed refs that reference multiple resources. | ||
const getters = new Map(); | ||
const getters = Object.create(null); | ||
const createGetter = (name, ref) => { | ||
// Unlike with resources, there's no real problem with different getters | ||
// using the same name. Supporting that case would just make managing | ||
// `getters` harder. | ||
if (getters.has(name)) throw new Error(`the name ${name} is in use`); | ||
getters.set(name, ref); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getters[name] = ref; |
||
provide(`requestData.getters.${name}`, ref); | ||
onUnmounted(() => { getters.delete(name); }); | ||
onUnmounted(() => { if (getters[name] === ref) delete getters[name]; }); | ||
return ref; | ||
}; | ||
|
||
// useRequestData() | ||
const getResource = (name) => { | ||
const resource = resources.get(name); | ||
if (resource == null) return null; | ||
// If the resource is a local resource, check whether it was created by an | ||
// ancestor component. | ||
if (!requestData.resources.has(resource) && | ||
inject(`requestData.localResources.${name}`) == null) | ||
return null; | ||
const useStore = stores.get(name); | ||
useStore(); | ||
return resource; | ||
const localResource = inject(`requestData.localResources.${name}`, null); | ||
if (localResource != null) return localResource; | ||
const value = requestData[name]; | ||
return requestData.resources.has(value) ? value : null; | ||
}; | ||
const composable = new Proxy( | ||
{ | ||
|
@@ -153,29 +100,21 @@ export const createRequestData = (container) => { | |
createGetter | ||
}, | ||
{ | ||
get: (target, prop) => { | ||
const resource = getResource(prop); | ||
if (resource != null) return resource; | ||
|
||
if (getters.has(prop)) { | ||
const getter = inject(`requestData.getters.${prop}`); | ||
if (getter != null) return getter; | ||
} | ||
|
||
return requestData[prop] != null ? requestData[prop] : target[prop]; | ||
} | ||
get: (target, prop) => getResource(prop) ?? | ||
inject(`requestData.getters.${prop}`, null) ?? | ||
requestData[prop] ?? | ||
target[prop] | ||
} | ||
); | ||
|
||
requestData.install = (app) => { | ||
app.provide(composableKey, composable); | ||
|
||
// Provide local resources and getters for testing. | ||
for (const [name, resource] of resources.entries()) { | ||
if (!requestData.resources.has(resource)) | ||
app.provide(`requestData.localResources.${name}`, resource); | ||
} | ||
for (const [name, getter] of getters.entries()) | ||
// Provide local resources and getters to a component being mounted in | ||
// testing. | ||
for (const [name, resource] of Object.entries(localResources)) | ||
app.provide(`requestData.localResources.${name}`, resource); | ||
for (const [name, getter] of Object.entries(getters)) | ||
app.provide(`requestData.getters.${name}`, getter); | ||
}; | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we even need getters anymore? They seem rarely used. We could probably just use
provide
/inject
instead. Let's see about removing them in a follow-up PR.