Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ func main() {
Href: "/fx",
Page: pager("fx.md"),
},
{
Text: "Managing Resources",
Href: "/managing-resources",
Page: pager("managing-resources.md"),
},
{
Text: "Error Handling",
Href: "/error-handling",
Expand Down
4 changes: 2 additions & 2 deletions docs/posts/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ export const updateUser = api.post<{ id: string; name: string }>(
body: JSON.stringify({ name: ctx.payload.name }),
});
yield* next();
},
}
);

const store = createStore(initialState);
store.run(api.register);
store.initialize(api.register);

store.dispatch(fetchUsers());
// now accessible with useCache(fetchUsers)
Expand Down
2 changes: 1 addition & 1 deletion docs/posts/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const fetchRepo = api.get(
api.cache(),
);

store.run(api.register);
store.initialize(api.register);

function App() {
return (
Expand Down
66 changes: 66 additions & 0 deletions docs/posts/managing-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Managing resources
description: How to use .manage() to register effection resources with store/thunks/api
---

# Managed resources ✅

`starfx` supports managing Effection `resource`s and exposing them via a `Context` using the `.manage()` helper on `store`, `thunks` (from `createThunks`) and `api` (from `createApi`). The `manage` call will start the resource inside the scope and return a `Context` you can `get()` or `expect()` inside your operations to access the provided value.

A `resource` is useful in encapsulating logic or functionality. It is particularly useful for managing connections such as a WebSocket connections, Web Workers, auth or telemetry. These processes can be wired up, including failure, restart and shutdown logic, and then used in any of your actions. See the [`effectionx` repo](https://github.com/thefrontside/effectionx) for published packages around an effection `resource` and examples.

Note: when using this API at the store, you need to use the new `store.initialize()` API to ensure that the resources are properly started. If you don't make use of `store.manage()`, this is not a required at this time, but `store.run()` for initial startup will be deprecated in the future preferring `store.initialize()`.

Example (contrived) pattern:

```ts
import { resource } from "effection";

function guessAge(): Operation<{ guess: number; cumulative: number | null }> {
return resource(function* (provide) {
let cumulative: number | null = 0;
try {
// this wouldn't be valuable per se, but demonstrates how the functionality is exposed
yield* provide({
get guess() {
const n = Math.floor(Math.random() * 100);
if (cumulative !== null) cumulative += n;
return n;
},
get cumulative() {
return cumulative;
},
});
} finally {
// cleanup when the resource is closed
cumulative = null;
}
});
}
```

Manage the resource:

```ts
// on a `store`:
const store = createStore({ initialState: {} });
const GuesserCtx = store.manage("guesser", guessAge());
// Or with `createThunks` (the pattern is the same for `createApi`):
const thunks = createThunks();
const GuesserCtx = thunks.manage("guesser", guessAge());

// inside an operation (thunk, middleware, etc.)
const action = thunks.create("do-thing", function* (ctx, next) {
// use the managed resource inside an action
const g = yield* GuesserCtx.get(); // may return undefined
const g2 = yield* GuesserCtx.expect(); // will throw if resource is not available

console.log(g2.guess, g2.cumulative);
yield* next();
});

store.initilize(thunks.register);
store.dispatch(action());
```

This API exists both at the overall store "level" and at the thunk "level". Resources managed at the store level are available in all registered thunks/apis whereas a resource managed at a thunk is _only available_ in that thunk. This would, for example, allow you to only enable auth for a single `createAPI()` subset of thunks.
2 changes: 1 addition & 1 deletion docs/posts/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ const [schema, initialState] = createSchema({
});
const store = createStore(initialState);

store.run(function* () {
store.initialize(function* () {
yield* schema.update([
schema.counter.increment(),
schema.counter.increment(),
Expand Down
2 changes: 1 addition & 1 deletion docs/posts/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const fetchUsers = api.get<never, User[]>(
);

const store = createStore(schema);
store.run(api.register);
store.initialize(api.register);
store.dispatch(fetchUsers());
```

Expand Down
2 changes: 1 addition & 1 deletion examples/basic/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const fetchRepo = api.get(
api.cache(),
);

store.run(api.register);
store.initialize(api.register);

function App() {
return (
Expand Down
22 changes: 12 additions & 10 deletions examples/parcel-react/src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { createStore, take } from "starfx";
import { createStore, take, parallel } from "starfx";
import { Provider } from "starfx/react";
import { api, initialState, schema } from "./api.js";
import { App } from "./app.jsx";
Expand All @@ -11,15 +11,17 @@ function init() {
const store = createStore({ initialState });
window.fx = store;

store.run([
function* logger() {
while (true) {
const action = yield* take("*");
console.log("action", action);
}
},
api.register,
]);
store.initialize(() =>
parallel([
function* logger() {
while (true) {
const action = yield* take("*");
console.log("action", action);
}
},
api.register,
]),
);

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
Expand Down
25 changes: 11 additions & 14 deletions examples/tests-rtl/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,17 @@ api.use(mdw.api({ schema }));
api.use(api.routes());
api.use(mdw.fetch({ baseUrl: "https://jsonplaceholder.typicode.com" }));

export const fetchUsers = api.get(
"/users",
function* (ctx, next) {
yield* next();
export const fetchUsers = api.get("/users", function* (ctx, next) {
yield* next();

if (!ctx.json.ok) {
return;
}
if (!ctx.json.ok) {
return;
}

const users = ctx.json.value.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
const users = ctx.json.value.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});

yield* schema.update(schema.users.add(users));
},
);
yield* schema.update(schema.users.add(users));
});
2 changes: 1 addition & 1 deletion examples/tests-rtl/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function setupStore({ initialState = {} }) {
},
});

store.run(api.register);
store.initialize(api.register);

return store;
}
22 changes: 12 additions & 10 deletions examples/vite-react/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { createStore, take } from "starfx";
import { createStore, take, parallel } from "starfx";
import { Provider } from "starfx/react";
import { api, initialState, schema } from "./api.ts";
import App from "./App.tsx";
Expand All @@ -13,15 +13,17 @@ function init() {
// makes `fx` available in devtools
(window as any).fx = store;

store.run([
function* logger() {
while (true) {
const action = yield* take("*");
console.log("action", action);
}
},
api.register,
]);
store.initialize(() =>
parallel([
function* logger() {
while (true) {
const action = yield* take("*");
console.log("action", action);
}
},
api.register,
]),
);

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"execa": "^9.6.0",
"effection": "4.0.2",
"nock": "^14.0.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
Expand Down
4 changes: 2 additions & 2 deletions src/query/thunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,11 @@ export function createThunks<Ctx extends ThunkCtx = ThunkCtx<any>>(
function manage<Resource>(name: string, inputResource: Operation<Resource>) {
const CustomContext = createContext<Resource>(name);
function curVisor(scope: Scope) {
return function* () {
return supervise(function* () {
const providedResource = yield* inputResource;
scope.set(CustomContext, providedResource);
yield* suspend();
};
});
}

watch.send(curVisor);
Expand Down
40 changes: 36 additions & 4 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import { any } from "./slice/any.js";
import { loaders } from "./slice/loaders.js";
import { num } from "./slice/num.js";
import { obj } from "./slice/obj.js";
import { str } from "./slice/str.js";
import { table } from "./slice/table.js";

export { createSchema } from "./schema.js";
export {
createStore,
configureStore,
IdContext,
type CreateStore,
} from "./store.js";
export const slice = {
str,
num,
table,
any,
obj,
loaders,
/**
* @deprecated Use `slice.loaders` instead
*/
loader: loaders,
};

export { defaultLoader, defaultLoaderItem } from "./slice/loaders.js";
export type { AnyOutput } from "./slice/any.js";
export type { LoaderOutput } from "./slice/loaders.js";
export type { NumOutput } from "./slice/num.js";
export type { ObjOutput } from "./slice/obj.js";
export type { StrOutput } from "./slice/str.js";
export type { TableOutput } from "./slice/table.js";

export * from "./types.js";
export * from "./context.js";
export * from "./fx.js";
export * from "./store.js";
export * from "./types.js";
export { createSelector } from "reselect";
export * from "./slice/index.js";
export * from "./schema.js";
export * from "./batch.js";
export * from "./persist.js";
10 changes: 5 additions & 5 deletions src/store/schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { updateStore } from "./fx.js";
import { slice } from "./slice/index.js";
import { loaders } from "./slice/loaders.js";
import { table } from "./slice/table.js";
import type { FxMap, FxSchema, StoreUpdater } from "./types.js";

const defaultSchema = <O>(): O =>
({ cache: slice.table(), loaders: slice.loaders() }) as O;

/**
* Creates a schema object and initial state from slice factories.
*
Expand Down Expand Up @@ -74,7 +72,9 @@ const defaultSchema = <O>(): O =>
export function createSchema<
O extends FxMap,
S extends { [key in keyof O]: ReturnType<O[key]>["initialState"] },
>(slices: O = defaultSchema<O>()): [FxSchema<S, O>, S] {
>(
slices: O = { cache: table(), loaders: loaders() } as O,
): [FxSchema<S, O>, S] {
const db = Object.keys(slices).reduce<FxSchema<S, O>>(
(acc, key) => {
(acc as any)[key] = slices[key](key);
Expand Down
33 changes: 0 additions & 33 deletions src/store/slice/index.ts

This file was deleted.

Loading
Loading