зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1541629 - Part 1: Create new Resource utility for common Redux state operations. r=jlast
Differential Revision: https://phabricator.services.mozilla.com/D27956 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
502d99561a
Коммит
74f51f53d5
|
@ -7,6 +7,7 @@ DIRS += [
|
|||
'breakpoint',
|
||||
'editor',
|
||||
'pause',
|
||||
'resource',
|
||||
'sources-tree',
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1,302 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
import {
|
||||
mutateState,
|
||||
createOrdered,
|
||||
createUnordered,
|
||||
type Resource,
|
||||
type Id,
|
||||
type Item,
|
||||
type Fields,
|
||||
type State,
|
||||
type Order,
|
||||
type UnorderedState,
|
||||
type OrderedState,
|
||||
type MutableResource,
|
||||
type MutableFields
|
||||
} from "./core";
|
||||
|
||||
/**
|
||||
* Provide the default Redux state for an unordered store.
|
||||
*/
|
||||
export function createInitial<R: Resource>(
|
||||
fields: Fields<R>
|
||||
): UnorderedState<R> {
|
||||
return createUnordered(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the default Redux state for an ordered store.
|
||||
*/
|
||||
export function createInitialOrdered<R: Resource>(
|
||||
fields: Fields<R>
|
||||
): OrderedState<R> {
|
||||
return createOrdered(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splice a given item into the resource set at a given index.
|
||||
*/
|
||||
export function insertResourceAtIndex<R: Resource>(
|
||||
state: OrderedState<R>,
|
||||
resource: R,
|
||||
index: number
|
||||
): OrderedState<R> {
|
||||
return mutateState(
|
||||
state,
|
||||
{ resources: [resource], index },
|
||||
insertFieldsMutator,
|
||||
insertOrderMutator
|
||||
);
|
||||
}
|
||||
export function insertResourcesAtIndex<R: Resource>(
|
||||
state: OrderedState<R>,
|
||||
resources: Array<R>,
|
||||
index: number
|
||||
): OrderedState<R> {
|
||||
if (resources.length === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return mutateState(
|
||||
state,
|
||||
{ resources, index },
|
||||
insertFieldsMutator,
|
||||
insertOrderMutator
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a given item into the resource set, at the end of the order if the
|
||||
* resource is an ordered one.
|
||||
*/
|
||||
export function insertResource<R: Resource, S: State<R>>(
|
||||
state: S,
|
||||
resource: R
|
||||
): S {
|
||||
return mutateState(
|
||||
state,
|
||||
{ resources: [resource] },
|
||||
insertFieldsMutator,
|
||||
insertOrderMutator
|
||||
);
|
||||
}
|
||||
export function insertResources<R: Resource, S: State<R>>(
|
||||
state: S,
|
||||
resources: Array<R>
|
||||
): S {
|
||||
if (resources.length === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return mutateState(
|
||||
state,
|
||||
{ resources },
|
||||
insertFieldsMutator,
|
||||
insertOrderMutator
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a given item from the resource set, including any data.
|
||||
*
|
||||
* Note: This function here requires an Item because we want to encourage users
|
||||
* of this API to include the "item" type in their action payload. This allows
|
||||
* for secondary reducers that might be handling the event to have a bit more
|
||||
* metadata about an item, since they won't be able to look it up in the state.
|
||||
*/
|
||||
export function removeResource<R: Resource, S: State<R>>(
|
||||
state: S,
|
||||
item: Item<R>
|
||||
): S {
|
||||
return mutateState(state, [item], removeFieldsMutator, removeOrderMutator);
|
||||
}
|
||||
export function removeResources<R: Resource, S: State<R>>(
|
||||
state: S,
|
||||
items: Array<Item<R>>
|
||||
): S {
|
||||
if (items.length === 0) {
|
||||
return state;
|
||||
}
|
||||
return mutateState(state, items, removeFieldsMutator, removeOrderMutator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single resource value for a single resource.
|
||||
*/
|
||||
export function setFieldValue<R: Resource, K: $Keys<R>, S: State<R>>(
|
||||
state: S,
|
||||
field: K,
|
||||
id: Id<R> | Item<R>,
|
||||
value: $ElementType<R, K>
|
||||
): S {
|
||||
if (typeof id !== "string") {
|
||||
id = id.id;
|
||||
}
|
||||
|
||||
return setFieldsValue(state, field, { [id]: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single resource value for multiple resources.
|
||||
*/
|
||||
export function setFieldValues<
|
||||
R: Resource,
|
||||
K: $Keys<$Rest<Fields<R>, { item: $ElementType<Fields<R>, "item"> }>>,
|
||||
S: State<R>
|
||||
>(state: S, field: K, fieldValues: { [Id<R>]: $ElementType<R, K> }): S {
|
||||
return setFieldsValues(state, { [field]: fieldValues });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple resource values for a single resource.
|
||||
*/
|
||||
export function setFieldsValue<R: Resource, S: State<R>>(
|
||||
state: S,
|
||||
id: Id<R> | Item<R>,
|
||||
idValues: MutableResource<R>
|
||||
): S {
|
||||
if (typeof id !== "string") {
|
||||
id = id.id;
|
||||
}
|
||||
|
||||
const fields = {};
|
||||
for (const key of Object.keys(idValues)) {
|
||||
fields[key] = { [id]: idValues[key] };
|
||||
}
|
||||
|
||||
return setFieldsValues(state, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple resource values for a multiple resource.
|
||||
*/
|
||||
export function setFieldsValues<R: Resource, S: State<R>>(
|
||||
state: S,
|
||||
fields: $Shape<MutableFields<R>>
|
||||
): S {
|
||||
return mutateState(state, fields, setFieldsMutator);
|
||||
}
|
||||
|
||||
function setFieldsMutator<R: Resource>(
|
||||
newFields: $Shape<MutableFields<R>>,
|
||||
fields: Fields<R>
|
||||
): void {
|
||||
if ("item" in newFields) {
|
||||
throw new Error(
|
||||
"Resource items cannot be updated. Remove the resource and recreate " +
|
||||
"it to change the item. Ideally though you should consider creating " +
|
||||
"an entirely new ID so that items are fully unique and immutable."
|
||||
);
|
||||
}
|
||||
|
||||
for (const field of Object.keys(newFields)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(fields, field)) {
|
||||
console.error(
|
||||
`Resource fields contains the property "${field}", which is unknown. ` +
|
||||
"The resource initial value should be updated to " +
|
||||
"include the new property. If you are seeing this and get no " +
|
||||
"Flow errors, please make sure that you are not using any " +
|
||||
"optional properties in your primary resource item type and " +
|
||||
"that it is an exact object."
|
||||
);
|
||||
}
|
||||
|
||||
const fieldsValues = newFields[field];
|
||||
if (fieldsValues) {
|
||||
fields[field] = { ...fields[field] };
|
||||
|
||||
for (const id of Object.keys(fieldsValues)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(fields.item, id)) {
|
||||
throw new Error(`Resource item ${id} not found, cannot set ${field}`);
|
||||
}
|
||||
|
||||
fields[field][id] = fieldsValues[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertFieldsMutator<R: Resource>(
|
||||
{
|
||||
resources
|
||||
}: {
|
||||
resources: Array<R>,
|
||||
index?: number
|
||||
},
|
||||
fields: Fields<R>
|
||||
): void {
|
||||
for (const key of Object.keys(fields)) {
|
||||
fields[key] = { ...fields[key] };
|
||||
}
|
||||
|
||||
for (const resource of resources) {
|
||||
const { id } = resource.item;
|
||||
if (Object.prototype.hasOwnProperty.call(fields.item, id)) {
|
||||
throw new Error(`Resource item ${id} already exists`);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(fields)) {
|
||||
fields[key][id] = resource[key];
|
||||
}
|
||||
|
||||
for (const key of Object.keys(resource)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(fields, key)) {
|
||||
console.error(
|
||||
`Resource ${id} contains the property "${key}", which is unknown.` +
|
||||
"The resource initial value should be updated to " +
|
||||
"include the new property. If you are seeing this and get no " +
|
||||
"Flow errors, please make sure that you are not using any " +
|
||||
"optional properties in your primary resource item type and " +
|
||||
"that it is an exact object."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function insertOrderMutator<R: Resource>(
|
||||
{
|
||||
resources,
|
||||
index
|
||||
}: {
|
||||
resources: Array<R>,
|
||||
index?: number
|
||||
},
|
||||
order: Order<R>
|
||||
): void {
|
||||
order.splice(
|
||||
typeof index === "number" ? index : order.length,
|
||||
0,
|
||||
...resources.map(r => r.item.id)
|
||||
);
|
||||
}
|
||||
|
||||
function removeFieldsMutator<R: Resource>(
|
||||
items: Array<Item<R>>,
|
||||
fields: Fields<R>
|
||||
): void {
|
||||
for (const key of Object.keys(fields)) {
|
||||
fields[key] = { ...fields[key] };
|
||||
}
|
||||
|
||||
for (const { id } of items) {
|
||||
if (!Object.prototype.hasOwnProperty.call(fields.item, id)) {
|
||||
throw new Error(`Resource item ${id} not found, cannot remove`);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(fields)) {
|
||||
delete fields[key][id];
|
||||
}
|
||||
}
|
||||
}
|
||||
function removeOrderMutator<R: Resource>(
|
||||
items: Array<Item<R>>,
|
||||
order: Order<R>
|
||||
): Order<R> {
|
||||
const ids = new Set(items.map(item => item.id));
|
||||
return order.filter(id => ids.has(id));
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
import { getFields, type Resource, type Id, type State } from "./core";
|
||||
|
||||
/**
|
||||
* A simple wrapper on createFieldReducer to take a set of IDs and get the
|
||||
* current values for a given field, in the target resource.
|
||||
*
|
||||
* Caches results based on object identity. See createFieldReducer for
|
||||
* more information about what specific caching is performed.
|
||||
*/
|
||||
export type FieldByIDGetter<R: Resource, K: $Keys<R>> = FieldByIDMapper<
|
||||
R,
|
||||
$ElementType<R, K>
|
||||
>;
|
||||
export function createFieldByIDGetter<R: Resource, K: $Keys<R>>(
|
||||
field: K
|
||||
): FieldByIDGetter<R, K> {
|
||||
return createFieldByIDMapper(field, v => v);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper on createFieldReducer for mapping a set of IDs to
|
||||
* a set of values.
|
||||
*
|
||||
* Caches results based on object identity. See createFieldReducer for
|
||||
* more information about what specific caching is performed.
|
||||
*/
|
||||
export type FieldByIDMapper<R: Resource, T> = FieldByIDReducer<R, Array<T>>;
|
||||
export function createFieldByIDMapper<R: Resource, K: $Keys<R>, T>(
|
||||
field: K,
|
||||
mapper: (value: $ElementType<R, K>, id: Id<R>, i: number) => T
|
||||
): FieldByIDMapper<R, T> {
|
||||
return createFieldByIDReducer(
|
||||
field,
|
||||
(acc, value, id, i) => {
|
||||
acc.push(mapper(value, id, i));
|
||||
return acc;
|
||||
},
|
||||
() => []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic utility for taking a given array of IDs for a type, and loading
|
||||
* data for those IDs from a given field for that type, and then reducing
|
||||
* those values into a final result.
|
||||
*
|
||||
* If the returned callback is passed the exact same array of IDs and the
|
||||
* same state value as the previous call with those IDs, it will return
|
||||
* the exact same result as last time.
|
||||
*
|
||||
* If the returned callback is passed the exact same array of IDs and a
|
||||
* different state, we will check if all of the same IDs point an unchanged
|
||||
* values, we will also reused the previously calculated result.
|
||||
*
|
||||
* If the set of values referenced by the IDs has changed, then the reducer
|
||||
* will be executed to create a new cached value for this set of IDs.
|
||||
*/
|
||||
export type FieldByIDReducer<R: Resource, T> = (
|
||||
State<R>,
|
||||
$ReadOnlyArray<Id<R>> | Set<Id<R>>
|
||||
) => T;
|
||||
export function createFieldByIDReducer<R: Resource, K: $Keys<R>, T>(
|
||||
field: K,
|
||||
reducer: (acc: T, value: $ElementType<R, K>, id: Id<R>, i: number) => T,
|
||||
initial: () => T
|
||||
): FieldByIDReducer<R, T> {
|
||||
const cache = new WeakMap();
|
||||
const handler = (acc, { id, value }, i) => reducer(acc, value, id, i);
|
||||
|
||||
return (state, ids) => {
|
||||
const fields = getFields(state);
|
||||
const fieldValues = fields[field];
|
||||
|
||||
if (!fieldValues) {
|
||||
throw new Error(`Field "${field}" does not exist in this resource.`);
|
||||
}
|
||||
|
||||
let result = cache.get(ids);
|
||||
if (!result || result.fieldValues !== fieldValues) {
|
||||
if (!Array.isArray(ids)) {
|
||||
ids = Array.from(ids);
|
||||
}
|
||||
|
||||
const items = ids.map(id => {
|
||||
if (!Object.prototype.hasOwnProperty.call(fields.item, id)) {
|
||||
throw new Error(`Resource item ${id} not found`);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
value: fieldValues[id]
|
||||
};
|
||||
});
|
||||
|
||||
// If the resource object itself changed but we still have the same set
|
||||
// of result values, we can also reuse the previously calculated result.
|
||||
if (
|
||||
!result ||
|
||||
result.items.length !== items.length ||
|
||||
result.items.some(
|
||||
(item, i) => items[i].id !== item.id || items[i].value !== item.value
|
||||
)
|
||||
) {
|
||||
result = {
|
||||
fieldValues,
|
||||
items,
|
||||
value: items.reduce(handler, initial())
|
||||
};
|
||||
cache.set(ids, result);
|
||||
} else {
|
||||
result.fieldValues = fieldValues;
|
||||
}
|
||||
}
|
||||
|
||||
return result.value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a memozed result based on all of the values for a given field.
|
||||
*/
|
||||
export type FieldReducer<R: Resource, T> = (State<R>) => T;
|
||||
export function createFieldReducer<R: Resource, K: $Keys<R>, T>(
|
||||
field: K,
|
||||
reducer: (acc: T, value: $ElementType<R, K>, id: Id<R>) => T,
|
||||
initial: () => T
|
||||
): FieldReducer<R, T> {
|
||||
const cache = new WeakMap();
|
||||
|
||||
return state => {
|
||||
const fieldValues = getFields(state)[field];
|
||||
|
||||
if (cache.has(fieldValues)) {
|
||||
// Flow isn't quite smary enough to know that "has" returning true means
|
||||
// that we'll always get the expected value, so we have to cast to 'any'.
|
||||
// We specifically use 'has' because we want be flexible and allow the
|
||||
// 'reducer' function to return 'undefined' if it wants to.
|
||||
return (cache.get(fieldValues): any);
|
||||
}
|
||||
|
||||
let result = initial();
|
||||
for (const id of Object.keys(fieldValues)) {
|
||||
result = reducer(result, fieldValues[id], id);
|
||||
}
|
||||
|
||||
cache.set(fieldValues, result);
|
||||
return result;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* This file defines the root resource datatypes and functions to manipulate
|
||||
* them. We specifically want these types to be opaque values because we want
|
||||
* to restrict access to them to the set of functions exposed specifically
|
||||
* for manipulation in order to promote good methods of access and good
|
||||
* memoization logic.
|
||||
*/
|
||||
|
||||
type PropertiesById<ID, T> = $Exact<$ObjMap<T, <V>(v: V) => { [ID]: V }>>;
|
||||
|
||||
export type Resource = {
|
||||
+item: { +id: string }
|
||||
};
|
||||
export type Value<R: Resource, K: $Keys<R>> = $ElementType<R, K>;
|
||||
export type Item<R: Resource> = Value<R, "item">;
|
||||
export type Id<R: Resource> = $ElementType<Item<R>, "id">;
|
||||
|
||||
export type Order<R: Resource> = Array<Id<R>>;
|
||||
export type ReadOnlyOrder<R: Resource> = $ReadOnlyArray<Id<R>>;
|
||||
|
||||
export type Fields<R: Resource> = {|
|
||||
// This spread clears the covariance on 'item'.
|
||||
...PropertiesById<Id<R>, R>
|
||||
|};
|
||||
export type ReadOnlyFields<R: Resource> = $ReadOnly<Fields<R>>;
|
||||
|
||||
// The resource type without "item" so it is all the types you're allow to
|
||||
// mutate via the resource API.
|
||||
export type MutableResource<R: Resource> = $Rest<
|
||||
R,
|
||||
{ item: $ElementType<R, "item"> }
|
||||
>;
|
||||
export type MutableFields<R: Resource> = PropertiesById<
|
||||
Id<R>,
|
||||
MutableResource<R>
|
||||
>;
|
||||
|
||||
// The general structure of a state store. Each top-level property of the
|
||||
// UnorderedState type will be stored as its own key/value object since the
|
||||
// types will likely mutate independent of one another and this approach
|
||||
// allows us to easily memoize values based on Redux selectors.
|
||||
export opaque type UnorderedState<R: Resource> = {
|
||||
fields: ReadOnlyFields<R>
|
||||
};
|
||||
|
||||
// An ordered version of the state where order is represented
|
||||
// as an array alongside the item lookup so that reordering items
|
||||
// does not need to affect any results that may have been computed
|
||||
// over the items as a whole ignoring sort order.
|
||||
export opaque type OrderedState<R: Resource> = {
|
||||
order: ReadOnlyOrder<R>,
|
||||
fields: ReadOnlyFields<R>
|
||||
};
|
||||
|
||||
export type State<R: Resource> = OrderedState<R> | UnorderedState<R>;
|
||||
|
||||
/**
|
||||
* Provide the default Redux state for an unordered store.
|
||||
*/
|
||||
export function createUnordered<R: Resource>(
|
||||
fields: Fields<R>
|
||||
): UnorderedState<R> {
|
||||
if (Object.keys(fields.item).length !== 0) {
|
||||
throw new Error("The initial 'fields' object should be empty.");
|
||||
}
|
||||
|
||||
return {
|
||||
fields
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the default Redux state for an ordered store.
|
||||
*/
|
||||
export function createOrdered<R: Resource>(fields: Fields<R>): OrderedState<R> {
|
||||
if (Object.keys(fields.item).length !== 0) {
|
||||
throw new Error("The initial 'fields' object should be empty.");
|
||||
}
|
||||
|
||||
return {
|
||||
order: [],
|
||||
fields
|
||||
};
|
||||
}
|
||||
|
||||
export function mutateState<R: Resource, S: State<R>, T>(
|
||||
state: S,
|
||||
arg: T,
|
||||
fieldsMutator: (T, Fields<R>) => void,
|
||||
orderMutator?: (T, Order<R>) => Order<R> | void
|
||||
): S {
|
||||
// $FlowIgnore - Flow can't quite tell that this is a valid way to copy state.
|
||||
state = { ...state };
|
||||
|
||||
state.fields = { ...state.fields };
|
||||
|
||||
fieldsMutator(arg, state.fields);
|
||||
|
||||
if (orderMutator && state.order) {
|
||||
const mutableOrder = state.order.slice();
|
||||
|
||||
state.order = orderMutator(arg, mutableOrder) || mutableOrder;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function getOrder<R: Resource>(
|
||||
state: OrderedState<R>
|
||||
): ReadOnlyOrder<R> {
|
||||
return state.order;
|
||||
}
|
||||
|
||||
export function getFields<R: Resource>(state: State<R>): ReadOnlyFields<R> {
|
||||
return state.fields;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
import type { UnorderedState, OrderedState } from "./core";
|
||||
export type {
|
||||
UnorderedState as ResourceState,
|
||||
OrderedState as OrderedResourceState
|
||||
};
|
||||
export type {
|
||||
FieldByIDGetter,
|
||||
FieldByIDMapper,
|
||||
FieldByIDReducer,
|
||||
FieldReducer
|
||||
} from "./caching";
|
||||
|
||||
export {
|
||||
createInitial,
|
||||
createInitialOrdered,
|
||||
insertResourceAtIndex,
|
||||
insertResourcesAtIndex,
|
||||
insertResource,
|
||||
insertResources,
|
||||
removeResource,
|
||||
removeResources,
|
||||
setFieldsValue,
|
||||
setFieldsValues,
|
||||
setFieldValue,
|
||||
setFieldValues
|
||||
} from "./actions";
|
||||
export {
|
||||
hasResource,
|
||||
getResource,
|
||||
listItems,
|
||||
getItem,
|
||||
getFieldValue
|
||||
} from "./selectors";
|
||||
export {
|
||||
createFieldByIDGetter,
|
||||
createFieldByIDMapper,
|
||||
createFieldByIDReducer,
|
||||
createFieldReducer
|
||||
} from "./caching";
|
|
@ -0,0 +1,15 @@
|
|||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
]
|
||||
|
||||
CompiledModules(
|
||||
'actions.js',
|
||||
'caching.js',
|
||||
'core.js',
|
||||
'index.js',
|
||||
'selectors.js'
|
||||
)
|
|
@ -0,0 +1,112 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
import {
|
||||
getOrder,
|
||||
getFields,
|
||||
type Resource,
|
||||
type Id,
|
||||
type Item,
|
||||
type OrderedState,
|
||||
type State
|
||||
} from "./core";
|
||||
|
||||
/**
|
||||
* Verify that a resource exists with the given ID.
|
||||
*/
|
||||
export function hasResource<R: Resource>(
|
||||
state: State<R>,
|
||||
id: Id<R> | Item<R>
|
||||
): boolean {
|
||||
if (typeof id !== "string") {
|
||||
id = id.id;
|
||||
}
|
||||
|
||||
return Object.prototype.hasOwnProperty.call(getFields(state).item, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the overall Resource value for a given ID.
|
||||
*
|
||||
* When using this function, please consider whether it may make more sense
|
||||
* to create a cached selector using one of the 'createFieldXXXXXXX' helpers.
|
||||
*/
|
||||
export function getResource<R: Resource>(
|
||||
state: State<R>,
|
||||
id: Id<R> | Item<R>
|
||||
): R {
|
||||
if (typeof id !== "string") {
|
||||
id = id.id;
|
||||
}
|
||||
|
||||
if (!hasResource(state, id)) {
|
||||
throw new Error(`Resource ${id} not found`);
|
||||
}
|
||||
const fields = getFields(state);
|
||||
|
||||
// We have to do some typecasting here because while we know that the the
|
||||
// types are the same, Flow isn't quite able to guarantee that the returned
|
||||
// Resource here will actually match the type.
|
||||
const resource: { [string]: mixed } = {};
|
||||
for (const key of Object.keys(fields)) {
|
||||
resource[key] = fields[key][id];
|
||||
}
|
||||
return (resource: any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all of the items in an ordered resource.
|
||||
*/
|
||||
const listCache: WeakMap<Array<mixed>, Array<mixed>> = new WeakMap();
|
||||
export function listItems<R: Resource>(state: OrderedState<R>): Array<Item<R>> {
|
||||
const order = getOrder(state);
|
||||
|
||||
// Note: The 'any' casts here are necessary because Flow has no way to know
|
||||
// that the 'R' type of 'order' and 'items' is required to match.
|
||||
let items: Array<Item<R>> | void = (listCache.get((order: any)): any);
|
||||
if (!items) {
|
||||
// Since items can't be changed, the order array's identity can fully
|
||||
// cache this item load. Changes to items would have changed the order too.
|
||||
items = order.map(id => getItem(state, id));
|
||||
listCache.set((order: any), (items: any));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export function getItem<R: Resource>(state: State<R>, id: Id<R>): Item<R> {
|
||||
if (!hasResource(state, id)) {
|
||||
throw new Error(`Resource item ${id} not found`);
|
||||
}
|
||||
|
||||
return getFields(state).item[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a given item in the resource based on its ID. Throws if the item
|
||||
* is not found.
|
||||
*/
|
||||
export function getFieldValue<R: Resource, K: $Keys<R>>(
|
||||
state: State<R>,
|
||||
field: K,
|
||||
id: Id<R> | Item<R>
|
||||
): $ElementType<R, K> {
|
||||
if (typeof id !== "string") {
|
||||
id = id.id;
|
||||
}
|
||||
|
||||
if (!hasResource(state, id)) {
|
||||
throw new Error(`Resource item ${id} not found`);
|
||||
}
|
||||
const fieldValues = getFields(state)[field];
|
||||
if (!fieldValues) {
|
||||
throw new Error(`Resource corrupt: Field "${field}" not found`);
|
||||
}
|
||||
|
||||
// We don't bother to check if this ID exists in 'values' because if it
|
||||
// does not, and things typechecked, then it means that the inserted
|
||||
// resource was missing that key and returning undefined is fine.
|
||||
return fieldValues[id];
|
||||
}
|
Загрузка…
Ссылка в новой задаче