Compare commits
64 Commits
reffuse-ne
...
6526953c37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6526953c37 | ||
|
|
4d411bc8dc | ||
|
|
0e7f5d93bb | ||
|
|
ca961e9122 | ||
|
|
e340ab0c8e | ||
|
|
2adc6ed186 | ||
|
|
9ff34addcd | ||
|
|
010416f0b1 | ||
|
|
14e028e8c8 | ||
|
|
53ed31be8d | ||
|
|
b46ba311ae | ||
|
|
8e6adc6b85 | ||
|
|
3813e63982 | ||
|
|
f3f44d9abe | ||
|
|
9acf34ee4a | ||
|
|
4a525d5f5d | ||
|
|
8b5c6169da | ||
|
|
e142010128 | ||
|
|
3cc34a2ed1 | ||
|
|
15c1fdd54c | ||
|
|
cb798ad466 | ||
|
|
75c3ad31d0 | ||
|
|
102e8a12b6 | ||
|
|
52b7b071f4 | ||
|
|
9f08894b61 | ||
|
|
ec264e0381 | ||
|
|
18d94c77e2 | ||
|
|
4f091ae221 | ||
|
|
671a80b6ff | ||
|
|
249de93047 | ||
|
|
ae6bb410a3 | ||
|
|
2aaee4826b | ||
|
|
f50adbf119 | ||
|
|
cf0951039c | ||
|
|
4b6cf9a46e | ||
|
|
12849d37da | ||
|
|
c60c396054 | ||
|
|
1720266761 | ||
|
|
edec837a87 | ||
|
|
d8553e95e2 | ||
|
|
f6dc7a0722 | ||
|
|
ed85f9804c | ||
|
|
cd2df017ec | ||
|
|
79a3779005 | ||
|
|
b3ec1c4f49 | ||
|
|
6aafadb4ad | ||
|
|
c4b902b110 | ||
|
|
7e239b0d1e | ||
|
|
038f38d32c | ||
|
|
e8580ec49e | ||
|
|
8ae59bdd93 | ||
|
|
e1a85fbb7e | ||
|
|
4e0cec051f | ||
|
|
6373919fc4 | ||
|
|
5f455295ad | ||
|
|
00bf5a3c63 | ||
|
|
9aa86f19f0 | ||
|
|
64c07a62e6 | ||
|
|
100169952c | ||
|
|
8624a507b3 | ||
|
|
fcd37a5910 | ||
|
|
c91b538c97 | ||
|
|
53232729c3 | ||
|
|
107d1ba359 |
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[install.scopes]
|
||||||
|
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"
|
||||||
@@ -5,5 +5,8 @@
|
|||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"npm-check-updates": "^17.1.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
packages/example/.gitignore
vendored
Normal file
24
packages/example/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
packages/example/README.md
Normal file
50
packages/example/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
28
packages/example/eslint.config.js
Normal file
28
packages/example/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
13
packages/example/index.html
Normal file
13
packages/example/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
packages/example/package.json
Normal file
43
packages/example/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@tanstack/react-router": "^1.95.3",
|
||||||
|
"@tanstack/router-devtools": "^1.95.3",
|
||||||
|
"@tanstack/router-plugin": "^1.95.3",
|
||||||
|
"@thilawyn/thilaschema": "^0.1.4",
|
||||||
|
"@types/react": "^19.0.4",
|
||||||
|
"@types/react-dom": "^19.0.2",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"effect": "^3.12.1",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.18.2",
|
||||||
|
"vite": "^6.0.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@effect/platform": "^0.73.1",
|
||||||
|
"@effect/platform-browser": "^0.52.1",
|
||||||
|
"@radix-ui/themes": "^3.1.6",
|
||||||
|
"@typed/id": "^0.17.1",
|
||||||
|
"lucide-react": "^0.471.1",
|
||||||
|
"mobx": "^6.13.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/example/postcss.config.js
Normal file
6
packages/example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/example/public/vite.svg
Normal file
1
packages/example/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
8
packages/example/src/domain/Post.ts
Normal file
8
packages/example/src/domain/Post.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class Post extends Schema.Class<Post>("Post")({
|
||||||
|
id: Schema.String,
|
||||||
|
title: Schema.String,
|
||||||
|
content: Schema.String,
|
||||||
|
}) {}
|
||||||
26
packages/example/src/domain/Todo.ts
Normal file
26
packages/example/src/domain/Todo.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ThSchema } from "@thilawyn/thilaschema"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class Todo extends Schema.Class<Todo>("Todo")({
|
||||||
|
_tag: Schema.tag("Todo"),
|
||||||
|
id: Schema.String,
|
||||||
|
content: Schema.String,
|
||||||
|
completedAt: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
|
||||||
|
export const TodoFromJsonStruct = Schema.Struct({
|
||||||
|
...Todo.fields,
|
||||||
|
completedAt: Schema.Option(Schema.DateTimeUtc),
|
||||||
|
}).pipe(
|
||||||
|
ThSchema.assertEncodedJsonifiable
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
|
||||||
|
|
||||||
|
|
||||||
|
export const generateUniqueID = makeUuid4.pipe(
|
||||||
|
Effect.provide(GetRandomValues.CryptoRandom)
|
||||||
|
)
|
||||||
2
packages/example/src/domain/index.ts
Normal file
2
packages/example/src/domain/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * as Post from "./Post"
|
||||||
|
export * as Todo from "./Todo"
|
||||||
3
packages/example/src/index.css
Normal file
3
packages/example/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
32
packages/example/src/main.tsx
Normal file
32
packages/example/src/main.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||||
|
import { ReffuseRuntime } from "@thilawyn/reffuse"
|
||||||
|
import { Layer } from "effect"
|
||||||
|
import { StrictMode } from "react"
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
import { GlobalContext } from "./reffuse"
|
||||||
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
import { FetchData } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
const layer = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(FetchData.mockLayer)
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ReffuseRuntime.Provider>
|
||||||
|
<GlobalContext.Provider layer={layer}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</GlobalContext.Provider>
|
||||||
|
</ReffuseRuntime.Provider>
|
||||||
|
</StrictMode>
|
||||||
|
)
|
||||||
7
packages/example/src/reffuse.ts
Normal file
7
packages/example/src/reffuse.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { ReffuseContext } from "@thilawyn/reffuse"
|
||||||
|
import { make } from "@thilawyn/reffuse/Reffuse"
|
||||||
|
import { FetchData } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const GlobalContext = ReffuseContext.make<FetchData.FetchData>()
|
||||||
|
export const Reffuse = make(GlobalContext)
|
||||||
134
packages/example/src/routeTree.gen.ts
Normal file
134
packages/example/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
// Import Routes
|
||||||
|
|
||||||
|
import { Route as rootRoute } from './routes/__root'
|
||||||
|
import { Route as TimeImport } from './routes/time'
|
||||||
|
import { Route as CountImport } from './routes/count'
|
||||||
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
|
||||||
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const TimeRoute = TimeImport.update({
|
||||||
|
id: '/time',
|
||||||
|
path: '/time',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const CountRoute = CountImport.update({
|
||||||
|
id: '/count',
|
||||||
|
path: '/count',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const IndexRoute = IndexImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/count': {
|
||||||
|
id: '/count'
|
||||||
|
path: '/count'
|
||||||
|
fullPath: '/count'
|
||||||
|
preLoaderRoute: typeof CountImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/time': {
|
||||||
|
id: '/time'
|
||||||
|
path: '/time'
|
||||||
|
fullPath: '/time'
|
||||||
|
preLoaderRoute: typeof TimeImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export the route tree
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/count': typeof CountRoute
|
||||||
|
'/time': typeof TimeRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/count': typeof CountRoute
|
||||||
|
'/time': typeof TimeRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRoute
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/count': typeof CountRoute
|
||||||
|
'/time': typeof TimeRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/' | '/count' | '/time'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/' | '/count' | '/time'
|
||||||
|
id: '__root__' | '/' | '/count' | '/time'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
CountRoute: typeof CountRoute
|
||||||
|
TimeRoute: typeof TimeRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
CountRoute: CountRoute,
|
||||||
|
TimeRoute: TimeRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeTree = rootRoute
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
|
/* ROUTE_MANIFEST_START
|
||||||
|
{
|
||||||
|
"routes": {
|
||||||
|
"__root__": {
|
||||||
|
"filePath": "__root.tsx",
|
||||||
|
"children": [
|
||||||
|
"/",
|
||||||
|
"/count",
|
||||||
|
"/time"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"/": {
|
||||||
|
"filePath": "index.tsx"
|
||||||
|
},
|
||||||
|
"/count": {
|
||||||
|
"filePath": "count.tsx"
|
||||||
|
},
|
||||||
|
"/time": {
|
||||||
|
"filePath": "time.tsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ROUTE_MANIFEST_END */
|
||||||
27
packages/example/src/routes/__root.tsx
Normal file
27
packages/example/src/routes/__root.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Container, Flex, Theme } from "@radix-ui/themes"
|
||||||
|
import "@radix-ui/themes/styles.css"
|
||||||
|
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||||
|
import "../index.css"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: Root
|
||||||
|
})
|
||||||
|
|
||||||
|
function Root() {
|
||||||
|
return (
|
||||||
|
<Theme>
|
||||||
|
<Container>
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
|
<Link to="/">Index</Link>
|
||||||
|
<Link to="/time">Time</Link>
|
||||||
|
<Link to="/count">Count</Link>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
<TanStackRouterDevtools />
|
||||||
|
</Theme>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
packages/example/src/routes/count.tsx
Normal file
27
packages/example/src/routes/count.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Reffuse } from "@/reffuse"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Ref } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/count")({
|
||||||
|
component: Count
|
||||||
|
})
|
||||||
|
|
||||||
|
function Count() {
|
||||||
|
|
||||||
|
const runSync = Reffuse.useRunSync()
|
||||||
|
|
||||||
|
const countRef = Reffuse.useRef(0)
|
||||||
|
const [count] = Reffuse.useRefState(countRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto">
|
||||||
|
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||||
|
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
||||||
|
count is {count}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
34
packages/example/src/routes/index.tsx
Normal file
34
packages/example/src/routes/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Reffuse } from "@/reffuse"
|
||||||
|
import { TodosContext } from "@/todos/reffuse"
|
||||||
|
import { TodosState } from "@/todos/services"
|
||||||
|
import { VTodos } from "@/todos/views/VTodos"
|
||||||
|
import { Container } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Effect, Layer } from "effect"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Index
|
||||||
|
})
|
||||||
|
|
||||||
|
function Index() {
|
||||||
|
|
||||||
|
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(TodosState.make("todos"))
|
||||||
|
), [])
|
||||||
|
|
||||||
|
Reffuse.useEffect(Effect.addFinalizer(() => Console.log("Effect cleanup")).pipe(
|
||||||
|
Effect.flatMap(() => Console.log("Effect recalculated"))
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<TodosContext.Provider layer={todosLayer}>
|
||||||
|
<VTodos />
|
||||||
|
</TodosContext.Provider>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
49
packages/example/src/routes/time.tsx
Normal file
49
packages/example/src/routes/time.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Reffuse } from "@/reffuse"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, DateTime, Effect, Ref, Schedule, Stream } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
const timeEverySecond = Stream.repeatEffectWithSchedule(
|
||||||
|
DateTime.now,
|
||||||
|
Schedule.intersect(Schedule.forever, Schedule.spaced("1 second")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/time")({
|
||||||
|
component: Time
|
||||||
|
})
|
||||||
|
|
||||||
|
function Time() {
|
||||||
|
|
||||||
|
const timeRef = Reffuse.useRefFromEffect(DateTime.now)
|
||||||
|
|
||||||
|
Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||||
|
Effect.flatMap(() =>
|
||||||
|
Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v))
|
||||||
|
)
|
||||||
|
), [timeRef])
|
||||||
|
// Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||||
|
// Effect.flatMap(() => DateTime.now),
|
||||||
|
// Effect.flatMap(v => Ref.set(timeRef, v)),
|
||||||
|
// Effect.repeat(Schedule.intersect(
|
||||||
|
// Schedule.forever,
|
||||||
|
// Schedule.spaced("1 second"),
|
||||||
|
// )),
|
||||||
|
// ), [timeRef])
|
||||||
|
|
||||||
|
const [time] = Reffuse.useRefState(timeRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<p className="text-center">
|
||||||
|
{DateTime.format(time, {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
second: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
24
packages/example/src/services/FetchData.ts
Normal file
24
packages/example/src/services/FetchData.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Post } from "@/domain"
|
||||||
|
import { Chunk, Context, Effect, Layer } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class FetchData extends Context.Tag("FetchData")<FetchData, {
|
||||||
|
readonly fetchPosts: Effect.Effect<Chunk.Chunk<Post.Post>>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
|
||||||
|
export const mockLayer = Layer.succeed(FetchData, {
|
||||||
|
fetchPosts: Effect.succeed(Chunk.make(
|
||||||
|
Post.Post.make({
|
||||||
|
id: "1",
|
||||||
|
title: "Lorem ipsum dolor sit amet",
|
||||||
|
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget lacus sit amet diam suscipit porttitor non at felis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla risus ligula, elementum nec scelerisque eget, volutpat vel sapien. Phasellus aliquam ac neque vitae sodales. Nunc sodales congue odio. Nulla eget nisl cursus, convallis lorem at, varius lectus. Aliquam vitae mauris vel mi dignissim condimentum. Proin sed dignissim sapien, ut cursus ex. Donec eget sapien sagittis, auctor metus vitae, fringilla lacus. Donec ut elit a quam aliquet consectetur interdum eu nisl. Etiam nec convallis purus, eu venenatis nulla. Phasellus non metus id mauris tincidunt cursus. Cras varius aliquet diam eu blandit. In hac habitasse platea dictumst.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
Post.Post.make({
|
||||||
|
id: "2",
|
||||||
|
title: "Vestibulum non bibendum ligula",
|
||||||
|
content: "Vestibulum non bibendum ligula. Integer pellentesque, diam ac faucibus volutpat, nulla libero porttitor nunc, ac pulvinar tortor diam id ipsum. Sed id enim at odio euismod imperdiet et ac purus. Etiam tempus ipsum semper scelerisque mollis. Integer auctor, magna et tristique tempus, nisi mi euismod est, nec finibus quam nunc nec libero. Maecenas aliquet viverra magna, vitae blandit ligula pharetra id. Vestibulum vel lacus at nibh placerat tincidunt. Sed suscipit tellus vel felis euismod, et sollicitudin neque cursus. Curabitur dapibus eros vitae ligula suscipit, at facilisis risus venenatis. Sed pharetra blandit pulvinar. Vivamus vestibulum at ligula pulvinar fringilla. Suspendisse vel mattis libero, eget vulputate massa. Vivamus vehicula, lectus id tempor maximus, erat tortor blandit purus, at scelerisque nunc urna faucibus sapien.",
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
})
|
||||||
1
packages/example/src/services/index.ts
Normal file
1
packages/example/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as FetchData from "./FetchData"
|
||||||
8
packages/example/src/todos/reffuse.ts
Normal file
8
packages/example/src/todos/reffuse.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { GlobalContext } from "@/reffuse"
|
||||||
|
import { ReffuseContext } from "@thilawyn/reffuse"
|
||||||
|
import { make } from "@thilawyn/reffuse/Reffuse"
|
||||||
|
import { TodosState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
||||||
|
export const Reffuse = make(GlobalContext, TodosContext)
|
||||||
69
packages/example/src/todos/services/TodosState.ts
Normal file
69
packages/example/src/todos/services/TodosState.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { KeyValueStore } from "@effect/platform"
|
||||||
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
|
import { PlatformError } from "@effect/platform/Error"
|
||||||
|
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||||
|
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||||
|
|
||||||
|
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||||
|
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||||
|
|
||||||
|
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
|
||||||
|
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
|
||||||
|
readonly remove: (index: number) => Effect.Effect<void>
|
||||||
|
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
|
||||||
|
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
||||||
|
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
|
||||||
|
|
||||||
|
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||||
|
Effect.flatMap(kv => kv.get(key)),
|
||||||
|
Effect.flatMap(identity),
|
||||||
|
Effect.flatMap(Schema.parseJson().pipe(
|
||||||
|
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||||
|
Schema.decode,
|
||||||
|
)),
|
||||||
|
Effect.flatMap(v => Ref.set(todos, v)),
|
||||||
|
|
||||||
|
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
|
||||||
|
|
||||||
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
|
||||||
|
Effect.flatMap(([kv, values]) => values.pipe(
|
||||||
|
Schema.parseJson().pipe(
|
||||||
|
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||||
|
Schema.encode,
|
||||||
|
),
|
||||||
|
Effect.flatMap(v => kv.set(key, v)),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
|
||||||
|
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
|
||||||
|
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
|
||||||
|
|
||||||
|
// const moveUp = (index: number) => Effect.gen(function*() {
|
||||||
|
|
||||||
|
// })
|
||||||
|
|
||||||
|
yield* readFromLocalStorage
|
||||||
|
|
||||||
|
return {
|
||||||
|
todos,
|
||||||
|
readFromLocalStorage,
|
||||||
|
saveToLocalStorage,
|
||||||
|
prepend,
|
||||||
|
replace,
|
||||||
|
remove,
|
||||||
|
}
|
||||||
|
}))
|
||||||
1
packages/example/src/todos/services/index.ts
Normal file
1
packages/example/src/todos/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as TodosState from "./TodosState"
|
||||||
52
packages/example/src/todos/views/VNewTodo.tsx
Normal file
52
packages/example/src/todos/views/VNewTodo.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||||
|
import { Effect, Option } from "effect"
|
||||||
|
import { Reffuse } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
|
export function VNewTodo() {
|
||||||
|
|
||||||
|
const runSync = Reffuse.useRunSync()
|
||||||
|
|
||||||
|
const createEmptyTodo = Todo.generateUniqueID.pipe(
|
||||||
|
Effect.map(id => Todo.Todo.make({
|
||||||
|
id,
|
||||||
|
content: "",
|
||||||
|
completedAt: Option.none(),
|
||||||
|
}, true))
|
||||||
|
)
|
||||||
|
|
||||||
|
const todoRef = Reffuse.useRefFromEffect(createEmptyTodo)
|
||||||
|
const [todo, setTodo] = Reffuse.useRefState(todoRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Card>
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<TextArea
|
||||||
|
value={todo.content}
|
||||||
|
onChange={e => setTodo(
|
||||||
|
Todo.Todo.make({ ...todo, content: e.target.value }, true)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center">
|
||||||
|
<Button
|
||||||
|
onClick={() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.prepend(todo)),
|
||||||
|
Effect.flatMap(() => createEmptyTodo),
|
||||||
|
Effect.map(setTodo),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
56
packages/example/src/todos/views/VTodo.tsx
Normal file
56
packages/example/src/todos/views/VTodo.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Delete } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Reffuse } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
|
export interface VTodoProps {
|
||||||
|
readonly index: number
|
||||||
|
readonly todo: Todo.Todo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VTodo({ index, todo }: VTodoProps) {
|
||||||
|
|
||||||
|
const runSync = Reffuse.useRunSync()
|
||||||
|
const editorMode = useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Card>
|
||||||
|
<Flex direction="column" align="stretch" gap="1">
|
||||||
|
<TextArea
|
||||||
|
value={todo.content}
|
||||||
|
onChange={e => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.replace(
|
||||||
|
index,
|
||||||
|
Todo.Todo.make({ ...todo, content: e.target.value }, true),
|
||||||
|
)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
disabled={!editorMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="between" align="center">
|
||||||
|
<Box></Box>
|
||||||
|
|
||||||
|
<Flex direction="row" align="center" gap="1">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.remove(index)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
36
packages/example/src/todos/views/VTodos.tsx
Normal file
36
packages/example/src/todos/views/VTodos.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Box, Flex } from "@radix-ui/themes"
|
||||||
|
import { Chunk, Effect, Stream } from "effect"
|
||||||
|
import { Reffuse } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
import { VNewTodo } from "./VNewTodo"
|
||||||
|
import { VTodo } from "./VTodo"
|
||||||
|
|
||||||
|
|
||||||
|
export function VTodos() {
|
||||||
|
|
||||||
|
// Sync changes to the todos with the local storage
|
||||||
|
Reffuse.useFork(TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state =>
|
||||||
|
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
const todosRef = Reffuse.useMemo(TodosState.TodosState.pipe(Effect.map(state => state.todos)))
|
||||||
|
const [todos] = Reffuse.useRefState(todosRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="center" gap="3">
|
||||||
|
<Box width="500px">
|
||||||
|
<VNewTodo />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{Chunk.map(todos, (todo, index) => (
|
||||||
|
<Box key={todo.id} width="500px">
|
||||||
|
<VTodo index={index} todo={todo} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
34
packages/example/src/views/post/VPost.tsx
Normal file
34
packages/example/src/views/post/VPost.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Post } from "@/domain"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { PostsState } from "../posts/services"
|
||||||
|
import { Reffuse } from "./reffuse"
|
||||||
|
|
||||||
|
|
||||||
|
export interface VPostProps {
|
||||||
|
readonly index: number
|
||||||
|
readonly post: Post.Post
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function VPost({ post, index }: VPostProps) {
|
||||||
|
|
||||||
|
const runSync = Reffuse.useRunSync()
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-col gap-1 items-stretch">
|
||||||
|
<p>{post.title}</p>
|
||||||
|
<p>{post.content}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => PostsState.PostsState.pipe(
|
||||||
|
Effect.flatMap(state => state.remove(index)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
1
packages/example/src/views/post/reffuse.ts
Normal file
1
packages/example/src/views/post/reffuse.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Reffuse } from "../posts/reffuse"
|
||||||
25
packages/example/src/views/posts/VPosts.tsx
Normal file
25
packages/example/src/views/posts/VPosts.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Chunk } from "effect"
|
||||||
|
import { VPost } from "../post/VPost"
|
||||||
|
import { Reffuse } from "./reffuse"
|
||||||
|
import { PostsState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export function VPosts() {
|
||||||
|
|
||||||
|
const state = Reffuse.useMemo(PostsState.PostsState)
|
||||||
|
const [posts] = Reffuse.useRefState(state.posts)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-col gap-2 items-stretch">
|
||||||
|
{Chunk.map(posts, (post, index) => (
|
||||||
|
<VPost
|
||||||
|
key={`${ index }-${ post.id }`}
|
||||||
|
index={index}
|
||||||
|
post={post}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
8
packages/example/src/views/posts/reffuse.ts
Normal file
8
packages/example/src/views/posts/reffuse.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { GlobalContext } from "@/reffuse"
|
||||||
|
import { ReffuseContext } from "@thilawyn/reffuse"
|
||||||
|
import { make } from "@thilawyn/reffuse/Reffuse"
|
||||||
|
import { PostsState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const PostsContext = ReffuseContext.make<PostsState.PostsState>()
|
||||||
|
export const Reffuse = make(GlobalContext, PostsContext)
|
||||||
15
packages/example/src/views/posts/services/PostsState.ts
Normal file
15
packages/example/src/views/posts/services/PostsState.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Post } from "@/domain"
|
||||||
|
import { Chunk, Context, Effect, Layer, Ref, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class PostsState extends Context.Tag("PostsState")<PostsState, {
|
||||||
|
readonly posts: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Post.Post>>
|
||||||
|
readonly remove: (index: number) => Effect.Effect<void>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
export const make = (posts: Chunk.Chunk<Post.Post>) => Layer.effect(PostsState, SubscriptionRef.make(posts).pipe(
|
||||||
|
Effect.map(posts => ({
|
||||||
|
posts,
|
||||||
|
remove: (index: number) => Ref.update(posts, Chunk.remove(index)),
|
||||||
|
}))
|
||||||
|
))
|
||||||
1
packages/example/src/views/posts/services/index.ts
Normal file
1
packages/example/src/views/posts/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as PostsState from "./PostsState"
|
||||||
1
packages/example/src/vite-env.d.ts
vendored
Normal file
1
packages/example/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
11
packages/example/tailwind.config.js
Normal file
11
packages/example/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import("tailwindcss").Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
30
packages/example/tsconfig.app.json
Normal file
30
packages/example/tsconfig.app.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
packages/example/tsconfig.json
Normal file
7
packages/example/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
24
packages/example/tsconfig.node.json
Normal file
24
packages/example/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
19
packages/example/vite.config.ts
Normal file
19
packages/example/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import path from "node:path"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
TanStackRouterVite(),
|
||||||
|
react(),
|
||||||
|
],
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -24,5 +24,9 @@
|
|||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.4",
|
||||||
|
"effect": "^3.12.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
packages/reffuse/src/Reffuse.ts
Normal file
205
packages/reffuse/src/Reffuse.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { Context, Effect, ExecutionStrategy, Exit, Fiber, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import React from "react"
|
||||||
|
import * as ReffuseContext from "./ReffuseContext.js"
|
||||||
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class Reffuse<R> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
useContext(): Context.Context<R> {
|
||||||
|
return ReffuseContext.useMergeAll(...this.contexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRunSync() {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
const context = this.useContext()
|
||||||
|
|
||||||
|
return React.useCallback(<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R>
|
||||||
|
): A => effect.pipe(
|
||||||
|
Effect.provide(context),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
), [runtime, context])
|
||||||
|
}
|
||||||
|
|
||||||
|
useRunPromise() {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
const context = this.useContext()
|
||||||
|
|
||||||
|
return React.useCallback(<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R>,
|
||||||
|
options?: { readonly signal?: AbortSignal },
|
||||||
|
): Promise<A> => effect.pipe(
|
||||||
|
Effect.provide(context),
|
||||||
|
effect => Runtime.runPromise(runtime)(effect, options),
|
||||||
|
), [runtime, context])
|
||||||
|
}
|
||||||
|
|
||||||
|
useRunFork() {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
const context = this.useContext()
|
||||||
|
|
||||||
|
return React.useCallback(<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R>,
|
||||||
|
options?: Runtime.RunForkOptions,
|
||||||
|
): Fiber.RuntimeFiber<A, E> => effect.pipe(
|
||||||
|
Effect.provide(context),
|
||||||
|
effect => Runtime.runFork(runtime)(effect, options),
|
||||||
|
), [runtime, context])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useMemo<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: RenderOptions,
|
||||||
|
): A {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
return React.useMemo(() => runSync(effect), [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: RenderOptions & ScopeOptions,
|
||||||
|
): void {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
return React.useEffect(() => {
|
||||||
|
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||||
|
Effect.tap(scope => Effect.provideService(effect, Scope.Scope, scope)),
|
||||||
|
runSync,
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||||
|
}, [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: RenderOptions & ScopeOptions,
|
||||||
|
): void {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
return React.useLayoutEffect(() => {
|
||||||
|
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||||
|
Effect.tap(scope => Effect.provideService(effect, Scope.Scope, scope)),
|
||||||
|
runSync,
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||||
|
}, [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
useSuspense<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: { readonly signal?: AbortSignal } & RenderOptions,
|
||||||
|
): A {
|
||||||
|
const runPromise = this.useRunPromise()
|
||||||
|
|
||||||
|
const promise = React.useMemo(() => runPromise(effect, options), [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
return React.use(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
useFork<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
||||||
|
): void {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
const runFork = this.useRunFork()
|
||||||
|
|
||||||
|
return React.useEffect(() => {
|
||||||
|
const scope = runSync(Scope.make(options?.finalizerExecutionStrategy))
|
||||||
|
const fiber = runFork(Effect.provideService(effect, Scope.Scope, scope), options)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Fiber.interrupt(fiber).pipe(
|
||||||
|
Effect.flatMap(() => Scope.close(scope, Exit.void)),
|
||||||
|
runFork,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
return this.useMemo(
|
||||||
|
SubscriptionRef.make(value),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, R>): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
return this.useMemo(
|
||||||
|
effect.pipe(Effect.flatMap(SubscriptionRef.make)),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
const initialState = React.useMemo(() => runSync(ref), [ref])
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
|
this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
||||||
|
setReactStateValue(v)
|
||||||
|
)), [ref])
|
||||||
|
|
||||||
|
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
|
||||||
|
runSync(Ref.update(ref, previousState =>
|
||||||
|
typeof setStateAction === "function"
|
||||||
|
? (setStateAction as (prevState: A) => A)(previousState)
|
||||||
|
: setStateAction
|
||||||
|
)),
|
||||||
|
[ref])
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface RenderOptions {
|
||||||
|
/** Prevents re-executing the effect when the Effect runtime or context changes. Defaults to `false`. */
|
||||||
|
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScopeOptions {
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = <
|
||||||
|
const Contexts extends readonly ReffuseContext.ReffuseContext<any>[]
|
||||||
|
>(
|
||||||
|
...contexts: Contexts
|
||||||
|
): Reffuse<{ [K in keyof Contexts]: ReffuseContext.R<Contexts[K]> }[number]> =>
|
||||||
|
new Reffuse(contexts)
|
||||||
73
packages/reffuse/src/ReffuseContext.tsx
Normal file
73
packages/reffuse/src/ReffuseContext.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Array, Context, Effect, Layer, Runtime } from "effect"
|
||||||
|
import React from "react"
|
||||||
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class ReffuseContext<R> {
|
||||||
|
|
||||||
|
readonly Context = React.createContext<Context.Context<R>>(null!)
|
||||||
|
readonly Provider: ReffuseContextReactProvider<R>
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.Provider = (props) => {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
|
||||||
|
const value = React.useMemo(() => Effect.context<R>().pipe(
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
), [runtime, props.layer])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<this.Context
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.Provider.displayName = "ReffuseContextReactProvider"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useContext(): Context.Context<R> {
|
||||||
|
return React.useContext(this.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayer(): Layer.Layer<R> {
|
||||||
|
const context = this.useContext()
|
||||||
|
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReffuseContextReactProvider<R> = React.FC<{
|
||||||
|
readonly layer: Layer.Layer<R, unknown>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
||||||
|
|
||||||
|
|
||||||
|
export function make<R = never>() {
|
||||||
|
return new ReffuseContext<R>()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMergeAll<
|
||||||
|
const Contexts extends readonly ReffuseContext<any>[]
|
||||||
|
>(
|
||||||
|
...contexts: Contexts
|
||||||
|
): Context.Context<{ [K in keyof Contexts]: R<Contexts[K]> }[number]> {
|
||||||
|
const values = contexts.map(v => React.use(v.Context))
|
||||||
|
return React.useMemo(() => Context.mergeAll(...values), values)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMergeAllLayers<
|
||||||
|
const Contexts extends Array.NonEmptyReadonlyArray<ReffuseContext<any>>
|
||||||
|
>(
|
||||||
|
...contexts: Contexts
|
||||||
|
): Layer.Layer<{ [K in keyof Contexts]: R<Contexts[K]> }[number]> {
|
||||||
|
const values = Array.map(contexts, v => React.use(v.Context))
|
||||||
|
|
||||||
|
return React.useMemo(() => Layer.mergeAll(
|
||||||
|
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
|
||||||
|
) as Layer.Layer<any>, values)
|
||||||
|
}
|
||||||
15
packages/reffuse/src/ReffuseRuntime.tsx
Normal file
15
packages/reffuse/src/ReffuseRuntime.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Runtime } from "effect"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
|
||||||
|
|
||||||
|
export const Provider = (props: { readonly children?: React.ReactNode }) => (
|
||||||
|
<Context
|
||||||
|
{...props}
|
||||||
|
value={Runtime.defaultRuntime}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Provider.displayName = "ReffuseRuntimeReactProvider"
|
||||||
|
|
||||||
|
export const useRuntime = () => React.useContext(Context)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * as Reffuse from "./Reffuse.js"
|
||||||
|
export * as ReffuseContext from "./ReffuseContext.js"
|
||||||
|
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
|||||||
Reference in New Issue
Block a user