зеркало из 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',
|
'breakpoint',
|
||||||
'editor',
|
'editor',
|
||||||
'pause',
|
'pause',
|
||||||
|
'resource',
|
||||||
'sources-tree',
|
'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];
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче