repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

starfx / docs / posts
Eric Bower · 17 Aug 24

schema.md

  1---
  2title: Schema
  3description: Learn more about schamas and slices
  4toc: 2
  5---
  6
  7A schema has two primary features:
  8
  9- A fully typed state shape
 10- Reusable pieces of state management logic
 11
 12A schema must be an object. It is composed of slices of state. Slices can
 13represent any data type, however, we recommend keeping it as JSON serializable
 14as possible. Slices not only hold a value, but with it comes some handy
 15functions to:
 16
 17- Update the value
 18- Query for data within the value
 19
 20Our schema implementation was heavily inspired by [zod](https://zod.dev/).
 21
 22# Schema assumptions
 23
 24`createSchema` requires two slices by default in order for it and everything
 25inside `starfx` to function properly: `cache` and `loaders`.
 26
 27Why do we require those slices? Because if we can assume those exist, we can
 28build a lot of useful middleware and supervisors on top of that assumption. It's
 29a place for `starfx` and third-party functionality to hold their state.
 30
 31```ts
 32import { createSchema, slice } from "starfx";
 33
 34const [schema, initialState] = createSchema({
 35  cache: slice.table(),
 36  loaders: slice.loaders(),
 37});
 38```
 39
 40# Built-in slices
 41
 42As a result, the following slices should cover the most common data types and
 43associated logic.
 44
 45## `slice.any`
 46
 47This is essentially a basic getter and setter slice. You can provide the type it
 48ought to be and it has a couple functions to manage and query the value stored
 49inside of it.
 50
 51```ts
 52const [schema] = createSchema({
 53  nav: slice.any<bool>(false),
 54});
 55
 56function*() {
 57  yield* schema.update(schema.nav.set(true)); // set the value
 58  const nav = yield* select(schema.nav.select); // grab the value
 59  yield* schema.update(schema.nav.reset()); // reset value back to inititial
 60}
 61```
 62
 63## `num`
 64
 65This slice has some custom actions to manage a number value.
 66
 67```ts
 68const [schema] = createSchema({
 69  views: slice.num(0),
 70});
 71
 72function*() {
 73  yield* schema.update(schema.views.increment());
 74  yield* schema.update(schema.views.decrement());
 75  yield* schema.update(schema.views.set(100));
 76  const views = yield* select(schema.views.select);
 77  yield* schema.update(schema.views.reset()); // reset value back to inititial
 78}
 79```
 80
 81## `str`
 82
 83This slice is probably not super useful since it is essentially the same as
 84`slice.any<string>` but we could add more actions to it in the future.
 85
 86```ts
 87const [schema] = createSchema({
 88  token: slice.str(""),
 89});
 90
 91function*() {
 92  yield* schema.update(schema.token.set("1234"));
 93  const token = yield* select(schema.token.select);
 94  yield* schema.update(schema.token.reset()); // reset value back to inititial
 95}
 96```
 97
 98## `obj`
 99
100This is a specialized slice with some custom actions to deal with javascript
101objects.
102
103```ts
104const [schema] = createSchema({
105  settings: slice.obj({
106    notifications: false,
107    theme: "light",
108  }),
109});
110
111function*() {
112  yield* schema.update(schema.settings.update(theme, "dark"));
113  yield* schema.update(schema.settings.update(notifications, true));
114  const settings = yield* select(schema.settings.select);
115  yield* schema.update(schema.token.reset()); // reset value back to inititial
116  yield* schema.update(
117    schema.token.set({ notifications: true, theme: "dark" }),
118  );
119}
120```
121
122## `table`
123
124This is the more powerful and specialized slice we created. It attempts to
125mimick a database table where it holds an object:
126
127```ts
128type Table<Entity = any> = Record<string | number, Entity>;
129```
130
131The key is the entity's primary id and the value is the entity itself.
132
133```ts
134const [schema] = createSchema({
135  users: slice.table({ empty: { id: "", name: "" } }),
136});
137
138function*() {
139  const user1 = { id: "1", name: "bbob" };
140  const user2 = { id: "2", name: "tony" };
141  const user3 = { id: "3", name: "jessica" };
142  yield* schema.update(
143    schema.users.add({
144      [user1.id]: user1,
145      [user2.id]: user2,
146      [user3.id]: user3,
147    }),
148  );
149  yield* schema.update(
150    schema.users.patch({ [user1.id]: { name: "bob" } }),
151  );
152  yield* schema.update(
153    schema.users.remove([user3.id]),
154  );
155  
156  // selectors
157  yield* select(schema.users.selectTable());
158  yield* select(schema.users.selectTableAsList());
159  yield* select(schema.users.selectById({ id: user1.id }));
160  yield* select(schema.users.selectByIds({ ids: [user1.id, user2.id] }));
161
162  yield* schema.update(schema.users.reset());
163}
164```
165
166### empty
167
168When `empty` is provided to `slice.table` and we use a selector like
169`selectById` to find an entity that does **not** exist, we will return the
170`empty` value.
171
172This mimicks golang's empty values but catered towards entities. When `empty` is
173provided, we guarentee that `selectById` will return the correct state shape,
174with all the empty values that the end-developer provides.
175
176By providing a "default" entity when none exists, it promotes safer code because
177it creates stable assumptions about the data we have when performing lookups.
178The last thing we want to do is litter our view layer with optional chaining,
179because it sets up poor assumptions about the data we have.
180
181Read more about this design philosophy in my blog post:
182[Death by a thousand existential checks](https://bower.sh/death-by-thousand-existential-checks).
183
184When creating table slices, we highly recommend providing an `empty` value.
185
186Further, we also recommend creating entity factories for each entity that exists
187in your system.
188
189[Read more about entity factories.](https://bower.sh/entity-factories)
190
191## `loaders`
192
193This is a specialized database table specific to managing loaders in `starfx`.
194[Read more about loaders here](/loaders).
195
196```ts
197const [schema] = createSchema({
198  loaders: slice.loaders(),
199});
200
201function*() {
202  yield* schema.update(schema.loaders.start({ id: "my-id" }));
203  yield* schema.update(schema.loaders.success({ id: "my-id" }));
204  const loader = yield* select(schema.loaders.selectById({ id: "my-id" }));
205  console.log(loader);
206}
207```
208
209# Build your own slice
210
211We will build a `counter` slice to demonstrate how to build your own slices.
212
213```ts
214import type { AnyState } from "starfx";
215import { BaseSchema, select } from "starfx/store";
216
217export interface CounterOutput<S extends AnyState> extends BaseSchema<number> {
218  schema: "counter";
219  initialState: number;
220  increment: (by?: number) => (s: S) => void;
221  decrement: (by?: number) => (s: S) => void;
222  reset: () => (s: S) => void;
223  select: (s: S) => number;
224}
225
226export function createCounter<S extends AnyState = AnyState>(
227  { name, initialState = 0 }: { name: keyof S; initialState?: number },
228): CounterOutput<S> {
229  return {
230    name: name as string,
231    schema: "counter",
232    initialState,
233    increment: (by = 1) => (state) => {
234      (state as any)[name] += by;
235    },
236    decrement: (by = 1) => (state) => {
237      (state as any)[name] -= by;
238    },
239    reset: () => (state) => {
240      (state as any)[name] = initialState;
241    },
242    select: (state) => {
243      return (state as any)[name];
244    },
245  };
246}
247
248export function counter(initialState?: number) {
249  return (name: string) => createCounter<AnyState>({ name, initialState });
250}
251
252const [schema, initialState] = createSchema({
253  counter: counter(100),
254});
255const store = createStore(initialState);
256
257store.run(function* () {
258  yield* schema.update([
259    schema.counter.increment(),
260    schema.counter.increment(),
261  ]);
262  const result = yield* select(schema.counter.select);
263  console.log(result); // 102
264});
265```