Skip to content
Merged
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
34 changes: 34 additions & 0 deletions src/stores/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,40 @@ async function postTodoCommand({ get, path, payload: { id } }: CommandRequest):
}
```

#### The `state` Object

In modern browsers, commands can also directly modify a `state` object that is passed in as part of the CommandRequest.
Any modifications to this object will be translated into the appropriate patch operations and executed against the store.
Commands that modify the state object can be synchronous or asynchronous, but if they are asynchronous they must return
a promise that will resolve when modifications are complete. Note that attempting to access `state` is not supported in IE
and will immediately throw an error.

```ts
function calculateCountsCommand = createCommand(({ state }) => {
const todos = state.todos;
const completedTodos = todos.filter((todo: any) => todo.completed);

state.activeCount = todos.length - completedTodos.length;
state.completedCount = completedTodos.length;
});

async function postTodoCommand({ state }: CommandRequest): Promise<PatchOperation[]> {
const response = await fetch('/todos');
if (!response.ok) {
throw new Error('Unable to post todo');
}
const json = await response.json();
const todos = state.todos
const index = findIndex(todos, byId(id));
// success
state.todos[index] = {
...todos[index],
loading: false,
id: json.uuid
};
}
```

### Processes

A `Process` is the construct used to execute commands against a `store` instance in order to make changes to the application state. `Processes` are created using the `createProcess` factory function that accepts an array of commands and an optional callback that can be used to manage errors thrown from a command. The optional callback receives an `error` object and a `result` object. The `error` object contains the `error` stack and the command that caused the error.
Expand Down
75 changes: 53 additions & 22 deletions src/stores/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface Path<M, T> {
value: T;
}

export type RequiredProps<T> = { [P in PurifyProps<keyof T>]: NonNullableProps<T[P]> };
export type PurifyProps<T extends string> = { [P in T]: T }[T];
export type NonNullableProps<T> = T & {};

/**
* An interface that enables typed traversal of an arbitrary type M. `path` and `at` can be used to generate
* `Path`s that allow access to properties within M via the `get` method. The returned `Path`s can also be passed to the
Expand All @@ -26,61 +30,88 @@ export interface State<M> {
}

export interface StatePaths<M> {
<T, P0 extends keyof T>(path: Path<M, T>, a: P0): Path<M, T[P0]>;
<T, P0 extends keyof T, P1 extends keyof T[P0]>(path: Path<M, T>, a: P0, b: P1): Path<M, T[P0][P1]>;
<T, P0 extends keyof T, P1 extends keyof T[P0], P2 extends keyof T[P0][P1]>(
<T, P0 extends keyof RequiredProps<T>>(path: Path<M, T>, a: P0): Path<M, RequiredProps<T>[P0]>;
<T, P0 extends keyof T, P1 extends keyof RequiredProps<T>[P0]>(path: Path<M, T>, a: P0, b: P1): Path<
M,
RequiredProps<RequiredProps<T>[P0]>[P1]
>;
<
T,
P0 extends keyof T,
P1 extends keyof RequiredProps<T>[P0],
P2 extends keyof RequiredProps<RequiredProps<T>[P0]>[P1]
>(
path: Path<M, T>,
a: P0,
b: P1,
c: P2
): Path<M, T[P0][P1][P2]>;
<T, P0 extends keyof T, P1 extends keyof T[P0], P2 extends keyof T[P0][P1], P3 extends keyof T[P0][P1][P2]>(
): Path<M, RequiredProps<RequiredProps<RequiredProps<T>[P0]>[P1]>[P2]>;
<
T,
P0 extends keyof T,
P1 extends keyof RequiredProps<T>[P0],
P2 extends keyof RequiredProps<RequiredProps<T>[P0]>[P1],
P3 extends keyof RequiredProps<RequiredProps<RequiredProps<T>[P0]>[P1]>[P2]
>(
path: Path<M, T>,
a: P0,
b: P1,
c: P2,
d: P3
): Path<M, T[P0][P1][P2][P3]>;
): Path<M, RequiredProps<RequiredProps<RequiredProps<RequiredProps<T>[P0]>[P1]>[P2]>[P3]>;
<
T,
P0 extends keyof T,
P1 extends keyof T[P0],
P2 extends keyof T[P0][P1],
P3 extends keyof T[P0][P1][P2],
P4 extends keyof T[P0][P1][P2][P3]
P1 extends keyof RequiredProps<T>[P0],
P2 extends keyof RequiredProps<RequiredProps<T>[P0]>[P1],
P3 extends keyof RequiredProps<RequiredProps<RequiredProps<T>[P0]>[P1]>[P2],
P4 extends keyof RequiredProps<RequiredProps<RequiredProps<RequiredProps<T>[P0]>[P1]>[P2]>[P3]
>(
path: Path<M, T>,
a: P0,
b: P1,
c: P2,
d: P3,
e: P4
): Path<M, T[P0][P1][P2][P3][P4]>;
<P0 extends keyof M>(a: P0): Path<M, M[P0]>;
<P0 extends keyof M, P1 extends keyof M[P0]>(a: P0, b: P1): Path<M, M[P0][P1]>;
<P0 extends keyof M, P1 extends keyof M[P0], P2 extends keyof M[P0][P1]>(a: P0, b: P1, c: P2): Path<
): Path<M, RequiredProps<RequiredProps<RequiredProps<RequiredProps<RequiredProps<T>[P0]>[P1]>[P2]>[P3]>[P4]>;
<P0 extends keyof M>(a: P0): Path<M, RequiredProps<M>[P0]>;
<P0 extends keyof M, P1 extends keyof RequiredProps<M>[P0]>(a: P0, b: P1): Path<
M,
M[P0][P1][P2]
RequiredProps<RequiredProps<M>[P0]>[P1]
>;
<P0 extends keyof M, P1 extends keyof M[P0], P2 extends keyof M[P0][P1], P3 extends keyof M[P0][P1][P2]>(
<
P0 extends keyof M,
P1 extends keyof RequiredProps<M>[P0],
P2 extends keyof RequiredProps<RequiredProps<M>[P0]>[P1]
>(
a: P0,
b: P1,
c: P2
): Path<M, RequiredProps<RequiredProps<RequiredProps<M>[P0]>[P1]>[P2]>;
<
P0 extends keyof M,
P1 extends keyof RequiredProps<M>[P0],
P2 extends keyof RequiredProps<RequiredProps<M>[P0]>[P1],
P3 extends keyof RequiredProps<RequiredProps<RequiredProps<M>[P0]>[P1]>[P2]
>(
a: P0,
b: P1,
c: P2,
d: P3
): Path<M, M[P0][P1][P2][P3]>;
): Path<M, RequiredProps<RequiredProps<RequiredProps<RequiredProps<M>[P0]>[P1]>[P2]>[P3]>;
<
P0 extends keyof M,
P1 extends keyof M[P0],
P2 extends keyof M[P0][P1],
P3 extends keyof M[P0][P1][P2],
P4 extends keyof M[P0][P1][P2][P3]
P1 extends keyof RequiredProps<M>[P0],
P2 extends keyof RequiredProps<RequiredProps<M>[P0]>[P1],
P3 extends keyof RequiredProps<RequiredProps<RequiredProps<M>[P0]>[P1]>[P2],
P4 extends keyof RequiredProps<RequiredProps<RequiredProps<RequiredProps<M>[P0]>[P1]>[P2]>[P3]
>(
a: P0,
b: P1,
c: P2,
d: P3,
e: P4
): Path<M, M[P0][P1][P2][P3][P4]>;
): Path<M, RequiredProps<RequiredProps<RequiredProps<RequiredProps<RequiredProps<M>[P0]>[P1]>[P2]>[P3]>[P4]>;
}

interface OnChangeCallback {
Expand Down
121 changes: 110 additions & 11 deletions src/stores/process.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isThenable } from '../shim/Promise';
import { PatchOperation } from './state/Patch';
import { State, Store } from './Store';
import { replace, remove } from './state/operations';
import { Path, State, Store } from './Store';
import Map from '../shim/Map';
import has from '../has/has';
import Symbol, { isSymbol } from '../shim/Symbol';

/**
* Default Payload interface
Expand All @@ -16,6 +18,7 @@ export interface DefaultPayload {
*/
export interface CommandRequest<T = any, P extends object = DefaultPayload> extends State<T> {
payload: P;
state: T;
}

/**
Expand All @@ -30,7 +33,7 @@ export interface CommandFactory<T = any, P extends object = DefaultPayload> {
* Command that returns patch operations based on the command request
*/
export interface Command<T = any, P extends object = DefaultPayload> {
(request: CommandRequest<T, P>): Promise<PatchOperation<T>[]> | PatchOperation<T>[];
(request: CommandRequest<T, P>): Promise<PatchOperation<T>[]> | PatchOperation<T>[] | void | Promise<void>;
}

/**
Expand Down Expand Up @@ -138,11 +141,14 @@ export function createCommandFactory<T, P extends object = DefaultPayload>(): Co
export type Commands<T = any, P extends object = DefaultPayload> = (Command<T, P>[] | Command<T, P>)[];

const processMap = new Map();
const valueSymbol = Symbol('value');

export function getProcess(id: string) {
return processMap.get(id);
}

const proxyError = 'State updates are not available on legacy browsers';

export function processExecutor<T = any, P extends object = DefaultPayload>(
id: string,
commands: Commands<T, P>,
Expand Down Expand Up @@ -174,29 +180,122 @@ export function processExecutor<T = any, P extends object = DefaultPayload>(
await result;
}
}

const proxies = new Map<string, any>();
const proxied = new Map<string, any>();

let proxyOperations: PatchOperation[] = [];
const createHandler = (partialPath?: Path<T, any>) => ({
get(obj: any, prop: string): any {
const fullPath = partialPath ? path(partialPath, prop) : path(prop as keyof T);
const stringPath = fullPath.path;

if (isSymbol(prop) && prop === valueSymbol) {
return proxied.get(stringPath);
}

let value = partialPath || obj.hasOwnProperty(prop) ? obj[prop] : get(fullPath);

if (typeof value === 'object' && value !== null) {
let proxiedValue;
if (!proxies.has(stringPath)) {
if (Array.isArray(value)) {
value = value.slice();
} else {
value = { ...value };
}
proxiedValue = new Proxy(value, createHandler(fullPath));
proxies.set(stringPath, proxiedValue);
proxied.set(stringPath, value);
} else {
proxiedValue = proxies.get(stringPath);
}

obj[prop] = value;
return proxiedValue;
}

obj[prop] = value;
return value;
},

set(obj: any, prop: string, value: any) {
if (typeof value === 'object' && value !== null && value[valueSymbol]) {
value = value[valueSymbol];
}

proxyOperations.push(replace(partialPath ? path(partialPath, prop) : path(prop as keyof T), value));
obj[prop] = value;

return true;
},

deleteProperty(obj: any, prop: string) {
proxyOperations.push(remove(partialPath ? path(partialPath, prop) : path(prop as keyof T)));
delete obj[prop];

return true;
}
});
let state: T;
if (typeof Proxy !== 'undefined') {
state = new Proxy({}, createHandler()) as T;
}

try {
while (command) {
let results = [];
if (Array.isArray(command)) {
results = command.map((commandFunction) => commandFunction({ at, get, path, payload }));
results = await Promise.all(results);
} else {
let result = command({ at, get, path, payload });
const commandArray = Array.isArray(command) ? command : [command];

results = commandArray.map((commandFunction: Command<T, P>) => {
let result = commandFunction({
at,
get,
path,
payload,
get state() {
if (typeof Proxy === 'undefined') {
throw new Error(proxyError);
}

return state;
}
});

if (isThenable(result)) {
result = await result;
return result.then((result) => {
result = result ? [...proxyOperations, ...result] : [...proxyOperations];
proxyOperations = [];

return result;
});
} else {
result =
result && Array.isArray(result) ? [...proxyOperations, ...result] : [...proxyOperations];
proxyOperations = [];

return result;
}
results = [result];
});
let resolvedResults: PatchOperation[][];
if (results.some(isThenable)) {
resolvedResults = await Promise.all(results);
} else {
resolvedResults = results as PatchOperation[][];
}

for (let i = 0; i < results.length; i++) {
operations.push(...results[i]);
undoOperations = [...apply(results[i]), ...undoOperations];
operations.push(...resolvedResults[i]);
undoOperations = [...apply(resolvedResults[i]), ...undoOperations];
}

store.invalidate();
command = commandsCopy.shift();
}
} catch (e) {
if (e.message === proxyError) {
throw e;
}
error = { error: e, command };
}

Expand Down
10 changes: 6 additions & 4 deletions src/stores/state/Patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,19 @@ export interface PatchResult<T = any, U = any> {
}

function add(pointerTarget: PointerTarget, value: any): any {
if (Array.isArray(pointerTarget.target)) {
pointerTarget.target.splice(parseInt(pointerTarget.segment, 10), 0, value);
let index = parseInt(pointerTarget.segment, 10);
if (Array.isArray(pointerTarget.target) && !isNaN(index)) {
pointerTarget.target.splice(index, 0, value);
} else {
pointerTarget.target[pointerTarget.segment] = value;
}
return pointerTarget.object;
}

function replace(pointerTarget: PointerTarget, value: any): any {
if (Array.isArray(pointerTarget.target)) {
pointerTarget.target.splice(parseInt(pointerTarget.segment, 10), 1, value);
let index = parseInt(pointerTarget.segment, 10);
if (Array.isArray(pointerTarget.target) && !isNaN(index)) {
pointerTarget.target.splice(index, 1, value);
} else {
pointerTarget.target[pointerTarget.segment] = value;
}
Expand Down
Loading