├── packages ├── koka-accessor │ ├── CONTRIBUTING.md │ ├── .github │ │ ├── renovate.json5 │ │ └── workflows │ │ │ ├── greetings.yml │ │ │ └── test.yml │ ├── CHANGELOG.md │ ├── tsconfig.json │ ├── jest.config.cjs │ ├── LICENSE │ ├── package.json │ ├── .gitignore │ └── README.md ├── koka-react │ ├── jest.setup.js │ ├── .github │ │ ├── renovate.json5 │ │ └── workflows │ │ │ ├── greetings.yml │ │ │ └── test.yml │ ├── tsconfig.json │ ├── LICENSE │ ├── package.json │ ├── jest.config.cjs │ ├── src │ │ └── koka-react.tsx │ ├── .gitignore │ └── README.md ├── koka │ ├── src │ │ ├── constant.ts │ │ ├── util.ts │ │ ├── async.ts │ │ ├── gen.ts │ │ ├── ctx.ts │ │ ├── opt.ts │ │ ├── err.ts │ │ ├── test.ts │ │ └── result.ts │ ├── .github │ │ ├── renovate.json5 │ │ └── workflows │ │ │ ├── greetings.yml │ │ │ └── test.yml │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── jest.config.cjs │ ├── LICENSE │ ├── package.json │ ├── __tests__ │ │ ├── gen.test.ts │ │ ├── err.test.ts │ │ ├── ctx.test.ts │ │ ├── async.test.ts │ │ └── opt.test.ts │ ├── .gitignore │ ├── docs │ │ ├── index.md │ │ ├── tutorials │ │ │ ├── getting-started.md │ │ │ └── core-concepts.md │ │ └── README.md │ └── README.md └── koka-domain │ ├── .github │ ├── renovate.json5 │ └── workflows │ │ ├── greetings.yml │ │ └── test.yml │ ├── tsconfig.json │ ├── src │ ├── shallowEqual.ts │ └── pretty-printer.ts │ ├── LICENSE │ ├── jest.config.cjs │ ├── package.json │ ├── .gitignore │ ├── README.md │ └── __tests__ │ ├── koka-domain-01.test.ts │ └── koka-domain-02.test.ts ├── .prettierignore ├── pnpm-workspace.yaml ├── projects └── koka-react-demo │ ├── src │ ├── vite-env.d.ts │ ├── useHash.tsx │ ├── App.css │ ├── test.ts │ ├── index.css │ ├── assets │ │ └── react.svg │ ├── domain.ts │ ├── main.tsx │ └── App.tsx │ ├── tsconfig.json │ ├── .gitignore │ ├── index.html │ ├── eslint.config.js │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── tsconfig.app.json │ ├── package.json │ ├── public │ └── vite.svg │ └── README.md ├── .commitlintrc.js ├── .eslintignore ├── .lintstagedrc ├── .husky ├── pre-commit └── commit-msg ├── .npmrc ├── .prettierrc ├── .editorconfig ├── .github ├── renovate.json5 └── workflows │ ├── greetings.yml │ └── test.yml ├── .changeset ├── config.json └── README.md ├── tsconfig.json ├── LICENSE ├── package.json ├── sync-repo.ts ├── .gitignore ├── .eslintrc └── README.md /packages/koka-accessor/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | examples/.cache 2 | node_modules 3 | dist 4 | esm -------------------------------------------------------------------------------- /packages/koka-react/jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'projects/*' -------------------------------------------------------------------------------- /projects/koka-react-demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples/.cache 2 | node_modules 3 | dist 4 | cjs 5 | esm 6 | */**/expected 7 | projects/** -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{json,yml,md,html,js,ts,tsx,vue}": [ 3 | "prettier --write" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- lint-staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint -e 5 | -------------------------------------------------------------------------------- /packages/koka/src/constant.ts: -------------------------------------------------------------------------------- 1 | export const EffSymbol = Symbol('ctx') 2 | 3 | export type EffSymbol = typeof EffSymbol 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = 'https://registry.npmjs.org/' 2 | prefer-workspace-packages = true 3 | auto-install-peers = true 4 | strict-peer-dependencies = false -------------------------------------------------------------------------------- /projects/koka-react-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "tabWidth": 4, 5 | "trailingComma": "all", 6 | "singleQuote": true, 7 | "arrowParens": "always", 8 | "jsxSingleQuote": false, 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 3 | "labels": ["dependencies"], 4 | "pin": false, 5 | "rangeStrategy": "bump", 6 | "node": false, 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["peerDependencies"], 10 | "enabled": false 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/koka/.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 3 | "labels": ["dependencies"], 4 | "pin": false, 5 | "rangeStrategy": "bump", 6 | "node": false, 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["peerDependencies"], 10 | "enabled": false 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/koka-accessor/.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 3 | "labels": ["dependencies"], 4 | "pin": false, 5 | "rangeStrategy": "bump", 6 | "node": false, 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["peerDependencies"], 10 | "enabled": false 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/koka-domain/.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 3 | "labels": ["dependencies"], 4 | "pin": false, 5 | "rangeStrategy": "bump", 6 | "node": false, 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["peerDependencies"], 10 | "enabled": false 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/koka-react/.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 3 | "labels": ["dependencies"], 4 | "pin": false, 5 | "rangeStrategy": "bump", 6 | "node": false, 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["peerDependencies"], 10 | "enabled": false 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/koka-accessor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # koka-accessor 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - improve koka, add accessor.proxy to koka-accessor 8 | - Updated dependencies 9 | - koka@1.0.3 10 | 11 | ## 1.0.1 12 | 13 | ### Patch Changes 14 | 15 | - add Accessor.get/Accessor.set, fix Eff.try().cach() 16 | - Updated dependencies 17 | - koka@1.0.2 18 | -------------------------------------------------------------------------------- /projects/koka-react-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank feedback. We will check it later:-)' 13 | pr-message: 'Thank for your PR. We will check it later:-)' 14 | -------------------------------------------------------------------------------- /packages/koka/.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank feedback. We will check it later:-)' 13 | pr-message: 'Thank for your PR. We will check it later:-)' 14 | -------------------------------------------------------------------------------- /packages/koka/src/util.ts: -------------------------------------------------------------------------------- 1 | export const withResolvers: () => PromiseWithResolvers = 2 | Promise.withResolvers?.bind(Promise) ?? 3 | (() => { 4 | let resolve: (value: T) => void 5 | let reject: (reason?: any) => void 6 | 7 | const promise = new Promise((res, rej) => { 8 | resolve = res 9 | reject = rej 10 | }) 11 | 12 | // @ts-ignore as expected 13 | return { promise, resolve, reject } 14 | }) 15 | -------------------------------------------------------------------------------- /packages/koka-accessor/.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank feedback. We will check it later:-)' 13 | pr-message: 'Thank for your PR. We will check it later:-)' 14 | -------------------------------------------------------------------------------- /packages/koka-domain/.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank feedback. We will check it later:-)' 13 | pr-message: 'Thank for your PR. We will check it later:-)' 14 | -------------------------------------------------------------------------------- /packages/koka-react/.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank feedback. We will check it later:-)' 13 | pr-message: 'Thank for your PR. We will check it later:-)' 14 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/useHash.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export function useHash() { 4 | const [hash, setHash] = useState(window.location.hash) 5 | 6 | useEffect(() => { 7 | const handleHashChange = () => { 8 | setHash(window.location.hash) 9 | } 10 | window.addEventListener('hashchange', handleHashChange) 11 | return () => window.removeEventListener('hashchange', handleHashChange) 12 | }, []) 13 | 14 | return hash 15 | } 16 | -------------------------------------------------------------------------------- /projects/koka-react-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Koka React Demo 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/koka/src/async.ts: -------------------------------------------------------------------------------- 1 | export type MaybePromise = T extends Promise ? T : T | Promise 2 | 3 | export type Async = { 4 | type: 'async' 5 | name?: undefined 6 | promise: Promise 7 | } 8 | 9 | function* awaitEffect(value: T | Promise): Generator { 10 | if (!(value instanceof Promise)) { 11 | return value 12 | } 13 | 14 | const result = yield { 15 | type: 'async', 16 | promise: value, 17 | } 18 | 19 | return result as T 20 | } 21 | 22 | export { awaitEffect as await } 23 | -------------------------------------------------------------------------------- /packages/koka/src/gen.ts: -------------------------------------------------------------------------------- 1 | export const isGen = ( 2 | value: unknown, 3 | ): value is Generator => { 4 | return typeof value === 'object' && value !== null && 'next' in value && 'throw' in value 5 | } 6 | 7 | export const cleanUpGen = (gen: Generator) => { 8 | const result = (gen as Generator).return(undefined) 9 | 10 | if (!result.done) { 11 | throw new Error(`You can not use yield in the finally block of a generator`) 12 | } 13 | } 14 | 15 | export function* of(value: T) { 16 | return value 17 | } 18 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/App.css: -------------------------------------------------------------------------------- 1 | /* 保留一些基础样式,其他使用 Tailwind CSS */ 2 | #root { 3 | max-width: none; 4 | margin: 0; 5 | padding: 0; 6 | text-align: left; 7 | } 8 | 9 | .logo { 10 | height: 6em; 11 | padding: 1.5em; 12 | will-change: filter; 13 | transition: filter 300ms; 14 | } 15 | .logo:hover { 16 | filter: drop-shadow(0 0 2em #646cffaa); 17 | } 18 | .logo.react:hover { 19 | filter: drop-shadow(0 0 2em #61dafbaa); 20 | } 21 | 22 | @keyframes logo-spin { 23 | from { 24 | transform: rotate(0deg); 25 | } 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | @media (prefers-reduced-motion: no-preference) { 32 | a:nth-of-type(2) .logo { 33 | animation: logo-spin infinite 20s linear; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "downlevelIteration": true, 7 | "importHelpers": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "newLine": "LF", 11 | "allowJs": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "target": "ES5", 16 | "sourceMap": true, 17 | "esModuleInterop": true, 18 | "stripInternal": true, 19 | "experimentalDecorators": false, 20 | "lib": ["ESNext", "DOM"] 21 | }, 22 | "include": ["./packages", "./projects", "sync-repo.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /projects/koka-react-demo/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /packages/koka/src/ctx.ts: -------------------------------------------------------------------------------- 1 | import { EffSymbol } from './constant.ts' 2 | 3 | export type Ctx = { 4 | type: 'ctx' 5 | name: Name 6 | context: EffSymbol | T 7 | } 8 | 9 | export type AnyCtx = Ctx 10 | 11 | export function Ctx(name: Name) { 12 | return class Eff implements Ctx { 13 | static field: Name = name 14 | type = 'ctx' as const 15 | name = name 16 | context = EffSymbol as EffSymbol | T 17 | } 18 | } 19 | 20 | export type CtxValue = Exclude 21 | 22 | export function* get(ctx: C | (new () => C)): Generator> { 23 | const context = yield typeof ctx === 'function' ? new ctx() : ctx 24 | 25 | return context as CtxValue 26 | } 27 | -------------------------------------------------------------------------------- /packages/koka/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "downlevelIteration": true, 7 | "importHelpers": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "newLine": "LF", 11 | "allowJs": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "target": "ES5", 16 | "sourceMap": true, 17 | "esModuleInterop": true, 18 | "stripInternal": true, 19 | "lib": ["ESNext", "DOM"], 20 | "noEmit": true, 21 | "allowImportingTsExtensions": true, 22 | "erasableSyntaxOnly": true 23 | }, 24 | "include": ["./src", "./__tests__"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/koka-accessor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "downlevelIteration": true, 7 | "importHelpers": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "newLine": "LF", 11 | "allowJs": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "target": "ES5", 16 | "sourceMap": true, 17 | "esModuleInterop": true, 18 | "stripInternal": true, 19 | "lib": ["ESNext", "DOM"], 20 | "noEmit": true, 21 | "allowImportingTsExtensions": true, 22 | "erasableSyntaxOnly": true 23 | }, 24 | "include": ["./src", "./__tests__"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/koka-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "downlevelIteration": true, 7 | "importHelpers": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "newLine": "LF", 11 | "allowJs": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "target": "ES5", 16 | "sourceMap": true, 17 | "esModuleInterop": true, 18 | "stripInternal": true, 19 | "lib": ["ESNext", "DOM"], 20 | "noEmit": true, 21 | "allowImportingTsExtensions": true, 22 | "erasableSyntaxOnly": true 23 | }, 24 | "include": ["./src", "./__tests__"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/koka/src/opt.ts: -------------------------------------------------------------------------------- 1 | import { EffSymbol } from './constant.ts' 2 | 3 | export type Opt = { 4 | type: 'opt' 5 | name: Name 6 | value: EffSymbol | T 7 | } 8 | 9 | export type AnyOpt = Opt 10 | 11 | export function Opt(name: Name) { 12 | return class Eff implements Opt { 13 | static field: Name = name 14 | type = 'opt' as const 15 | name = name 16 | value = EffSymbol as EffSymbol | T 17 | } 18 | } 19 | 20 | export type OptValue = Exclude | undefined 21 | 22 | export function* get( 23 | opt: O | (new () => O), 24 | ): Generator | undefined> { 25 | const optValue = yield typeof opt === 'function' ? new opt() : opt 26 | 27 | return optValue as OptValue 28 | } 29 | -------------------------------------------------------------------------------- /projects/koka-react-demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "experimentalDecorators": false, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "erasableSyntaxOnly": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["vite.config.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/koka-domain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "downlevelIteration": true, 7 | "importHelpers": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "newLine": "LF", 11 | "allowJs": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "target": "ES5", 16 | "sourceMap": true, 17 | "esModuleInterop": true, 18 | "stripInternal": true, 19 | "lib": ["ESNext", "DOM"], 20 | "noEmit": true, 21 | "allowImportingTsExtensions": true, 22 | "erasableSyntaxOnly": true, 23 | "experimentalDecorators": false 24 | }, 25 | "include": ["./src", "./__tests__"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/koka/src/err.ts: -------------------------------------------------------------------------------- 1 | export type Err = { 2 | type: 'err' 3 | name: Name 4 | error: T 5 | } 6 | 7 | export type AnyErr = Err 8 | 9 | export type ExtractErr = T extends AnyErr ? T : never 10 | 11 | export type ExcludeErr = T extends AnyErr ? never : T 12 | 13 | export function Err(name: Name) { 14 | return class Eff implements Err { 15 | static field: Name = name 16 | type = 'err' as const 17 | name = name 18 | error: E 19 | constructor(error: E) { 20 | this.error = error 21 | } 22 | } 23 | } 24 | 25 | function* throwError(err: E): Generator { 26 | yield err 27 | /* istanbul ignore next */ 28 | throw new Error(`Unexpected resumption of error effect [${err.name}]`) 29 | } 30 | 31 | export { throwError as throw } 32 | -------------------------------------------------------------------------------- /packages/koka/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # koka 2 | 3 | ## 1.0.9 4 | 5 | ### Patch Changes 6 | 7 | - fix: Promise.withResolvers should bind promise or it will throw error 8 | 9 | ## 1.0.8 10 | 11 | ### Patch Changes 12 | 13 | - feat: build cjs/esm 14 | 15 | ## 1.0.7 16 | 17 | ### Patch Changes 18 | 19 | - feat: supports stream/all/race/communicate 20 | 21 | ## 1.0.6 22 | 23 | ### Patch Changes 24 | 25 | - feat: add Optional Effect support to koka 26 | 27 | ## 1.0.5 28 | 29 | ### Patch Changes 30 | 31 | - feat: Eff.all supports has been added 32 | 33 | ## 1.0.4 34 | 35 | ### Patch Changes 36 | 37 | - feat: add design-first approach support 38 | 39 | ## 1.0.3 40 | 41 | ### Patch Changes 42 | 43 | - improve koka, add accessor.proxy to koka-accessor 44 | 45 | ## 1.0.2 46 | 47 | ### Patch Changes 48 | 49 | - add Accessor.get/Accessor.set, fix Eff.try().cach() 50 | 51 | ## 1.0.1 52 | 53 | ### Patch Changes 54 | 55 | - first publish 56 | -------------------------------------------------------------------------------- /projects/koka-react-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react({ 9 | useAtYourOwnRisk_mutateSwcOptions: (options) => { 10 | options.jsc = { 11 | ...options.jsc, 12 | parser: { 13 | ...(options.jsc?.parser ?? {}), 14 | syntax: options.jsc?.parser?.syntax ?? 'typescript', 15 | decorators: true, 16 | }, 17 | transform: { 18 | ...(options.jsc?.transform ?? {}), 19 | decoratorVersion: '2022-03', 20 | }, 21 | } 22 | }, 23 | }), 24 | tailwindcss(), 25 | ], 26 | }) 27 | -------------------------------------------------------------------------------- /projects/koka-react-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "experimentalDecorators": false, 7 | "lib": ["ES2022", "DOM", "DOM.Iterable", "esnext.decorators"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "erasableSyntaxOnly": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/koka-domain/src/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | export function shallowEqual(objA: any, objB: any): boolean { 2 | if (Object.is(objA, objB)) { 3 | return true 4 | } 5 | 6 | if (typeof objA !== 'object' || !objA || typeof objB !== 'object' || !objB) { 7 | return false 8 | } 9 | 10 | const keysA = Object.keys(objA) 11 | const keysB = Object.keys(objB) 12 | 13 | if (keysA.length !== keysB.length) { 14 | return false 15 | } 16 | 17 | const bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB) 18 | 19 | // Test for A's keys different from B. 20 | for (let idx = 0; idx < keysA.length; idx++) { 21 | const key = keysA[idx] 22 | 23 | if (!bHasOwnProperty(key)) { 24 | return false 25 | } 26 | 27 | const valueA = objA[key] 28 | const valueB = objB[key] 29 | 30 | if (!Object.is(valueA, valueB)) { 31 | return false 32 | } 33 | } 34 | 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /packages/koka/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/*.|\\.(test|spec))\\.(ts|tsx|js|jsx)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 7 | coveragePathIgnorePatterns: ['/example/', '/node_modules/', '/__tests__/'], 8 | coverageThreshold: { 9 | global: { 10 | branches: 90, 11 | functions: 95, 12 | lines: 95, 13 | statements: 95, 14 | }, 15 | }, 16 | collectCoverageFrom: ['./src/**/*.{ts,tsx}'], 17 | rootDir: __dirname, 18 | testEnvironment: 'jsdom', 19 | moduleNameMapper: {}, 20 | testPathIgnorePatterns: ['/node_modules/', '/examples/'], 21 | globals: { 22 | 'ts-jest': { 23 | diagnostics: false, 24 | }, 25 | }, 26 | displayName: { 27 | name: 'koka', 28 | color: 'purple', 29 | }, 30 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 31 | rootDir: __dirname, 32 | } 33 | -------------------------------------------------------------------------------- /packages/koka-accessor/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/*.|\\.(test|spec))\\.(ts|tsx|js|jsx)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 7 | coveragePathIgnorePatterns: ['/example/', '/node_modules/', '/__tests__/'], 8 | coverageThreshold: { 9 | global: { 10 | branches: 90, 11 | functions: 95, 12 | lines: 95, 13 | statements: 95, 14 | }, 15 | }, 16 | collectCoverageFrom: ['./src/**/*.{ts,tsx}'], 17 | rootDir: __dirname, 18 | testEnvironment: 'jsdom', 19 | moduleNameMapper: { 20 | '^koka$': 'koka/src/koka.ts', 21 | '^koka/(.*)$': 'koka/src/$1.ts', 22 | }, 23 | testPathIgnorePatterns: ['/node_modules/', '/examples/'], 24 | globals: { 25 | 'ts-jest': { 26 | diagnostics: false, 27 | }, 28 | }, 29 | displayName: { 30 | name: 'koka-accessor', 31 | color: 'blue', 32 | }, 33 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 34 | rootDir: __dirname, 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 koka-ts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/koka/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 工业聚 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/koka-react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 工业聚 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/koka-accessor/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 工业聚 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/koka-domain/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 工业聚 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/koka-react-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koka-react-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "koka": "workspace:*", 14 | "koka-accessor": "workspace:*", 15 | "koka-react": "workspace:*", 16 | "koka-domain": "workspace:*", 17 | "react": "^19.1.0", 18 | "react-dom": "^19.1.0" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.30.1", 22 | "@tailwindcss/vite": "^4.1.11", 23 | "@types/react": "^19.1.8", 24 | "@types/react-dom": "^19.1.6", 25 | "@vitejs/plugin-react-swc": "^3.10.2", 26 | "eslint": "^9.30.1", 27 | "eslint-plugin-react-hooks": "^5.2.0", 28 | "eslint-plugin-react-refresh": "^0.4.20", 29 | "globals": "^16.3.0", 30 | "tailwindcss": "^4.1.11", 31 | "typescript": "~5.8.3", 32 | "typescript-eslint": "^8.35.1", 33 | "vite": "^7.0.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/test.ts: -------------------------------------------------------------------------------- 1 | import * as Koka from 'koka' 2 | import * as Opt from 'koka/opt' 3 | import * as Async from 'koka/async' 4 | import * as Ctx from 'koka/ctx' 5 | 6 | const actions: string[] = [] 7 | class CleanupOpt extends Opt.Opt('CleanupOpt') {} 8 | class CleanupCtx extends Ctx.Ctx('CleanupCtx') {} 9 | 10 | function* program() { 11 | return yield* Koka.try(function* () { 12 | actions.push('main') 13 | // never resolves 14 | yield* Async.await(new Promise(() => {})) 15 | return 'done' 16 | }).finally(function* () { 17 | const cleanupMode = yield* Opt.get(CleanupOpt) 18 | actions.push(`cleanup: ${cleanupMode ?? 'default'}`) 19 | }) 20 | } 21 | 22 | const controller = new AbortController() 23 | 24 | async function test() { 25 | try { 26 | const result = await Koka.try(program) 27 | .handle({ 28 | [CleanupOpt.field]: 'custom-cleanup', 29 | }) 30 | .runAsync({ abortSignal: controller.signal }) 31 | 32 | console.log(result) 33 | } finally { 34 | console.log(actions) 35 | } 36 | } 37 | 38 | test() 39 | controller.abort() 40 | -------------------------------------------------------------------------------- /packages/koka-accessor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koka-accessor", 3 | "version": "1.0.2", 4 | "description": "A bidirectional data accessors library based on Algebraic Effects and Accessors", 5 | "keywords": [], 6 | "repository": "github:koka-ts/koka", 7 | "bugs": "https://github.com/koka-ts/koka/issues", 8 | "author": "https://github.com/koka-ts", 9 | "type": "module", 10 | "exports": { 11 | ".": "./src/koka-accessor.ts" 12 | }, 13 | "files": [ 14 | "src" 15 | ], 16 | "engines": { 17 | "node": ">=22.18" 18 | }, 19 | "scripts": { 20 | "test": "jest", 21 | "test:coverage": "jest --collectCoverage --coverage" 22 | }, 23 | "dependencies": { 24 | "tslib": "^2.3.1", 25 | "koka": "workspace:*" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^27.5.0", 29 | "jest": "^27.5.1", 30 | "ts-jest": "^27.1.4", 31 | "rimraf": "^3.0.2", 32 | "shx": "^0.3.4", 33 | "tsx": "^4.7.0", 34 | "typescript": "^5.8.0", 35 | "npm-run-all": "^4.1.5" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/koka-domain/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/*.|\\.(test|spec))\\.(ts|tsx|js|jsx)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 7 | coveragePathIgnorePatterns: ['/example/', '/node_modules/', '/__tests__/'], 8 | coverageThreshold: { 9 | global: { 10 | branches: 90, 11 | functions: 95, 12 | lines: 95, 13 | statements: 95, 14 | }, 15 | }, 16 | collectCoverageFrom: ['./src/**/*.{ts,tsx}'], 17 | rootDir: __dirname, 18 | testEnvironment: 'jsdom', 19 | moduleNameMapper: { 20 | '^koka$': 'koka/src/koka.ts', 21 | '^koka/(.*)$': 'koka/src/$1.ts', 22 | '^koka-accessor$': 'koka-accessor/src/koka-accessor.ts', 23 | '^koka-accessor/(.*)$': 'koka-accessor/src/$1.ts', 24 | }, 25 | testPathIgnorePatterns: ['/node_modules/', '/examples/'], 26 | globals: { 27 | 'ts-jest': { 28 | diagnostics: false, 29 | }, 30 | }, 31 | displayName: { 32 | name: 'koka-accessor', 33 | color: 'blue', 34 | }, 35 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 36 | rootDir: __dirname, 37 | } 38 | -------------------------------------------------------------------------------- /packages/koka-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koka-react", 3 | "version": "1.0.0", 4 | "description": "An AI-Oriented React framework based on Algebraic Effects, Accessors and CQRS.", 5 | "keywords": [], 6 | "repository": "github:koka-ts/koka", 7 | "bugs": "https://github.com/koka-ts/koka-react/issues", 8 | "author": "https://github.com/koka-ts", 9 | "type": "module", 10 | "exports": { 11 | ".": "./src/koka-react.tsx" 12 | }, 13 | "files": [ 14 | "src" 15 | ], 16 | "engines": { 17 | "node": ">=22.18" 18 | }, 19 | "scripts": { 20 | "test": "jest", 21 | "test:coverage": "jest --collectCoverage --coverage" 22 | }, 23 | "dependencies": { 24 | "koka": "workspace:*", 25 | "koka-accessor": "workspace:*", 26 | "koka-domain": "workspace:*", 27 | "react": "^18.0.0" 28 | }, 29 | "devDependencies": { 30 | "@testing-library/react": "^16.3.0", 31 | "@testing-library/jest-dom": "^6.6.4", 32 | "@types/jest": "^27.5.0", 33 | "@types/react": "^18.0.0", 34 | "jest": "^27.5.1", 35 | "ts-jest": "^27.1.4", 36 | "typescript": "^5.8.0" 37 | }, 38 | "publishConfig": { 39 | "access": "public" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/koka-react/jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/*.|\\.(test|spec))\\.(ts|tsx|js|jsx)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 7 | coveragePathIgnorePatterns: ['/example/', '/node_modules/', '/__tests__/'], 8 | coverageThreshold: { 9 | global: { 10 | branches: 90, 11 | functions: 95, 12 | lines: 95, 13 | statements: 95, 14 | }, 15 | }, 16 | collectCoverageFrom: ['./src/**/*.{ts,tsx}'], 17 | rootDir: __dirname, 18 | testEnvironment: 'jsdom', 19 | setupFilesAfterEnv: ['/jest.setup.js'], 20 | moduleNameMapper: { 21 | '^koka$': 'koka/src/koka.ts', 22 | '^koka/(.*)$': 'koka/src/$1.ts', 23 | '^koka-accessor$': 'koka-accessor/src/koka-accessor.ts', 24 | '^koka-accessor/(.*)$': 'koka-accessor/src/$1.ts', 25 | '^koka-domain$': 'koka-domain/src/koka-domain.ts', 26 | '^koka-domain/(.*)$': 'koka-domain/src/$1.ts', 27 | }, 28 | testPathIgnorePatterns: ['/node_modules/', '/examples/'], 29 | globals: { 30 | 'ts-jest': { 31 | diagnostics: false, 32 | }, 33 | }, 34 | displayName: { 35 | name: 'koka-react', 36 | color: 'green', 37 | }, 38 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 39 | rootDir: __dirname, 40 | } 41 | -------------------------------------------------------------------------------- /packages/koka-domain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koka-domain", 3 | "version": "1.0.0", 4 | "description": "An AI-Oriented DDD framework based on Algebraic Effects, Accessors and CQRS.", 5 | "keywords": [], 6 | "repository": "github:koka-ts/koka", 7 | "bugs": "https://github.com/koka-ts/koka/issues", 8 | "author": "https://github.com/koka-ts", 9 | "type": "module", 10 | "exports": { 11 | ".": "./src/koka-domain.ts", 12 | "./pretty-browser-logger": "./src/pretty-browser-logger.ts", 13 | "./pretty-cli-logger": "./src/pretty-cli-logger.ts" 14 | }, 15 | "files": [ 16 | "src" 17 | ], 18 | "engines": { 19 | "node": ">=22.18" 20 | }, 21 | "scripts": { 22 | "test": "jest", 23 | "test:coverage": "jest --collectCoverage --coverage" 24 | }, 25 | "dependencies": { 26 | "tslib": "^2.3.1", 27 | "koka": "workspace:*", 28 | "koka-accessor": "workspace:*", 29 | "chalk": "^4.1.2" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^27.5.0", 33 | "jest": "^27.5.1", 34 | "ts-jest": "^27.1.4", 35 | "rimraf": "^3.0.2", 36 | "shx": "^0.3.4", 37 | "tsx": "^4.7.0", 38 | "typescript": "^5.8.0", 39 | "npm-run-all": "^4.1.5", 40 | "@types/chalk": "^2.2.0" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/koka/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koka", 3 | "version": "1.0.9", 4 | "description": "Lightweight 3kB Effect-TS alternative library based on Algebraic Effects", 5 | "keywords": [], 6 | "repository": "github:koka-ts/koka", 7 | "bugs": "https://github.com/koka-ts/koka/issues", 8 | "author": "https://github.com/koka-ts", 9 | "type": "module", 10 | "exports": { 11 | ".": "./src/koka.ts", 12 | "./err": "./src/err.ts", 13 | "./ctx": "./src/ctx.ts", 14 | "./opt": "./src/opt.ts", 15 | "./async": "./src/async.ts", 16 | "./result": "./src/result.ts", 17 | "./task": "./src/task.ts", 18 | "./gen": "./src/gen.ts" 19 | }, 20 | "files": [ 21 | "src" 22 | ], 23 | "engines": { 24 | "node": ">=22.18" 25 | }, 26 | "scripts": { 27 | "test:path": "jest --testPathPattern $1", 28 | "test": "jest", 29 | "test:coverage": "jest --collectCoverage --coverage" 30 | }, 31 | "dependencies": { 32 | "tslib": "^2.3.1" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^27.5.0", 36 | "jest": "^27.5.1", 37 | "ts-jest": "^27.1.4", 38 | "rimraf": "^3.0.2", 39 | "shx": "^0.3.4", 40 | "tsx": "^4.7.0", 41 | "typescript": "^5.8.0", 42 | "npm-run-all": "^4.1.5" 43 | }, 44 | "publishConfig": { 45 | "access": "public" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | test: 17 | # The type of runner that the job will run on 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [22.x, 24.x] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | - name: Check out 27 | uses: actions/checkout@v2 28 | 29 | - uses: pnpm/action-setup@v2.0.1 30 | with: 31 | version: '10' 32 | 33 | - name: Setup Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v1.4.4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: 'pnpm' 38 | 39 | - name: Init 40 | run: pnpm run init 41 | 42 | - name: Build & Test 43 | run: pnpm run build && pnpm run test 44 | -------------------------------------------------------------------------------- /projects/koka-react-demo/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/koka/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | test: 17 | # The type of runner that the job will run on 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [22.x, 24.x] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | - name: Check out 27 | uses: actions/checkout@v2 28 | 29 | - uses: pnpm/action-setup@v2.0.1 30 | with: 31 | version: '10' 32 | 33 | - name: Setup Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v1.4.4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: 'pnpm' 38 | 39 | - name: Init 40 | run: pnpm install 41 | 42 | - name: Build & Test 43 | run: pnpm run build && pnpm run test 44 | -------------------------------------------------------------------------------- /packages/koka-accessor/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | test: 17 | # The type of runner that the job will run on 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [22.x, 24.x] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | - name: Check out 27 | uses: actions/checkout@v2 28 | 29 | - uses: pnpm/action-setup@v2.0.1 30 | with: 31 | version: '10' 32 | 33 | - name: Setup Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v1.4.4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: 'pnpm' 38 | 39 | - name: Init 40 | run: pnpm install 41 | 42 | - name: Build & Test 43 | run: pnpm run build && pnpm run test 44 | -------------------------------------------------------------------------------- /packages/koka-domain/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | test: 17 | # The type of runner that the job will run on 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [22.x, 24.x] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | - name: Check out 27 | uses: actions/checkout@v2 28 | 29 | - uses: pnpm/action-setup@v2.0.1 30 | with: 31 | version: '10' 32 | 33 | - name: Setup Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v1.4.4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: 'pnpm' 38 | 39 | - name: Init 40 | run: pnpm install 41 | 42 | - name: Build & Test 43 | run: pnpm run build && pnpm run test 44 | -------------------------------------------------------------------------------- /packages/koka-react/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | test: 17 | # The type of runner that the job will run on 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | node-version: [22.x, 24.x] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | - name: Check out 27 | uses: actions/checkout@v2 28 | 29 | - uses: pnpm/action-setup@v2.0.1 30 | with: 31 | version: '10' 32 | 33 | - name: Setup Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v1.4.4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: 'pnpm' 38 | 39 | - name: Init 40 | run: pnpm install 41 | 42 | - name: Build & Test 43 | run: pnpm run build && pnpm run test 44 | -------------------------------------------------------------------------------- /packages/koka/__tests__/gen.test.ts: -------------------------------------------------------------------------------- 1 | import * as Gen from '../src/gen' 2 | 3 | describe('Gen', () => { 4 | it('should check if value is a generator', () => { 5 | function* gen() {} 6 | const notGen = () => {} 7 | 8 | expect(Gen.isGen(gen())).toBe(true) 9 | expect(Gen.isGen(notGen())).toBe(false) 10 | }) 11 | 12 | it('should handle different types of generators', () => { 13 | function* emptyGen() {} 14 | function* numberGen() { 15 | yield 1 16 | yield 2 17 | yield 3 18 | } 19 | function* stringGen() { 20 | yield 'hello' 21 | yield 'world' 22 | } 23 | 24 | expect(Gen.isGen(emptyGen())).toBe(true) 25 | expect(Gen.isGen(numberGen())).toBe(true) 26 | expect(Gen.isGen(stringGen())).toBe(true) 27 | }) 28 | 29 | it('should handle non-generator values', () => { 30 | expect(Gen.isGen(42)).toBe(false) 31 | expect(Gen.isGen('hello')).toBe(false) 32 | expect(Gen.isGen({})).toBe(false) 33 | expect(Gen.isGen([])).toBe(false) 34 | expect(Gen.isGen(null)).toBe(false) 35 | expect(Gen.isGen(undefined)).toBe(false) 36 | expect(Gen.isGen(() => {})).toBe(false) 37 | }) 38 | 39 | it('should handle async generators', () => { 40 | async function* asyncGen() { 41 | yield 1 42 | yield 2 43 | } 44 | 45 | expect(Gen.isGen(asyncGen())).toBe(true) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 5 | line-height: 1.5; 6 | font-weight: 400; 7 | 8 | color-scheme: light dark; 9 | color: rgba(255, 255, 255, 0.87); 10 | background-color: #242424; 11 | 12 | font-synthesis: none; 13 | text-rendering: optimizeLegibility; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/koka/src/test.ts: -------------------------------------------------------------------------------- 1 | import * as Koka from './koka.ts' 2 | import * as Async from './async.ts' 3 | import * as Task from './task.ts' 4 | const executionOrder: string[] = [] 5 | const controller = new AbortController() 6 | 7 | function* innerTask(index: number) { 8 | return yield* Koka.try(function* () { 9 | executionOrder.push(`inner-task-${index}-start`) 10 | yield* Async.await(new Promise(() => { })) // Never resolves 11 | executionOrder.push(`inner-task-${index}-end`) // Should not reach here 12 | return `inner-${index}` 13 | }).finally(function* () { 14 | executionOrder.push(`inner-task-${index}-finally`) 15 | }) 16 | } 17 | 18 | function* middleTask() { 19 | return yield* Koka.try(function* () { 20 | executionOrder.push('middle-task-start') 21 | return yield* Task.all([innerTask(0), innerTask(1)]) 22 | }).finally(function* () { 23 | executionOrder.push('middle-task-finally') 24 | }) 25 | } 26 | 27 | function* program() { 28 | return yield* Koka.try(function* () { 29 | executionOrder.push('program-start') 30 | return yield* middleTask() 31 | }).finally(function* () { 32 | executionOrder.push('program-finally') 33 | }) 34 | } 35 | 36 | const promise = Koka.runAsync(program(), { abortSignal: controller.signal }) 37 | 38 | // Abort after a short delay to allow tasks to start 39 | setTimeout(() => controller.abort(), 10) 40 | 41 | promise.then( 42 | (result) => { 43 | console.log('Result:', result) 44 | console.log('Execution Order:', executionOrder) 45 | }, 46 | (error) => { 47 | console.log('Error:', error) 48 | console.log('Execution Order:', executionOrder) 49 | }, 50 | ) -------------------------------------------------------------------------------- /packages/koka-react/src/koka-react.tsx: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from 'react' 2 | import * as Domain from 'koka-domain' 3 | import * as Err from 'koka/err' 4 | import * as Result from 'koka/result' 5 | import * as Accessor from 'koka-accessor' 6 | 7 | export function useDomainResult( 8 | domain: Domain.Domain, 9 | ): Result.Result { 10 | const subscribe = (onStoreChange: () => void) => { 11 | return Domain.subscribeDomainResult(domain, onStoreChange) 12 | } 13 | 14 | const getState = () => { 15 | return Domain.getState(domain) 16 | } 17 | 18 | const result = useSyncExternalStore(subscribe, getState, getState) 19 | 20 | return result 21 | } 22 | 23 | export function useDomainState(domain: Domain.Domain): State { 24 | const result = useDomainResult(domain) 25 | 26 | if (result.type === 'err') { 27 | throw result.error 28 | } 29 | 30 | return result.value 31 | } 32 | 33 | export function useDomainQueryResult( 34 | query: Domain.Query, 35 | ): Result.Result { 36 | const subscribe = (onStoreChange: () => void) => { 37 | return Domain.subscribeQueryResult(query, onStoreChange) 38 | } 39 | 40 | const getState = () => { 41 | return Domain.getQueryResult(query) 42 | } 43 | 44 | const result = useSyncExternalStore(subscribe, getState, getState) 45 | 46 | return result 47 | } 48 | 49 | export function useDomainQuery( 50 | query: Domain.Query, 51 | ): Return { 52 | const result = useDomainQueryResult(query) 53 | 54 | if (result.type === 'err') { 55 | throw result.error 56 | } 57 | 58 | return result.value 59 | } 60 | -------------------------------------------------------------------------------- /packages/koka/__tests__/err.test.ts: -------------------------------------------------------------------------------- 1 | import * as Err from '../src/err' 2 | import * as Result from '../src/result' 3 | 4 | describe('Err', () => { 5 | it('should create error effect class', () => { 6 | class TestErr extends Err.Err('TestErr') {} 7 | const err = new TestErr('error') 8 | 9 | expect(err.type).toBe('err') 10 | expect(err.name).toBe('TestErr') 11 | expect(err.error).toBe('error') 12 | }) 13 | 14 | it('should throw error effect', () => { 15 | class TestErr extends Err.Err('TestErr') {} 16 | 17 | function* test() { 18 | yield* Err.throw(new TestErr('error')) 19 | return 'should not reach here' 20 | } 21 | 22 | const result = Result.runSync(test()) 23 | 24 | expect(result).toEqual(new TestErr('error')) 25 | }) 26 | 27 | it('should propagate error through nested calls', () => { 28 | const TestErr = Err.Err('TestErr') 29 | 30 | function* inner() { 31 | yield* Err.throw(new TestErr('inner error')) 32 | return 'should not reach here' 33 | } 34 | 35 | function* outer() { 36 | return yield* inner() 37 | } 38 | 39 | const result = Result.runSync(outer()) 40 | expect(result).toEqual(new TestErr('inner error')) 41 | }) 42 | 43 | it('should handle complex error types', () => { 44 | class ComplexError extends Err.Err('ComplexError')<{ code: number; message: string }> {} 45 | 46 | function* test() { 47 | yield* Err.throw(new ComplexError({ code: 404, message: 'Not found' })) 48 | return 'should not reach here' 49 | } 50 | 51 | const result = Result.runSync(test()) 52 | expect(result).toEqual(new ComplexError({ code: 404, message: 'Not found' })) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koka", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "author": "https://github.com/Lucifier129", 6 | "private": true, 7 | "engines": { 8 | "node": ">=22.18" 9 | }, 10 | "scripts": { 11 | "clean:deps": "shx rm -rf ./node_modules && shx rm -rf ./packages/*/node_modules && shx rm -rf ./projects/*/node_modules", 12 | "lint": "eslint --report-unused-disable-directives ./packages", 13 | "format": "run-p format:source format:other", 14 | "format:other": "prettier ./**/*.{md,yml,json,html} --write", 15 | "format:source": "prettier ./**/*.{ts,tsx,js,vue} --write", 16 | "init": "pnpm install && pnpm run build", 17 | "test": "pnpm --filter \"./packages/**\" run test", 18 | "test:coverage": "pnpm --filter \"./packages/**\" run test:coverage ", 19 | "publish": "pnpm changeset publish", 20 | "prepare": "husky install", 21 | "prepublish": "pnpm run build", 22 | "version": "pnpm changeset version --access public", 23 | "release": "pnpm run version && pnpm run publish", 24 | "prerelease": "pnpm run format && git add -A && pnpm run build", 25 | "change": "pnpm changeset", 26 | "fix": "eslint --fix ./packages", 27 | "sync-repo": "tsx ./sync-repo.ts", 28 | "sync-repo-all": "tsx ./sync-repo.ts --all" 29 | }, 30 | "devDependencies": { 31 | "@changesets/cli": "^2.22.0", 32 | "@commitlint/cli": "^17.0.3", 33 | "@commitlint/config-conventional": "^17.0.3", 34 | "@types/jest": "^27.5.0", 35 | "@types/node": "^22.14.1", 36 | "@typescript-eslint/eslint-plugin": "^5.23.0", 37 | "@typescript-eslint/parser": "^5.23.0", 38 | "codecov": "^3.8.3", 39 | "commitlint": "^17.0.3", 40 | "eslint": "^8.15.0", 41 | "eslint-config-prettier": "^8.5.0", 42 | "eslint-plugin-jest": "^26.1.5", 43 | "eslint-plugin-prettier": "^4.0.0", 44 | "tslib": "^2.3.1", 45 | "esno": "*", 46 | "husky": "^8.0.1", 47 | "jest": "^27.5.1", 48 | "lint-staged": "^12.4.1", 49 | "npm-run-all": "^4.1.5", 50 | "prettier": "^2.2.1", 51 | "shx": "^0.3.4", 52 | "ts-jest": "^27.1.4", 53 | "tsx": "^4.7.0", 54 | "typescript": "^5.8.0", 55 | "gh-pages": "^6.3.0", 56 | "@types/gh-pages": "^6.1.0", 57 | "globby": "^13.0.0", 58 | "prompts": "^2.4.2", 59 | "@types/prompts": "^2.4.9" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/koka-domain/src/pretty-printer.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionTree, Store } from './koka-domain.ts' 2 | 3 | export const PrettyPrinter = () => { 4 | return (store: Store) => { 5 | store.subscribeExecution((tree) => { 6 | const output = prettyPrintExecutionTree(tree) 7 | console.log(output) 8 | }) 9 | } 10 | } 11 | 12 | export const prettyPrintExecutionTree = (tree: ExecutionTree) => { 13 | const treeLines: string[] = [] 14 | 15 | const formatTree = (node: ExecutionTree, indent = 0) => { 16 | const spaces = ' '.repeat(indent * 2) 17 | 18 | treeLines.push(`${spaces}${node.type === 'command' ? 'Command' : 'Query'}: ${node.commandName}`) 19 | 20 | if (node.args.length > 0) { 21 | treeLines.push(`${spaces}Args: ${JSON.stringify(node.args)}`) 22 | } 23 | 24 | if (node.return !== undefined) { 25 | treeLines.push(`${spaces}Return: ${JSON.stringify(node.return)}`) 26 | } 27 | 28 | if (node.type === 'query' && node.states.length > 0) { 29 | treeLines.push(`${spaces}States:`) 30 | for (const state of node.states) { 31 | treeLines.push(`${spaces} State: ${JSON.stringify(state)}`) 32 | } 33 | } 34 | 35 | if (node.type === 'command' && node.changes.length > 0) { 36 | treeLines.push(`${spaces}State Changes:`) 37 | for (const change of node.changes) { 38 | treeLines.push(`${spaces} Previous: ${JSON.stringify(change.previous)}`) 39 | treeLines.push(`${spaces} Next: ${JSON.stringify(change.next)}`) 40 | } 41 | } 42 | 43 | if (node.type === 'command') { 44 | if (node.commands.length > 0) { 45 | treeLines.push(`${spaces}Sub-Commands:`) 46 | for (const subCmd of node.commands) { 47 | formatTree(subCmd, indent + 1) 48 | } 49 | } 50 | if (node.queries.length > 0) { 51 | treeLines.push(`${spaces}Queries:`) 52 | for (const query of node.queries) { 53 | formatTree(query, indent + 1) 54 | } 55 | } 56 | } else { 57 | if (node.queries.length > 0) { 58 | treeLines.push(`${spaces}Sub-Queries:`) 59 | for (const subQuery of node.queries) { 60 | formatTree(subQuery, indent + 1) 61 | } 62 | } 63 | } 64 | } 65 | 66 | formatTree(tree) 67 | return treeLines.join('\n') 68 | } 69 | -------------------------------------------------------------------------------- /packages/koka/src/result.ts: -------------------------------------------------------------------------------- 1 | import type * as Async from './async.ts' 2 | import type * as Err from './err.ts' 3 | import type * as Opt from './opt.ts' 4 | import * as Koka from './koka.ts' 5 | 6 | export type Ok = { 7 | type: 'ok' 8 | value: T 9 | } 10 | 11 | export type AnyOk = Ok 12 | 13 | export type Result = Ok | (E extends Err.AnyErr ? E : never) 14 | 15 | export type AnyResult = Result 16 | 17 | export const ok = (value: T): Ok => { 18 | return { 19 | type: 'ok', 20 | value, 21 | } 22 | } 23 | 24 | export const err = (name: Name, error: T): Err.Err => { 25 | return { 26 | type: 'err', 27 | name, 28 | error, 29 | } 30 | } 31 | 32 | export type InferOkValue = T extends Ok ? U : never 33 | 34 | export function* wrap( 35 | effector: Koka.Effector, 36 | ): Generator | Koka.Final, Ok | Err.ExtractErr> { 37 | const gen = Koka.readEffector(effector) 38 | try { 39 | let result = gen.next() 40 | 41 | while (!result.done) { 42 | const effect = result.value 43 | 44 | if (effect.type === 'err') { 45 | return effect as Err.ExtractErr 46 | } else { 47 | result = gen.next(yield effect as any) 48 | } 49 | } 50 | 51 | return { 52 | type: 'ok', 53 | value: result.value, 54 | } 55 | } finally { 56 | yield* Koka.cleanUpGen(gen) as Generator 57 | } 58 | } 59 | 60 | /** 61 | * convert a generator to a generator that returns a value 62 | * move the err from return to throw 63 | */ 64 | export function* unwrap( 65 | effector: Koka.Effector, 66 | ): Generator, InferOkValue> { 67 | const gen = Koka.readEffector(effector) 68 | const result = yield* gen 69 | 70 | if (result.type === 'ok') { 71 | return result.value 72 | } else { 73 | throw yield result as Err.ExtractErr 74 | } 75 | } 76 | 77 | export function runSync( 78 | effector: Koka.Effector, 79 | ): Ok | Err.ExtractErr { 80 | return Koka.runSync(wrap(effector) as any) 81 | } 82 | 83 | export function runAsync( 84 | effector: Koka.Effector, 85 | options?: Koka.RunAsyncOptions, 86 | ): Promise | Err.ExtractErr> { 87 | return Koka.runAsync(wrap(effector) as any, options) 88 | } 89 | -------------------------------------------------------------------------------- /projects/koka-react-demo/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config([ 16 | globalIgnores(['dist']), 17 | { 18 | files: ['**/*.{ts,tsx}'], 19 | extends: [ 20 | // Other configs... 21 | 22 | // Remove tseslint.configs.recommended and replace with this 23 | ...tseslint.configs.recommendedTypeChecked, 24 | // Alternatively, use this for stricter rules 25 | ...tseslint.configs.strictTypeChecked, 26 | // Optionally, add this for stylistic rules 27 | ...tseslint.configs.stylisticTypeChecked, 28 | 29 | // Other configs... 30 | ], 31 | languageOptions: { 32 | parserOptions: { 33 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 34 | tsconfigRootDir: import.meta.dirname, 35 | }, 36 | // other options... 37 | }, 38 | }, 39 | ]) 40 | ``` 41 | 42 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 43 | 44 | ```js 45 | // eslint.config.js 46 | import reactX from 'eslint-plugin-react-x' 47 | import reactDom from 'eslint-plugin-react-dom' 48 | 49 | export default tseslint.config([ 50 | globalIgnores(['dist']), 51 | { 52 | files: ['**/*.{ts,tsx}'], 53 | extends: [ 54 | // Other configs... 55 | // Enable lint rules for React 56 | reactX.configs['recommended-typescript'], 57 | // Enable lint rules for React DOM 58 | reactDom.configs.recommended, 59 | ], 60 | languageOptions: { 61 | parserOptions: { 62 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 63 | tsconfigRootDir: import.meta.dirname, 64 | }, 65 | // other options... 66 | }, 67 | }, 68 | ]) 69 | ``` 70 | -------------------------------------------------------------------------------- /packages/koka/__tests__/ctx.test.ts: -------------------------------------------------------------------------------- 1 | import * as Ctx from '../src/ctx' 2 | import * as Koka from '../src/koka' 3 | 4 | describe('Ctx', () => { 5 | it('should create context effect class', () => { 6 | class TestCtx extends Ctx.Ctx('TestCtx') {} 7 | 8 | const ctx = new TestCtx() 9 | ctx.context = 42 10 | 11 | expect(ctx.type).toBe('ctx') 12 | expect(ctx.name).toBe('TestCtx') 13 | expect(ctx.context).toBe(42) 14 | }) 15 | 16 | it('should get context value', () => { 17 | class TestCtx extends Ctx.Ctx('TestCtx') {} 18 | class Num extends Ctx.Ctx('Num') {} 19 | 20 | function* test() { 21 | const value = yield* Ctx.get(TestCtx) 22 | const num = yield* Ctx.get(Num) 23 | return value * num 24 | } 25 | 26 | const program0 = Koka.try(test()).handle({ 27 | Num: 2, 28 | }) 29 | 30 | const program1 = Koka.try(program0).handle({ 31 | TestCtx: 21, 32 | }) 33 | 34 | const result = Koka.runSync(program1) 35 | expect(result).toBe(42) 36 | }) 37 | 38 | it('should propagate context when not handled', () => { 39 | class TestCtx extends Ctx.Ctx('TestCtx') {} 40 | 41 | function* inner() { 42 | return yield* Ctx.get(TestCtx) 43 | } 44 | 45 | function* outer() { 46 | return yield* inner() 47 | } 48 | 49 | const program = Koka.try(outer()).handle({ 50 | TestCtx: 42, 51 | }) 52 | 53 | const result = Koka.runSync(program) 54 | expect(result).toBe(42) 55 | }) 56 | 57 | it('should handle complex context types', () => { 58 | class UserCtx extends Ctx.Ctx('User')<{ id: string; name: string; role: string }> {} 59 | 60 | function* test() { 61 | const user = yield* Ctx.get(UserCtx) 62 | return `${user.name} (${user.role})` 63 | } 64 | 65 | const program = Koka.try(test()).handle({ 66 | User: { id: '1', name: 'Alice', role: 'admin' }, 67 | }) 68 | 69 | const result = Koka.runSync(program) 70 | expect(result).toBe('Alice (admin)') 71 | }) 72 | 73 | it('should handle nested context propagation', () => { 74 | class ConfigCtx extends Ctx.Ctx('Config')<{ apiUrl: string }> {} 75 | class AuthCtx extends Ctx.Ctx('Auth')<{ token: string }> {} 76 | 77 | function* service() { 78 | const config = yield* Ctx.get(ConfigCtx) 79 | const auth = yield* Ctx.get(AuthCtx) 80 | return `${config.apiUrl} with token ${auth.token}` 81 | } 82 | 83 | function* controller() { 84 | return yield* service() 85 | } 86 | 87 | const program = Koka.try(controller()).handle({ 88 | Config: { apiUrl: 'https://api.example.com' }, 89 | Auth: { token: 'secret-token' }, 90 | }) 91 | 92 | const result = Koka.runSync(program) 93 | expect(result).toBe('https://api.example.com with token secret-token') 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /sync-repo.ts: -------------------------------------------------------------------------------- 1 | import { globby } from 'globby' 2 | import prompts from 'prompts' 3 | import ghPages from 'gh-pages' 4 | import path from 'path' 5 | import fs from 'fs' 6 | 7 | type Options = { 8 | all?: boolean 9 | } 10 | 11 | async function publish(pkg: { name: string; path: string }, message?: string) { 12 | const defaultMessage = `Publish ${pkg.name} from koka-stack repository` 13 | 14 | // 4. Publish using gh-pages 15 | const repo = `https://github.com/koka-ts/${pkg.name}.git` 16 | 17 | console.log(`Publishing ${pkg.name}...`, { 18 | path: pkg.path, 19 | repo, 20 | }) 21 | 22 | ghPages.clean() 23 | 24 | await ghPages.publish( 25 | pkg.path, 26 | { 27 | repo: repo, 28 | branch: 'main', 29 | message: message || defaultMessage, 30 | dotfiles: true, 31 | }, 32 | (err) => { 33 | if (err) { 34 | console.error('Publish failed:', err) 35 | process.exit(1) 36 | } 37 | console.log(`${pkg.name} published successfully!`) 38 | }, 39 | ) 40 | } 41 | 42 | async function main(options?: Options) { 43 | // 1. Find all package.json files in packages/* 44 | const packageJsonPaths = await globby('packages/*/package.json') 45 | 46 | // 2. Extract package names 47 | const packages = packageJsonPaths.map((pkgPath) => { 48 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) 49 | return { 50 | name: pkg.name, 51 | path: path.dirname(pkgPath), 52 | } 53 | }) 54 | 55 | if (packages.length === 0) { 56 | console.error('No packages found in packages/* directory') 57 | process.exit(1) 58 | } 59 | 60 | if (options?.all) { 61 | // If --all option is provided, publish all packages 62 | console.log('Publishing all packages...') 63 | await Promise.all(packages.map((pkg) => publish(pkg))) 64 | console.log('All packages published successfully!') 65 | return 66 | } 67 | 68 | // 3. Prompt user to select package 69 | const { package: pkg } = await prompts({ 70 | type: 'select', 71 | name: 'package', 72 | message: 'Select package to publish:', 73 | choices: packages.map((pkg) => ({ 74 | title: pkg.name, 75 | value: pkg, 76 | })), 77 | }) 78 | 79 | if (!pkg) { 80 | console.log('No package selected') 81 | process.exit(0) 82 | } 83 | 84 | const defaultMessage = `Publish ${pkg.name} from koka-stack repository` 85 | 86 | const { message } = await prompts({ 87 | type: 'text', 88 | name: 'message', 89 | message: `Enter commit message for ${pkg.name}:`, 90 | initial: defaultMessage, 91 | }) 92 | 93 | await publish(pkg, message) 94 | } 95 | 96 | const args = process.argv.slice(2) 97 | 98 | const options: Options = { 99 | all: args.includes('--all'), 100 | } 101 | 102 | main(options).catch((err) => { 103 | console.error('Error:', err) 104 | process.exit(1) 105 | }) 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos 3 | # Edit at https://www.gitignore.io/?templates=macos 4 | 5 | pnpm 6 | pnpm.CMD 7 | pnpx 8 | pnpx.CMD 9 | 10 | 11 | ### macOS ### 12 | # General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | legacy 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | # End of https://www.gitignore.io/api/macos 42 | # Created by https://www.gitignore.io/api/node 43 | # Edit at https://www.gitignore.io/?templates=node 44 | 45 | ### Node ### 46 | # Logs 47 | logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | lerna-debug.log* 53 | 54 | # Diagnostic reports (https://nodejs.org/api/report.html) 55 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | *.pid.lock 62 | 63 | .vscode 64 | 65 | # Directory for instrumented libs generated by jscoverage/JSCover 66 | lib-cov 67 | 68 | # Coverage directory used by tools like istanbul 69 | coverage 70 | *.lcov 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variables file 113 | .env 114 | .env.test 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | 119 | # next.js build output 120 | .next 121 | 122 | # nuxt.js build output 123 | .nuxt 124 | 125 | # rollup.js default build output 126 | dist 127 | cjs 128 | esm 129 | 130 | .kiro 131 | 132 | # Uncomment the public line if your project uses Gatsby 133 | # https://nextjs.org/blog/next-9-1#public-directory-support 134 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 135 | # public 136 | 137 | # Storybook build outputs 138 | .out 139 | .storybook-out 140 | 141 | # vuepress build output 142 | .vuepress/dist 143 | 144 | # Serverless directories 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | .fusebox/ 149 | 150 | # DynamoDB Local files 151 | .dynamodb/ 152 | 153 | # Temporary folders 154 | tmp/ 155 | temp/ 156 | 157 | # End of https://www.gitignore.io/api/node 158 | esm 159 | lib 160 | 161 | .idea 162 | 163 | .clinic 164 | 165 | projects/builder/ -------------------------------------------------------------------------------- /packages/koka-accessor/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos 3 | # Edit at https://www.gitignore.io/?templates=macos 4 | 5 | pnpm 6 | pnpm.CMD 7 | pnpx 8 | pnpx.CMD 9 | 10 | 11 | ### macOS ### 12 | # General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | legacy 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | # End of https://www.gitignore.io/api/macos 42 | # Created by https://www.gitignore.io/api/node 43 | # Edit at https://www.gitignore.io/?templates=node 44 | 45 | ### Node ### 46 | # Logs 47 | logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | lerna-debug.log* 53 | 54 | # Diagnostic reports (https://nodejs.org/api/report.html) 55 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | *.pid.lock 62 | 63 | .vscode 64 | 65 | # Directory for instrumented libs generated by jscoverage/JSCover 66 | lib-cov 67 | 68 | # Coverage directory used by tools like istanbul 69 | coverage 70 | *.lcov 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variables file 113 | .env 114 | .env.test 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | 119 | # next.js build output 120 | .next 121 | 122 | # nuxt.js build output 123 | .nuxt 124 | 125 | # rollup.js default build output 126 | dist 127 | cjs 128 | esm 129 | 130 | # Uncomment the public line if your project uses Gatsby 131 | # https://nextjs.org/blog/next-9-1#public-directory-support 132 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 133 | # public 134 | 135 | # Storybook build outputs 136 | .out 137 | .storybook-out 138 | 139 | # vuepress build output 140 | .vuepress/dist 141 | 142 | # Serverless directories 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | .fusebox/ 147 | 148 | # DynamoDB Local files 149 | .dynamodb/ 150 | 151 | # Temporary folders 152 | tmp/ 153 | temp/ 154 | 155 | # End of https://www.gitignore.io/api/node 156 | esm 157 | lib 158 | 159 | .idea 160 | 161 | .clinic 162 | 163 | projects/builder/ -------------------------------------------------------------------------------- /packages/koka-domain/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos 3 | # Edit at https://www.gitignore.io/?templates=macos 4 | 5 | pnpm 6 | pnpm.CMD 7 | pnpx 8 | pnpx.CMD 9 | 10 | 11 | ### macOS ### 12 | # General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | legacy 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | # End of https://www.gitignore.io/api/macos 42 | # Created by https://www.gitignore.io/api/node 43 | # Edit at https://www.gitignore.io/?templates=node 44 | 45 | ### Node ### 46 | # Logs 47 | logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | lerna-debug.log* 53 | 54 | # Diagnostic reports (https://nodejs.org/api/report.html) 55 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | *.pid.lock 62 | 63 | .vscode 64 | 65 | # Directory for instrumented libs generated by jscoverage/JSCover 66 | lib-cov 67 | 68 | # Coverage directory used by tools like istanbul 69 | coverage 70 | *.lcov 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variables file 113 | .env 114 | .env.test 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | 119 | # next.js build output 120 | .next 121 | 122 | # nuxt.js build output 123 | .nuxt 124 | 125 | # rollup.js default build output 126 | dist 127 | cjs 128 | esm 129 | 130 | # Uncomment the public line if your project uses Gatsby 131 | # https://nextjs.org/blog/next-9-1#public-directory-support 132 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 133 | # public 134 | 135 | # Storybook build outputs 136 | .out 137 | .storybook-out 138 | 139 | # vuepress build output 140 | .vuepress/dist 141 | 142 | # Serverless directories 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | .fusebox/ 147 | 148 | # DynamoDB Local files 149 | .dynamodb/ 150 | 151 | # Temporary folders 152 | tmp/ 153 | temp/ 154 | 155 | # End of https://www.gitignore.io/api/node 156 | esm 157 | lib 158 | 159 | .idea 160 | 161 | .clinic 162 | 163 | projects/builder/ -------------------------------------------------------------------------------- /packages/koka-react/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos 3 | # Edit at https://www.gitignore.io/?templates=macos 4 | 5 | pnpm 6 | pnpm.CMD 7 | pnpx 8 | pnpx.CMD 9 | 10 | 11 | ### macOS ### 12 | # General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | legacy 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | # End of https://www.gitignore.io/api/macos 42 | # Created by https://www.gitignore.io/api/node 43 | # Edit at https://www.gitignore.io/?templates=node 44 | 45 | ### Node ### 46 | # Logs 47 | logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | lerna-debug.log* 53 | 54 | # Diagnostic reports (https://nodejs.org/api/report.html) 55 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | *.pid.lock 62 | 63 | .vscode 64 | 65 | # Directory for instrumented libs generated by jscoverage/JSCover 66 | lib-cov 67 | 68 | # Coverage directory used by tools like istanbul 69 | coverage 70 | *.lcov 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variables file 113 | .env 114 | .env.test 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | 119 | # next.js build output 120 | .next 121 | 122 | # nuxt.js build output 123 | .nuxt 124 | 125 | # rollup.js default build output 126 | dist 127 | cjs 128 | esm 129 | 130 | # Uncomment the public line if your project uses Gatsby 131 | # https://nextjs.org/blog/next-9-1#public-directory-support 132 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 133 | # public 134 | 135 | # Storybook build outputs 136 | .out 137 | .storybook-out 138 | 139 | # vuepress build output 140 | .vuepress/dist 141 | 142 | # Serverless directories 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | .fusebox/ 147 | 148 | # DynamoDB Local files 149 | .dynamodb/ 150 | 151 | # Temporary folders 152 | tmp/ 153 | temp/ 154 | 155 | # End of https://www.gitignore.io/api/node 156 | esm 157 | lib 158 | 159 | .idea 160 | 161 | .clinic 162 | 163 | projects/builder/ -------------------------------------------------------------------------------- /packages/koka/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos 3 | # Edit at https://www.gitignore.io/?templates=macos 4 | 5 | pnpm 6 | pnpm.CMD 7 | pnpx 8 | pnpx.CMD 9 | 10 | 11 | ### macOS ### 12 | # General 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | legacy 18 | 19 | # Icon must end with two \r 20 | Icon 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | # End of https://www.gitignore.io/api/macos 42 | # Created by https://www.gitignore.io/api/node 43 | # Edit at https://www.gitignore.io/?templates=node 44 | 45 | ### Node ### 46 | # Logs 47 | logs 48 | *.log 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | lerna-debug.log* 53 | 54 | # Diagnostic reports (https://nodejs.org/api/report.html) 55 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | *.pid.lock 62 | 63 | .vscode 64 | 65 | # Directory for instrumented libs generated by jscoverage/JSCover 66 | lib-cov 67 | 68 | # Coverage directory used by tools like istanbul 69 | coverage 70 | *.lcov 71 | 72 | # nyc test coverage 73 | .nyc_output 74 | 75 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 76 | .grunt 77 | 78 | # Bower dependency directory (https://bower.io/) 79 | bower_components 80 | 81 | # node-waf configuration 82 | .lock-wscript 83 | 84 | # Compiled binary addons (https://nodejs.org/api/addons.html) 85 | build/Release 86 | 87 | # Dependency directories 88 | node_modules/ 89 | jspm_packages/ 90 | 91 | # TypeScript v1 declaration files 92 | typings/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variables file 113 | .env 114 | .env.test 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | 119 | # next.js build output 120 | .next 121 | 122 | # nuxt.js build output 123 | .nuxt 124 | 125 | # rollup.js default build output 126 | dist 127 | cjs 128 | esm 129 | packages/*/dist/ 130 | projects/*/dist/ 131 | packages/*/cjs/ 132 | projects/*/cjs/ 133 | packages/*/esm/ 134 | projects/*/esm/ 135 | 136 | # Uncomment the public line if your project uses Gatsby 137 | # https://nextjs.org/blog/next-9-1#public-directory-support 138 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 139 | # public 140 | 141 | # Storybook build outputs 142 | .out 143 | .storybook-out 144 | 145 | # vuepress build output 146 | .vuepress/dist 147 | 148 | # Serverless directories 149 | .serverless/ 150 | 151 | # FuseBox cache 152 | .fusebox/ 153 | 154 | # DynamoDB Local files 155 | .dynamodb/ 156 | 157 | # Temporary folders 158 | tmp/ 159 | temp/ 160 | 161 | # End of https://www.gitignore.io/api/node 162 | esm 163 | lib 164 | 165 | .idea 166 | 167 | .clinic 168 | 169 | projects/builder/ -------------------------------------------------------------------------------- /packages/koka/__tests__/async.test.ts: -------------------------------------------------------------------------------- 1 | import * as Async from '../src/async' 2 | import * as Koka from '../src/koka' 3 | 4 | describe('Async', () => { 5 | it('should handle basic async operations', async () => { 6 | function* test() { 7 | const value = yield* Async.await(Promise.resolve(42)) 8 | return value * 2 9 | } 10 | 11 | const result = await Koka.runAsync(test()) 12 | expect(result).toBe(84) 13 | }) 14 | 15 | it('should handle async errors', async () => { 16 | function* test() { 17 | try { 18 | yield* Async.await(Promise.reject(new Error('Async error'))) 19 | return 'should not reach here' 20 | } catch (err) { 21 | if (err instanceof Error) { 22 | return `Caught: ${err.message}` 23 | } 24 | throw err 25 | } 26 | } 27 | 28 | const result = await Koka.runAsync(test()) 29 | expect(result).toBe('Caught: Async error') 30 | }) 31 | 32 | it('should handle mixed sync and async operations', async () => { 33 | function* test() { 34 | const syncValue = 10 35 | const asyncValue = yield* Async.await(Promise.resolve(32)) 36 | return syncValue + asyncValue 37 | } 38 | 39 | const result = await Koka.runAsync(test()) 40 | expect(result).toBe(42) 41 | }) 42 | 43 | it('should handle multiple async operations', async () => { 44 | function* test() { 45 | const value1 = yield* Async.await(Promise.resolve(1)) 46 | const value2 = yield* Async.await(Promise.resolve(2)) 47 | const value3 = yield* Async.await(Promise.resolve(3)) 48 | return value1 + value2 + value3 49 | } 50 | 51 | const result = await Koka.runAsync(test()) 52 | expect(result).toBe(6) 53 | }) 54 | 55 | it('should handle async operations with delays', async () => { 56 | function* test() { 57 | const start = Date.now() 58 | yield* Async.await(new Promise((resolve) => setTimeout(resolve, 10))) 59 | const end = Date.now() 60 | return end - start 61 | } 62 | 63 | const result = await Koka.runAsync(test()) 64 | expect(result).toBeGreaterThanOrEqual(10) 65 | }) 66 | 67 | it('should handle async operations with complex data', async () => { 68 | function* test() { 69 | const user = yield* Async.await( 70 | Promise.resolve({ 71 | id: 1, 72 | name: 'Alice', 73 | email: 'alice@example.com', 74 | }), 75 | ) 76 | return `${user.name} (${user.email})` 77 | } 78 | 79 | const result = await Koka.runAsync(test()) 80 | expect(result).toBe('Alice (alice@example.com)') 81 | }) 82 | 83 | it('should handle async operations with arrays', async () => { 84 | function* test() { 85 | const numbers = yield* Async.await(Promise.resolve([1, 2, 3, 4, 5])) 86 | return numbers.reduce((sum, num) => sum + num, 0) 87 | } 88 | 89 | const result = await Koka.runAsync(test()) 90 | expect(result).toBe(15) 91 | }) 92 | 93 | it('should handle async operations with null and undefined', async () => { 94 | function* test() { 95 | const nullValue = yield* Async.await(Promise.resolve(null)) 96 | const undefinedValue = yield* Async.await(Promise.resolve(undefined)) 97 | return { nullValue, undefinedValue } 98 | } 99 | 100 | const result = await Koka.runAsync(test()) 101 | expect(result).toEqual({ nullValue: null, undefinedValue: undefined }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/koka-domain/README.md: -------------------------------------------------------------------------------- 1 | # koka-domain - Domain Modeling with Effects 2 | 3 | **Warning: This library is in early development and may change significantly. Do not use in production yet.** 4 | 5 | Koka DDD provides a domain modeling framework built on Koka's algebraic effects system, featuring: 6 | 7 | - **Domain Modeling**: Type-safe domain definitions 8 | - **Store Pattern**: Centralized state management 9 | - **Command/Query Separation**: Clear distinction between writes and reads 10 | - **Effect Integration**: Seamless Koka effects usage 11 | - **Accessors Support**: Composable data access patterns 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install koka-domain 17 | # or 18 | yarn add koka-domain 19 | # or 20 | pnpm add koka-domain 21 | ``` 22 | 23 | ## Core Concepts 24 | 25 | ### Domain Modeling 26 | 27 | Define your domain using the `Domain` base class: 28 | 29 | ```typescript 30 | import { Domain } from 'koka-domain' 31 | 32 | class TodoDomain extends Domain { 33 | text = new TextDomain(this.$prop('text')) 34 | done = new BoolDomain(this.$prop('done')) 35 | 36 | *updateText(newText: string) { 37 | yield* this.text.updateText(newText) 38 | } 39 | } 40 | ``` 41 | 42 | ### Store Pattern 43 | 44 | Manage application state with the `Store` class: 45 | 46 | ```typescript 47 | import { Store } from 'koka-domain' 48 | 49 | const store = new Store({ 50 | state: { 51 | todos: [], 52 | filter: 'all', 53 | }, 54 | }) 55 | ``` 56 | 57 | ### Commands and Queries 58 | 59 | ```typescript 60 | // Query example 61 | const todos = store.get(todoListDomain) 62 | 63 | // Command example 64 | store.runCommand(todoDomain.addTodo('New task')) 65 | ``` 66 | 67 | ## Complete Todo App Example 68 | 69 | ```typescript 70 | import { Domain, Store } from 'koka-domain' 71 | 72 | // Define domains 73 | class TodoDomain extends Domain { 74 | text = new TextDomain(this.$prop('text')) 75 | done = new BoolDomain(this.$prop('done')) 76 | 77 | *toggle() { 78 | yield* this.done.toggle() 79 | } 80 | } 81 | 82 | class TodoListDomain extends Domain { 83 | *addTodo(text: string) { 84 | const newTodo = { id: Date.now(), text, done: false } 85 | yield* set(this, todos => [...todos, newTodo]) 86 | } 87 | 88 | todo(id: number) { 89 | return new TodoDomain(this.$find(todo => todo.id === id)) 90 | } 91 | } 92 | 93 | // Create store 94 | const store = new Store({ 95 | state: { 96 | todos: [] 97 | } 98 | }) 99 | 100 | // Usage 101 | store.runCommand(todoListDomain.addTodo('Learn Koka DDD')) 102 | store.runCommand(todoListDomain.todo(1).toggle()) 103 | ``` 104 | 105 | ## Advanced Patterns 106 | 107 | ### Nested Domains 108 | 109 | ```typescript 110 | class AppDomain extends Domain { 111 | todos = new TodoListDomain(this.$prop('todos')) 112 | user = new UserDomain(this.$prop('user')) 113 | } 114 | ``` 115 | 116 | ### Async Operations 117 | 118 | ```typescript 119 | *loadTodos() { 120 | const response = yield* Eff.await(fetch('/todos')) 121 | const todos = yield* Eff.await(response.json()) 122 | yield* set(this, todos) 123 | } 124 | ``` 125 | 126 | ## Testing 127 | 128 | Tests follow the same patterns as production code: 129 | 130 | ```typescript 131 | test('should add todo', async () => { 132 | await store.runCommand(todos.addTodo('Test')) 133 | expect(store.getState().todos.length).toBe(1) 134 | }) 135 | ``` 136 | 137 | ## API Reference 138 | 139 | ### Domain 140 | 141 | - `$prop()`: Create property accessor 142 | - `$find()`: Find item in collection 143 | - `$filter()`: Filter collection 144 | - `$map()`: Transform values 145 | 146 | ### Store 147 | 148 | - `get(domain)`: Query state 149 | - `runCommand(command)`: Execute state mutation 150 | - `subscribe(listener)`: React to changes 151 | 152 | ## Contributing 153 | 154 | 1. Ensure tests pass (`npm test`) 155 | 2. Update documentation 156 | 3. Follow existing patterns 157 | 158 | ## License 159 | 160 | MIT 161 | -------------------------------------------------------------------------------- /packages/koka-react/README.md: -------------------------------------------------------------------------------- 1 | # koka-domain - Domain Modeling with Effects 2 | 3 | **Warning: This library is in early development and may change significantly. Do not use in production yet.** 4 | 5 | Koka-Domain provides a domain modeling framework built on Koka's algebraic effects system, featuring: 6 | 7 | - **Domain Modeling**: Type-safe domain definitions 8 | - **Store Pattern**: Centralized state management 9 | - **Command/Query Separation**: Clear distinction between writes and reads 10 | - **Effect Integration**: Seamless Koka effects usage 11 | - **Accessors Support**: Composable data access patterns 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install koka-domain 17 | # or 18 | yarn add koka-domain 19 | # or 20 | pnpm add koka-domain 21 | ``` 22 | 23 | ## Core Concepts 24 | 25 | ### Domain Modeling 26 | 27 | Define your domain using the `Domain` base class: 28 | 29 | ```typescript 30 | import { Domain } from 'koka-domain' 31 | 32 | class TodoDomain extends Domain { 33 | text = new TextDomain(this.$prop('text')) 34 | done = new BoolDomain(this.$prop('done')) 35 | 36 | *updateText(newText: string) { 37 | yield* this.text.updateText(newText) 38 | } 39 | } 40 | ``` 41 | 42 | ### Store Pattern 43 | 44 | Manage application state with the `Store` class: 45 | 46 | ```typescript 47 | import { Store } from 'koka-domain' 48 | 49 | const store = new Store({ 50 | state: { 51 | todos: [], 52 | filter: 'all', 53 | }, 54 | }) 55 | ``` 56 | 57 | ### Commands and Queries 58 | 59 | ```typescript 60 | // Query example 61 | const todos = store.get(todoListDomain) 62 | 63 | // Command example 64 | store.runCommand(todoDomain.addTodo('New task')) 65 | ``` 66 | 67 | ## Complete Todo App Example 68 | 69 | ```typescript 70 | import { Domain, Store } from 'koka-domain' 71 | 72 | // Define domains 73 | class TodoDomain extends Domain { 74 | text = new TextDomain(this.$prop('text')) 75 | done = new BoolDomain(this.$prop('done')) 76 | 77 | *toggle() { 78 | yield* this.done.toggle() 79 | } 80 | } 81 | 82 | class TodoListDomain extends Domain { 83 | *addTodo(text: string) { 84 | const newTodo = { id: Date.now(), text, done: false } 85 | yield* set(this, todos => [...todos, newTodo]) 86 | } 87 | 88 | todo(id: number) { 89 | return new TodoDomain(this.$find(todo => todo.id === id)) 90 | } 91 | } 92 | 93 | // Create store 94 | const store = new Store({ 95 | state: { 96 | todos: [] 97 | } 98 | }) 99 | 100 | // Usage 101 | store.runCommand(todoListDomain.addTodo('Learn Koka Domain')) 102 | store.runCommand(todoListDomain.todo(1).toggle()) 103 | ``` 104 | 105 | ## Advanced Patterns 106 | 107 | ### Nested Domains 108 | 109 | ```typescript 110 | class AppDomain extends Domain { 111 | todos = new TodoListDomain(this.$prop('todos')) 112 | user = new UserDomain(this.$prop('user')) 113 | } 114 | ``` 115 | 116 | ### Async Operations 117 | 118 | ```typescript 119 | *loadTodos() { 120 | const response = yield* Eff.await(fetch('/todos')) 121 | const todos = yield* Eff.await(response.json()) 122 | yield* set(this, todos) 123 | } 124 | ``` 125 | 126 | ## Testing 127 | 128 | Tests follow the same patterns as production code: 129 | 130 | ```typescript 131 | test('should add todo', async () => { 132 | await store.runCommand(todos.addTodo('Test')) 133 | expect(store.getState().todos.length).toBe(1) 134 | }) 135 | ``` 136 | 137 | ## API Reference 138 | 139 | ### Domain 140 | 141 | - `$prop()`: Create property accessor 142 | - `$find()`: Find item in collection 143 | - `$filter()`: Filter collection 144 | - `$map()`: Transform values 145 | 146 | ### Store 147 | 148 | - `get(domain)`: Query state 149 | - `runCommand(command)`: Execute state mutation 150 | - `subscribe(listener)`: React to changes 151 | 152 | ## Contributing 153 | 154 | 1. Ensure tests pass (`npm test`) 155 | 2. Update documentation 156 | 3. Follow existing patterns 157 | 158 | ## License 159 | 160 | MIT 161 | -------------------------------------------------------------------------------- /packages/koka/__tests__/opt.test.ts: -------------------------------------------------------------------------------- 1 | import * as Opt from '../src/opt' 2 | import * as Koka from '../src/koka' 3 | import * as Async from '../src/async' 4 | 5 | describe('Opt', () => { 6 | it('should return undefined when no value provided', () => { 7 | class TestOpt extends Opt.Opt('TestOpt') {} 8 | 9 | function* test() { 10 | return yield* Opt.get(TestOpt) 11 | } 12 | 13 | const result = Koka.runSync(test()) 14 | expect(result).toBeUndefined() 15 | }) 16 | 17 | it('should return value when provided', () => { 18 | class TestOpt extends Opt.Opt('TestOpt') {} 19 | 20 | function* test() { 21 | const optValue = yield* Opt.get(TestOpt) 22 | return optValue ?? 42 23 | } 24 | 25 | const result = Koka.runSync(Koka.try(test()).handle({ TestOpt: 21 })) 26 | expect(result).toBe(21) 27 | }) 28 | 29 | it('should work with async effects', async () => { 30 | class TestOpt extends Opt.Opt('TestOpt') {} 31 | 32 | function* test() { 33 | const optValue = yield* Opt.get(TestOpt) 34 | const asyncValue = yield* Async.await(Promise.resolve(optValue ?? 42)) 35 | return asyncValue 36 | } 37 | 38 | const result = await Koka.runAsync(test()) 39 | expect(result).toBe(42) 40 | }) 41 | 42 | it('should handle undefined context value', () => { 43 | class TestOpt extends Opt.Opt('TestOpt') {} 44 | 45 | function* test() { 46 | const optValue = yield* Opt.get(TestOpt) 47 | return optValue ?? 100 48 | } 49 | 50 | const result = Koka.runSync(Koka.try(test()).handle({ TestOpt: undefined })) 51 | expect(result).toBe(100) 52 | }) 53 | 54 | it('should handle complex optional types', () => { 55 | class ConfigOpt extends Opt.Opt('Config')<{ theme: string; language: string }> {} 56 | 57 | function* test() { 58 | const config = yield* Opt.get(ConfigOpt) 59 | return config ?? { theme: 'dark', language: 'en' } 60 | } 61 | 62 | const result = Koka.runSync( 63 | Koka.try(test()).handle({ 64 | Config: { theme: 'light', language: 'zh' }, 65 | }), 66 | ) 67 | expect(result).toEqual({ theme: 'light', language: 'zh' }) 68 | }) 69 | 70 | it('should handle optional functions', () => { 71 | class LoggerOpt extends Opt.Opt('Logger')<(message: string) => void> {} 72 | 73 | function* test() { 74 | const logger = yield* Opt.get(LoggerOpt) 75 | const message = 'Hello, World!' 76 | logger?.(message) 77 | return message 78 | } 79 | 80 | const logs: string[] = [] 81 | const logger = (message: string) => logs.push(message) 82 | 83 | const result = Koka.runSync(Koka.try(test()).handle({ Logger: logger })) 84 | expect(result).toBe('Hello, World!') 85 | expect(logs).toEqual(['Hello, World!']) 86 | }) 87 | 88 | it('should handle multiple optional effects', () => { 89 | class ThemeOpt extends Opt.Opt('Theme') {} 90 | class LanguageOpt extends Opt.Opt('Language') {} 91 | class DebugOpt extends Opt.Opt('Debug') {} 92 | 93 | function* test() { 94 | const theme = yield* Opt.get(ThemeOpt) 95 | const language = yield* Opt.get(LanguageOpt) 96 | const debug = yield* Opt.get(DebugOpt) 97 | 98 | return { 99 | theme: theme ?? 'dark', 100 | language: language ?? 'en', 101 | debug: debug ?? false, 102 | } 103 | } 104 | 105 | const result = Koka.runSync( 106 | Koka.try(test()).handle({ 107 | Theme: 'light', 108 | Debug: true, 109 | }), 110 | ) 111 | 112 | expect(result).toEqual({ 113 | theme: 'light', 114 | language: 'en', 115 | debug: true, 116 | }) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /packages/koka-domain/__tests__/koka-domain-01.test.ts: -------------------------------------------------------------------------------- 1 | import * as Accessor from 'koka-accessor' 2 | import * as Async from 'koka/async' 3 | import * as Koka from 'koka' 4 | import * as Result from 'koka/result' 5 | import * as Err from 'koka/err' 6 | import { Domain, Store, get, set } from '../src/koka-domain.ts' 7 | 8 | type Todo = { 9 | id: number 10 | text: string 11 | done: boolean 12 | } 13 | 14 | type TodoApp = { 15 | todos: Todo[] 16 | filter: 'all' | 'done' | 'undone' 17 | input: string 18 | } 19 | 20 | class TextDomain extends Domain { 21 | *updateText(text: string) { 22 | yield* set(this, text) 23 | return 'text updated' 24 | } 25 | *clearText() { 26 | yield* set(this, '') 27 | return 'text cleared' 28 | } 29 | } 30 | 31 | class BoolDomain extends Domain { 32 | *toggle() { 33 | yield* set(this, (value) => !value) 34 | return 'bool toggled' 35 | } 36 | } 37 | 38 | class TodoDomain extends Domain { 39 | text$ = new TextDomain(this.prop('text')) 40 | done$ = new BoolDomain(this.prop('done')); 41 | 42 | *updateTodoText(text: string) { 43 | const done = yield* get(this.done$) 44 | yield* this.text$.updateText(text) 45 | return 'todo updated' 46 | } 47 | 48 | *toggleTodo() { 49 | yield* this.done$.toggle() 50 | return 'todo toggled' 51 | } 52 | } 53 | 54 | let todoUid = 0 55 | 56 | class TodoListDomain extends Domain { 57 | *addTodo(text: string) { 58 | const newTodo = { 59 | id: todoUid++, 60 | text, 61 | done: false, 62 | } 63 | yield* set(this, (todos) => [...todos, newTodo]) 64 | 65 | return 'todo added' 66 | } 67 | 68 | todo(id: number) { 69 | return new TodoDomain(this.find((todo) => todo.id === id)) 70 | } 71 | 72 | getKey = (todo: Todo) => { 73 | return todo.id 74 | } 75 | 76 | completedTodoList$ = this.filter((todo) => todo.done) 77 | activeTodoList$ = this.filter((todo) => !todo.done) 78 | activeTodoTextList$ = this.activeTodoList$.map((todo$) => todo$.prop('text')) 79 | completedTodoTextList$ = this.completedTodoList$.map((todo$) => todo$.prop('text')) 80 | } 81 | 82 | class TodoInputErr extends Err.Err('TodoInputErr') {} 83 | 84 | class TodoAppDomain extends Domain { 85 | todos$ = new TodoListDomain(this.prop('todos')) 86 | input$ = new TextDomain(this.prop('input')); 87 | 88 | *addTodo() { 89 | const todoApp = yield* get(this) 90 | 91 | if (todoApp.input === '') { 92 | throw yield* Err.throw(new TodoInputErr('Input is empty')) 93 | } 94 | 95 | yield* this.todos$.addTodo(todoApp.input) 96 | yield* this.updateInput('') 97 | return 'Todo added' 98 | } 99 | 100 | *updateInput(input: string) { 101 | yield* Async.await(Promise.resolve('test async')) 102 | yield* this.input$.updateText(input) 103 | return 'Input updated' 104 | } 105 | } 106 | 107 | describe('TodoAppStore', () => { 108 | let store: Store 109 | 110 | beforeEach(() => { 111 | const state: TodoApp = { 112 | todos: [], 113 | filter: 'all', 114 | input: '', 115 | } 116 | 117 | store = new Store({ 118 | state, 119 | }) 120 | }) 121 | 122 | it('should add a todo', async () => { 123 | const todoAppDomain = new TodoAppDomain(store.domain) 124 | const result: Promise> = Result.runAsync(todoAppDomain.addTodo()) 125 | 126 | expect(await result).toEqual({ 127 | type: 'err', 128 | name: 'TodoInputErr', 129 | error: 'Input is empty', 130 | }) 131 | 132 | const result2: Promise> = Result.runAsync(todoAppDomain.updateInput('test')) 133 | expect(await result2).toEqual({ 134 | type: 'ok', 135 | value: 'Input updated', 136 | }) 137 | 138 | const result3: Promise> = Result.runAsync(todoAppDomain.addTodo()) 139 | 140 | expect(await result3).toEqual({ 141 | type: 'ok', 142 | value: 'Todo added', 143 | }) 144 | 145 | const todos: Result.Result = Result.runSync(get(todoAppDomain.todos$)) 146 | 147 | expect(todos).toEqual({ 148 | type: 'ok', 149 | value: [ 150 | { 151 | id: 0, 152 | text: 'test', 153 | done: false, 154 | }, 155 | ], 156 | }) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /packages/koka/docs/index.md: -------------------------------------------------------------------------------- 1 | # Koka Documentation Index 2 | 3 | Welcome to the Koka documentation! This index provides quick navigation to all documentation sections. 4 | 5 | ## 📚 Tutorials (Learning-oriented) 6 | 7 | Start here if you're new to Koka or algebraic effects: 8 | 9 | - **[Getting Started](./tutorials/getting-started.md)** - Your first steps with Koka 10 | - **[Core Concepts](./tutorials/core-concepts.md)** - Understanding Algebraic Effects 11 | - **[Error Handling](./tutorials/error-handling.md)** - Managing errors with effects 12 | - **[Context Management](./tutorials/context-management.md)** - Working with context effects 13 | - **[Async Operations](./tutorials/async-operations.md)** - Handling asynchronous code 14 | - **[Task Management](./tutorials/task-management.md)** - Managing concurrent tasks 15 | 16 | ## 🛠️ How-to Guides (Task-oriented) 17 | 18 | Solve specific problems with Koka: 19 | 20 | - **[Handle Multiple Effects](./how-to/handle-multiple-effects.md)** - Working with multiple effect types 21 | - **[Create Custom Effects](./how-to/create-custom-effects.md)** - Building your own effects 22 | - **[Migrate from Effect-TS](./how-to/migrate-from-effect-ts.md)** - Transitioning from Effect-TS 23 | - **[Debug Effects](./how-to/debug-effects.md)** - Troubleshooting effectful code 24 | - **[Test Effectful Code](./how-to/test-effectful-code.md)** - Testing strategies 25 | - **[Performance Optimization](./how-to/performance-optimization.md)** - Optimizing your code 26 | 27 | ## 📖 Reference (Information-oriented) 28 | 29 | Quick lookups and API documentation: 30 | 31 | - **[API Reference](./reference/api.md)** - Complete API documentation 32 | - **[Effect Types](./reference/effect-types.md)** - All available effect types 33 | - **[Type Definitions](./reference/types.md)** - TypeScript type definitions 34 | - **[Configuration](./reference/configuration.md)** - Configuration options 35 | 36 | ## 💡 Explanations (Understanding-oriented) 37 | 38 | Deep dives into concepts and decisions: 39 | 40 | - **[Algebraic Effects Explained](./explanations/algebraic-effects.md)** - Theory behind algebraic effects 41 | - **[Comparison with Effect-TS](./explanations/effect-ts-comparison.md)** - Detailed comparison 42 | - **[Design Decisions](./explanations/design-decisions.md)** - Why Koka is designed this way 43 | - **[Performance Characteristics](./explanations/performance.md)** - Performance analysis 44 | - **[Best Practices](./explanations/best-practices.md)** - Recommended patterns 45 | 46 | ## 🚀 Quick Start 47 | 48 | ```typescript 49 | import * as Koka from 'koka' 50 | import * as Err from 'koka/err' 51 | import * as Ctx from 'koka/ctx' 52 | 53 | // Define effects 54 | class UserNotFound extends Err.Err('UserNotFound') {} 55 | class AuthToken extends Ctx.Ctx('AuthToken') {} 56 | 57 | // Write effectful code 58 | function* getUser(id: string) { 59 | const token = yield* Ctx.get(AuthToken) 60 | 61 | if (!token) { 62 | yield* Err.throw(new UserNotFound('No auth token')) 63 | } 64 | 65 | return { id, name: 'John Doe' } 66 | } 67 | 68 | // Handle effects 69 | const program = Koka.try(getUser('123')).handle({ 70 | UserNotFound: (error) => ({ error }), 71 | AuthToken: 'secret-token', 72 | }) 73 | 74 | const result = Koka.run(program) 75 | ``` 76 | 77 | ## 📋 Documentation Structure 78 | 79 | This documentation follows the [Diátaxis](https://diataxis.fr/) framework: 80 | 81 | - **Tutorials** help you learn Koka step by step 82 | - **How-to guides** show you how to solve specific problems 83 | - **Reference** provides complete API documentation 84 | - **Explanations** help you understand concepts and decisions 85 | 86 | ## 🎯 Choose Your Path 87 | 88 | ### New to Algebraic Effects? 89 | 90 | Start with [Getting Started](./tutorials/getting-started.md) and [Core Concepts](./tutorials/core-concepts.md) 91 | 92 | ### Coming from Effect-TS? 93 | 94 | Check out [Migration Guide](./how-to/migrate-from-effect-ts.md) and [Comparison](./explanations/effect-ts-comparison.md) 95 | 96 | ### Need to Solve a Specific Problem? 97 | 98 | Browse the [How-to Guides](./how-to/) section 99 | 100 | ### Looking for API Details? 101 | 102 | Go to [API Reference](./reference/api.md) 103 | 104 | ### Want to Understand the Design? 105 | 106 | Read [Design Decisions](./explanations/design-decisions.md) and [Best Practices](./explanations/best-practices.md) 107 | 108 | ## 🔗 External Resources 109 | 110 | - [GitHub Repository](https://github.com/koka-ts/koka) 111 | - [NPM Package](https://www.npmjs.com/package/koka) 112 | - [Effect-TS Documentation](https://effect.website/) (for comparison) 113 | - [Algebraic Effects Research](https://en.wikipedia.org/wiki/Algebraic_effect) 114 | 115 | ## 🤝 Contributing 116 | 117 | Found an issue or want to improve the documentation? Please contribute! 118 | 119 | - [GitHub Issues](https://github.com/koka-ts/koka/issues) 120 | - [GitHub Discussions](https://github.com/koka-ts/koka/discussions) 121 | - [Contributing Guide](../../CONTRIBUTING.md) 122 | 123 | ## 📄 License 124 | 125 | MIT License - see [LICENSE](../../LICENSE) for details. 126 | -------------------------------------------------------------------------------- /packages/koka/docs/tutorials/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Koka 2 | 3 | Welcome to Koka! This tutorial will guide you through your first steps with algebraic effects in TypeScript. 4 | 5 | ## What You'll Learn 6 | 7 | - How to install and set up Koka 8 | - Basic concepts of algebraic effects 9 | - Writing your first effectful program 10 | - Handling effects with type safety 11 | 12 | ## Prerequisites 13 | 14 | - Node.js >= 22.18 15 | - TypeScript >= 5.0 16 | - Basic understanding of TypeScript and generators 17 | 18 | ## Installation 19 | 20 | First, install Koka in your project: 21 | 22 | ```bash 23 | npm install koka 24 | # or 25 | yarn add koka 26 | # or 27 | pnpm add koka 28 | ``` 29 | 30 | ## Your First Effectful Program 31 | 32 | Let's start with a simple example that demonstrates the core concepts of Koka. 33 | 34 | ### Step 1: Import the Core Modules 35 | 36 | ```typescript 37 | import * as Koka from 'koka' 38 | import * as Err from 'koka/err' 39 | import * as Ctx from 'koka/ctx' 40 | ``` 41 | 42 | ### Step 2: Define Your Effects 43 | 44 | Effects in Koka are classes that represent different types of operations. Let's create some basic effects: 45 | 46 | ```typescript 47 | // Error effect for when a user is not found 48 | class UserNotFound extends Err.Err('UserNotFound') {} 49 | 50 | // Context effect for authentication token 51 | class AuthToken extends Ctx.Ctx('AuthToken') {} 52 | ``` 53 | 54 | ### Step 3: Write Effectful Code 55 | 56 | Now let's write a function that uses these effects: 57 | 58 | ```typescript 59 | function* getUser(id: string) { 60 | // Get the auth token from context 61 | const token = yield* Ctx.get(AuthToken) 62 | 63 | // Check if we have a valid token 64 | if (!token) { 65 | // Throw an error effect if no token is available 66 | yield* Err.throw(new UserNotFound('No authentication token provided')) 67 | } 68 | 69 | // Simulate fetching user data 70 | const user = { id, name: 'John Doe', email: 'john@example.com' } 71 | 72 | return user 73 | } 74 | ``` 75 | 76 | ### Step 4: Handle Effects 77 | 78 | The magic happens when we handle these effects. We provide implementations for each effect type: 79 | 80 | ```typescript 81 | // Create a program that handles the effects 82 | const program = Koka.try(getUser('123')).handle({ 83 | // Handle the UserNotFound error 84 | UserNotFound: (error) => ({ error, status: 'error' }), 85 | // Provide the auth token 86 | AuthToken: 'secret-token-123', 87 | }) 88 | 89 | // Run the program 90 | const result = Koka.run(program) 91 | console.log(result) // { id: '123', name: 'John Doe', email: 'john@example.com' } 92 | ``` 93 | 94 | ## Understanding What Happened 95 | 96 | Let's break down what we just did: 97 | 98 | 1. **Effect Definition**: We defined two types of effects: 99 | 100 | - `UserNotFound`: An error effect that can be thrown 101 | - `AuthToken`: A context effect that provides a value 102 | 103 | 2. **Effectful Code**: Our `getUser` function uses `yield*` to: 104 | 105 | - Request the auth token from context 106 | - Potentially throw an error if no token is available 107 | 108 | 3. **Effect Handling**: We provided implementations for each effect: 109 | 110 | - `UserNotFound`: A function that handles the error 111 | - `AuthToken`: A value that provides the token 112 | 113 | 4. **Program Execution**: Koka runs our code and automatically: 114 | - Provides the auth token when requested 115 | - Handles any errors that are thrown 116 | 117 | ## Key Concepts 118 | 119 | ### Generators and Effects 120 | 121 | Koka uses TypeScript generators (`function*`) to represent effectful computations. The `yield*` keyword is used to perform effects. 122 | 123 | ### Effect Types 124 | 125 | Koka supports three main types of effects: 126 | 127 | - **Error Effects** (`Err`): For throwing and handling errors 128 | - **Context Effects** (`Ctx`): For dependency injection and configuration 129 | - **Optional Effects** (`Opt`): For optional dependencies 130 | 131 | ### Effect Handling 132 | 133 | Effects are handled using the `Koka.try().handle()` pattern, which provides type-safe implementations for each effect type. 134 | 135 | ## Next Steps 136 | 137 | Now that you understand the basics, you can: 138 | 139 | - Learn about [Core Concepts](./core-concepts.md) to dive deeper into algebraic effects 140 | - Explore [Error Handling](./error-handling.md) for more sophisticated error management 141 | - Discover [Context Management](./context-management.md) for dependency injection patterns 142 | - Try [Async Operations](./async-operations.md) for handling asynchronous code 143 | 144 | ## Complete Example 145 | 146 | Here's the complete working example: 147 | 148 | ```typescript 149 | import * as Koka from 'koka' 150 | import * as Err from 'koka/err' 151 | import * as Ctx from 'koka/ctx' 152 | 153 | // Define effects 154 | class UserNotFound extends Err.Err('UserNotFound') {} 155 | class AuthToken extends Ctx.Ctx('AuthToken') {} 156 | 157 | // Write effectful code 158 | function* getUser(id: string) { 159 | const token = yield* Ctx.get(AuthToken) 160 | 161 | if (!token) { 162 | yield* Err.throw(new UserNotFound('No authentication token provided')) 163 | } 164 | 165 | return { id, name: 'John Doe', email: 'john@example.com' } 166 | } 167 | 168 | // Handle effects and run 169 | const program = Koka.try(getUser('123')).handle({ 170 | UserNotFound: (error) => ({ error, status: 'error' }), 171 | AuthToken: 'secret-token-123', 172 | }) 173 | 174 | const result = Koka.run(program) 175 | console.log(result) 176 | ``` 177 | 178 | Congratulations! You've written your first effectful program with Koka. The power of algebraic effects is now at your fingertips! 179 | -------------------------------------------------------------------------------- /packages/koka/docs/README.md: -------------------------------------------------------------------------------- 1 | # Koka Documentation 2 | 3 | Welcome to the Koka documentation! This documentation follows the [Diátaxis](https://diataxis.fr/) framework to provide you with the most effective learning experience. 4 | 5 | ## What is Koka? 6 | 7 | Koka is a lightweight 3kB Effect-TS alternative library based on Algebraic Effects. It provides a powerful and ergonomic way to handle effects in TypeScript applications with minimal bundle size and maximum developer experience. 8 | 9 | ## Documentation Structure 10 | 11 | Our documentation is organized into four distinct sections, each serving a different purpose: 12 | 13 | ### 📚 [Tutorials](./tutorials/) 14 | 15 | **Learning-oriented guides** that help you get started with Koka. 16 | 17 | - [Getting Started](./tutorials/getting-started.md) - Your first steps with Koka 18 | - [Core Concepts](./tutorials/core-concepts.md) - Understanding Algebraic Effects 19 | - [Error Handling](./tutorials/error-handling.md) - Managing errors with effects 20 | - [Context Management](./tutorials/context-management.md) - Working with context effects 21 | - [Async Operations](./tutorials/async-operations.md) - Handling asynchronous code 22 | - [Task Management](./tutorials/task-management.md) - Managing concurrent tasks 23 | 24 | ### 🛠️ [How-to Guides](./how-to/) 25 | 26 | **Task-oriented guides** that show you how to solve specific problems. 27 | 28 | - [Handle Multiple Effects](./how-to/handle-multiple-effects.md) 29 | - [Create Custom Effects](./how-to/create-custom-effects.md) 30 | - [Migrate from Effect-TS](./how-to/migrate-from-effect-ts.md) 31 | - [Debug Effects](./how-to/debug-effects.md) 32 | - [Test Effectful Code](./how-to/test-effectful-code.md) 33 | - [Performance Optimization](./how-to/performance-optimization.md) 34 | 35 | ### 📖 [Reference](./reference/) 36 | 37 | **Information-oriented documentation** for quick lookups. 38 | 39 | - [API Reference](./reference/api.md) - Complete API documentation 40 | - [Effect Types](./reference/effect-types.md) - All available effect types 41 | - [Type Definitions](./reference/types.md) - TypeScript type definitions 42 | - [Configuration](./reference/configuration.md) - Configuration options 43 | 44 | ### 💡 [Explanations](./explanations/) 45 | 46 | **Understanding-oriented content** that explains concepts and decisions. 47 | 48 | - [Algebraic Effects Explained](./explanations/algebraic-effects.md) 49 | - [Comparison with Effect-TS](./explanations/effect-ts-comparison.md) 50 | - [Design Decisions](./explanations/design-decisions.md) 51 | - [Performance Characteristics](./explanations/performance.md) 52 | - [Best Practices](./explanations/best-practices.md) 53 | 54 | ## Quick Start 55 | 56 | ```typescript 57 | import * as Koka from 'koka' 58 | import * as Err from 'koka/err' 59 | import * as Ctx from 'koka/ctx' 60 | 61 | // Define your effects 62 | class UserNotFound extends Err.Err('UserNotFound') {} 63 | class AuthToken extends Ctx.Ctx('AuthToken') {} 64 | 65 | // Write effectful code 66 | function* getUser(id: string) { 67 | const token = yield* Ctx.get(AuthToken) 68 | 69 | if (!token) { 70 | yield* Err.throw(new UserNotFound('No auth token')) 71 | } 72 | 73 | return { id, name: 'John Doe' } 74 | } 75 | 76 | // Handle effects 77 | const program = Koka.try(getUser('123')).handle({ 78 | UserNotFound: (error) => ({ error }), 79 | AuthToken: 'secret-token', 80 | }) 81 | 82 | const result = Koka.run(program) 83 | ``` 84 | 85 | ## Key Features 86 | 87 | - **Lightweight**: Only 3kB minified and gzipped 88 | - **Type Safe**: Full TypeScript support with excellent type inference 89 | - **Algebraic Effects**: Based on proven algebraic effects theory 90 | - **Async Support**: Seamless integration with Promises and async/await 91 | - **Error Handling**: Powerful error handling with type safety 92 | - **Context Management**: Dependency injection made simple 93 | - **Task Management**: Concurrent task execution with control 94 | 95 | ## Installation 96 | 97 | ```bash 98 | npm install koka 99 | # or 100 | yarn add koka 101 | # or 102 | pnpm add koka 103 | ``` 104 | 105 | ## Requirements 106 | 107 | - Node.js >= 22.18 108 | - TypeScript >= 5.0 109 | 110 | ## Documentation Navigation 111 | 112 | ### 🎯 Choose Your Path 113 | 114 | **New to Algebraic Effects?** 115 | Start with [Getting Started](./tutorials/getting-started.md) and [Core Concepts](./tutorials/core-concepts.md) 116 | 117 | **Coming from Effect-TS?** 118 | Check out [Migration Guide](./how-to/migrate-from-effect-ts.md) and [Comparison](./explanations/effect-ts-comparison.md) 119 | 120 | **Need to Solve a Specific Problem?** 121 | Browse the [How-to Guides](./how-to/) section 122 | 123 | **Looking for API Details?** 124 | Go to [API Reference](./reference/api.md) 125 | 126 | **Want to Understand the Design?** 127 | Read [Design Decisions](./explanations/design-decisions.md) and [Best Practices](./explanations/best-practices.md) 128 | 129 | ## Code Examples 130 | 131 | All code examples in this documentation follow the pattern used in the test files: 132 | 133 | ```typescript 134 | // ✅ Correct import style (as used in tests) 135 | import * as Koka from 'koka' 136 | import * as Err from 'koka/err' 137 | import * as Ctx from 'koka/ctx' 138 | import * as Async from 'koka/async' 139 | import * as Task from 'koka/task' 140 | import * as Result from 'koka/result' 141 | import * as Opt from 'koka/opt' 142 | import * as Gen from 'koka/gen' 143 | 144 | // ❌ Not used in this documentation 145 | import { try, run } from 'koka' 146 | import { Err, Ctx } from 'koka' 147 | ``` 148 | 149 | ## Contributing 150 | 151 | We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details. 152 | 153 | ## License 154 | 155 | MIT License - see [LICENSE](../../LICENSE) for details. 156 | 157 | ## External Resources 158 | 159 | - [GitHub Repository](https://github.com/koka-ts/koka) 160 | - [NPM Package](https://www.npmjs.com/package/koka) 161 | - [Effect-TS Documentation](https://effect.website/) (for comparison) 162 | - [Algebraic Effects Research](https://en.wikipedia.org/wiki/Algebraic_effect) 163 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [], 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "ecmaFeatures": { 11 | "jsx": true 12 | } 13 | }, 14 | "plugins": ["jest", "prettier", "@typescript-eslint"], 15 | "rules": { 16 | "react/prop-types": "off", 17 | "react/react-in-jsx-scope": "off", 18 | "jest/no-conditional-expect": "warn", 19 | "no-var": "error", 20 | "for-direction": "error", 21 | "getter-return": "error", 22 | "no-async-promise-executor": "error", 23 | "no-compare-neg-zero": "error", 24 | "no-cond-assign": "error", 25 | "no-constant-condition": "error", 26 | "no-debugger": "error", 27 | "no-dupe-keys": "error", 28 | "no-duplicate-case": "error", 29 | "no-empty": "error", 30 | "no-empty-character-class": "error", 31 | "no-ex-assign": "error", 32 | "no-extra-boolean-cast": "error", 33 | "no-func-assign": "error", 34 | "no-inner-declarations": "error", 35 | "no-invalid-regexp": "error", 36 | "no-misleading-character-class": "error", 37 | "no-obj-calls": "error", 38 | "no-prototype-builtins": "error", 39 | "no-regex-spaces": "error", 40 | "no-sparse-arrays": "error", 41 | "no-template-curly-in-string": "warn", 42 | "no-unexpected-multiline": "error", 43 | "no-unreachable": "error", 44 | "no-unsafe-finally": "error", 45 | "no-unsafe-negation": "error", 46 | "require-atomic-updates": "warn", 47 | "use-isnan": "error", 48 | "valid-typeof": ["error", { "requireStringLiterals": true }], 49 | "accessor-pairs": "error", 50 | "array-callback-return": "error", 51 | "class-methods-use-this": "off", 52 | "curly": ["error", "multi-line"], 53 | "default-case": "error", 54 | "eqeqeq": "error", 55 | "guard-for-in": "off", 56 | "no-caller": "error", 57 | "no-case-declarations": "error", 58 | "no-div-regex": "error", 59 | "no-else-return": "off", 60 | "no-empty-function": "warn", 61 | "no-empty-pattern": "off", 62 | "no-eq-null": "error", 63 | "no-extend-native": "error", 64 | "no-extra-bind": "error", 65 | "no-fallthrough": "error", 66 | "no-global-assign": "error", 67 | "no-new": "error", 68 | "no-new-func": "error", 69 | "no-new-wrappers": "error", 70 | "no-octal": "error", 71 | "no-proto": "error", 72 | "no-return-assign": "error", 73 | "no-return-await": "error", 74 | "no-self-assign": "error", 75 | "no-self-compare": "error", 76 | "no-sequences": "error", 77 | "no-throw-literal": "error", 78 | "no-unmodified-loop-condition": "error", 79 | "no-useless-call": "error", 80 | "no-useless-catch": "error", 81 | "no-useless-concat": "off", 82 | "no-useless-escape": "error", 83 | "no-useless-return": "error", 84 | "no-with": "error", 85 | "prefer-promise-reject-errors": "error", 86 | "radix": "error", 87 | "require-await": "off", 88 | "wrap-iife": "error", 89 | "yoda": "error", 90 | "no-delete-var": "error", 91 | "no-shadow-restricted-names": "error", 92 | "no-undef": "error", 93 | "no-unused-vars": [ 94 | "warn", 95 | { 96 | "argsIgnorePattern": "^_", 97 | "varsIgnorePattern": "^_", 98 | "ignoreRestSiblings": true 99 | } 100 | ], 101 | "global-require": "off", 102 | "handle-callback-err": "error", 103 | "no-buffer-constructor": "error", 104 | "no-path-concat": "error", 105 | "constructor-super": "error", 106 | "no-class-assign": "error", 107 | "no-const-assign": "error", 108 | "no-dupe-class-members": "off", 109 | "no-new-symbol": "error", 110 | "no-this-before-super": "error", 111 | "no-useless-computed-key": "error", 112 | "no-useless-constructor": "error", 113 | "no-useless-rename": "error", 114 | "object-shorthand": "off", 115 | "prefer-arrow-callback": "error", 116 | "prefer-destructuring": "off", 117 | "prefer-rest-params": "error", 118 | "prefer-spread": "error", 119 | "prefer-template": "warn", 120 | "require-yield": "off", 121 | "symbol-description": "error", 122 | "prefer-const": "error", 123 | "prettier/prettier": "error" 124 | }, 125 | "overrides": [ 126 | { 127 | "files": ["*.ts", "*.tsx"], 128 | "parser": "@typescript-eslint/parser", 129 | "parserOptions": { 130 | "project": "./tsconfig.json" 131 | }, 132 | "rules": { 133 | "no-undef": "off", 134 | "no-unused-vars": "off", 135 | "@typescript-eslint/no-unused-vars": [ 136 | "warn", 137 | { 138 | "argsIgnorePattern": "^_", 139 | "varsIgnorePattern": "^_", 140 | "ignoreRestSiblings": true 141 | } 142 | ], 143 | "@typescript-eslint/array-type": "off", 144 | "@typescript-eslint/await-thenable": "error", 145 | "@typescript-eslint/adjacent-overload-signatures": "error", 146 | "@typescript-eslint/ban-ts-comment": [ 147 | "warn", 148 | { 149 | "ts-expect-error": true, 150 | "ts-ignore": "allow-with-description" 151 | } 152 | ], 153 | "@typescript-eslint/ban-types": [ 154 | "error", 155 | { 156 | "extendDefaults": false, 157 | "types": { 158 | "Symbol": { 159 | "message": "Use symbol instead", 160 | "fixWith": "symbol" 161 | }, 162 | "Object": { 163 | "message": "\nThe `Object` type actually means \"any non-nullish value\", so it is marginally better than `unknown`.\n\n- If you want a type meaning \"any object\", you probably want `Record` instead.\n\n- If you want a type meaning \"any value\", you probably want `unknown` instead.\n", 164 | "fixWith": "Record" 165 | } 166 | } 167 | } 168 | ], 169 | "@typescript-eslint/no-floating-promises": "off" 170 | } 171 | }, 172 | { 173 | "files": ["**/__tests__/**/*.js", "**/__tests__/**/*.ts", "**/__tests__/**/*.tsx"], 174 | "env": { 175 | "jest": true 176 | }, 177 | "extends": ["plugin:jest/recommended"], 178 | "rules": { 179 | "jest/expect-expect": "off", 180 | "jest/no-focused-tests": "off", 181 | "@typescript-eslint/ban-ts-comment": [ 182 | "error", 183 | { 184 | "ts-expect-error": "allow-with-description", 185 | "ts-ignore": true 186 | } 187 | ] 188 | } 189 | } 190 | ], 191 | "settings": { 192 | "react": { 193 | "version": "detect" 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/domain.ts: -------------------------------------------------------------------------------- 1 | import * as Domain from 'koka-domain' 2 | import * as Async from 'koka/async' 3 | import * as Err from 'koka/err' 4 | 5 | export class TextDomain extends Domain.Domain { 6 | @Domain.command() 7 | *updateText(text: string) { 8 | yield* Domain.set(this, text) 9 | return 'text updated' 10 | } 11 | @Domain.command() 12 | *clearText() { 13 | yield* Domain.set(this, '') 14 | return 'text cleared' 15 | } 16 | } 17 | 18 | export class BoolDomain extends Domain.Domain { 19 | @Domain.command() 20 | *toggle() { 21 | yield* Domain.set(this, (value) => !value) 22 | return 'bool toggled' 23 | } 24 | } 25 | 26 | // Event definition - only for operations that sub-domain cannot handle itself 27 | export class RemoveTodoEvent extends Domain.Event('RemoveTodo')<{ todoId: number }> {} 28 | 29 | export type Todo = { 30 | id: number 31 | text: string 32 | done: boolean 33 | } 34 | 35 | export class TodoDomain extends Domain.Domain { 36 | text$ = new TextDomain(this.prop('text')) 37 | done$ = new BoolDomain(this.prop('done')); 38 | 39 | @Domain.command() 40 | *updateTodoText(text: string) { 41 | // Can be handled within the domain, no event needed 42 | yield* this.text$.updateText(text) 43 | return 'todo updated' 44 | } 45 | 46 | @Domain.command() 47 | *toggleTodo() { 48 | // Can be handled within the domain, no event needed 49 | yield* this.done$.toggle() 50 | return 'todo toggled' 51 | } 52 | 53 | @Domain.command() 54 | *removeTodo() { 55 | // Cannot remove itself from the list, needs to emit event to parent domain 56 | const todo = yield* Domain.get(this) 57 | yield* Domain.emit(this, new RemoveTodoEvent({ todoId: todo.id })) 58 | return 'todo remove requested' 59 | } 60 | } 61 | 62 | let todoUid = 100 63 | 64 | export class TodoListDomain extends Domain.Domain { 65 | @Domain.command() 66 | *addTodo(text: string) { 67 | const newTodo = { 68 | id: todoUid++, 69 | text, 70 | done: false, 71 | } 72 | yield* Domain.set(this, (todos) => [...todos, newTodo]) 73 | 74 | return 'todo added' 75 | } 76 | 77 | @Domain.command() 78 | *toggleAll() { 79 | const todos = yield* Domain.get(this) 80 | const allDone = todos.every((todo) => todo.done) 81 | 82 | // Can be handled directly within this domain 83 | yield* Domain.set(this, (todos) => { 84 | return todos.map((todo) => ({ ...todo, done: !allDone })) 85 | }) 86 | 87 | return 'all todos toggled' 88 | } 89 | 90 | @Domain.command() 91 | *clearCompleted() { 92 | // Can be handled directly within this domain 93 | yield* Domain.set(this, (todos) => todos.filter((todo) => !todo.done)) 94 | return 'completed todos cleared' 95 | } 96 | 97 | @Domain.command() 98 | *removeTodo(id: number) { 99 | yield* Domain.set(this, (todos) => todos.filter((todo) => todo.id !== id)) 100 | return 'todo removed' 101 | } 102 | 103 | // Event handler - only handle events from sub-domains that cannot be handled locally 104 | @Domain.event(RemoveTodoEvent) 105 | *handleRemoveTodo(event: RemoveTodoEvent) { 106 | yield* this.removeTodo(event.payload.todoId) 107 | } 108 | 109 | todo(id: number) { 110 | const options = this.find((todo) => todo.id === id) 111 | return new TodoDomain(options) as TodoDomain 112 | } 113 | 114 | getKey = (todo: Todo) => { 115 | return todo.id 116 | } 117 | 118 | completedTodoList$ = this.filter((todo) => todo.done) 119 | activeTodoList$ = this.filter((todo) => !todo.done) 120 | activeTodoTextList$ = this.activeTodoList$.map((todo$) => todo$.prop('text')) 121 | completedTodoTextList$ = this.completedTodoList$.map((todo$) => todo$.prop('text')); 122 | 123 | @Domain.query() 124 | *getCompletedTodoList() { 125 | const completedTodoList = yield* Domain.get(this.completedTodoList$) 126 | return completedTodoList 127 | } 128 | 129 | @Domain.query() 130 | *getActiveTodoList() { 131 | const activeTodoList = yield* Domain.get(this.activeTodoList$) 132 | return activeTodoList 133 | } 134 | 135 | @Domain.query() 136 | *getActiveTodoTextList() { 137 | const activeTodoTextList = yield* Domain.get(this.activeTodoTextList$) 138 | return activeTodoTextList 139 | } 140 | 141 | @Domain.query() 142 | *getCompletedTodoTextList() { 143 | const completedTodoTextList = yield* Domain.get(this.completedTodoTextList$) 144 | return completedTodoTextList 145 | } 146 | 147 | @Domain.query() 148 | *getTodoCount() { 149 | const todoList = yield* Domain.get(this) 150 | return todoList.length 151 | } 152 | 153 | @Domain.query() 154 | *getCompletedTodoCount() { 155 | const completedTodoList = yield* Domain.get(this.completedTodoList$) 156 | return completedTodoList.length 157 | } 158 | 159 | @Domain.query() 160 | *getActiveTodoCount() { 161 | const activeTodoList = yield* Domain.get(this.activeTodoList$) 162 | return activeTodoList.length 163 | } 164 | } 165 | 166 | export type TodoFilter = 'all' | 'done' | 'undone' 167 | 168 | export class TodoFilterDomain extends Domain.Domain { 169 | @Domain.command() 170 | *setFilter(filter: TodoFilter) { 171 | yield* Domain.set(this, filter) 172 | return 'filter set' 173 | } 174 | } 175 | 176 | export class TodoInputErr extends Err.Err('TodoInputErr') {} 177 | 178 | export type TodoApp = { 179 | todos: Todo[] 180 | filter: TodoFilter 181 | input: string 182 | } 183 | 184 | export class TodoAppDomain extends Domain.Domain { 185 | todos$ = new TodoListDomain(this.prop('todos')) 186 | filter$ = new TodoFilterDomain(this.prop('filter')) 187 | input$ = new TextDomain(this.prop('input')); 188 | 189 | @Domain.command() 190 | *addTodo() { 191 | const todoApp = yield* Domain.get(this) 192 | 193 | if (todoApp.input === '') { 194 | throw yield* Err.throw(new TodoInputErr('Input is empty')) 195 | } 196 | 197 | yield* this.todos$.addTodo(todoApp.input) 198 | yield* this.input$.clearText() 199 | return 'Todo added' 200 | } 201 | 202 | @Domain.command() 203 | *updateInput(input: string) { 204 | yield* Async.await(Promise.resolve('test async')) 205 | yield* this.input$.updateText(input) 206 | return 'Input updated' 207 | } 208 | 209 | @Domain.query() 210 | *getFilteredTodoList() { 211 | const todoList = yield* Domain.get(this.todos$) 212 | const filter = yield* Domain.get(this.filter$) 213 | 214 | if (filter === 'all') { 215 | return todoList 216 | } 217 | 218 | if (filter === 'done') { 219 | return todoList.filter((todo) => todo.done) 220 | } 221 | 222 | if (filter === 'undone') { 223 | return todoList.filter((todo) => !todo.done) 224 | } 225 | 226 | return todoList 227 | } 228 | 229 | @Domain.query() 230 | *getFilteredTodoIds() { 231 | const todoList = yield* this.getFilteredTodoList() 232 | return todoList.map((todo) => todo.id) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /packages/koka-domain/__tests__/koka-domain-02.test.ts: -------------------------------------------------------------------------------- 1 | import * as Err from 'koka/err' 2 | import * as Result from 'koka/result' 3 | import { Domain, Store, get, set } from '../src/koka-domain.ts' 4 | 5 | type UserEntity = { 6 | id: string 7 | name: string 8 | orderIds: string[] 9 | } 10 | 11 | type OrderEntity = { 12 | id: string 13 | userId: string 14 | productIds: string[] 15 | } 16 | 17 | type ProductEntity = { 18 | id: string 19 | name: string 20 | price: number 21 | collectorIds: string[] 22 | } 23 | 24 | type RootState = { 25 | users: Record 26 | orders: Record 27 | products: Record 28 | } 29 | 30 | class DomainErr extends Err.Err('DomainErr') {} 31 | 32 | class UserStorageDomain extends Domain { 33 | *getUser(id: string) { 34 | const users = yield* get(this) 35 | if (id in users) { 36 | return users[id] 37 | } 38 | 39 | throw yield* Err.throw(new DomainErr(`User ${id} not found`)) 40 | } 41 | *addUser(user: UserEntity) { 42 | const users = yield* get(this) 43 | if (user.id in users) { 44 | throw yield* Err.throw(new DomainErr(`User ${user.id} exists`)) 45 | } 46 | yield* set(this, { ...users, [user.id]: user }) 47 | } 48 | *addOrder(userId: string, orderId: string) { 49 | const user = yield* this.getUser(userId) 50 | yield* set(this, { 51 | ...(yield* get(this)), 52 | [userId]: { 53 | ...user, 54 | orderIds: [...user.orderIds, orderId], 55 | }, 56 | }) 57 | } 58 | } 59 | 60 | class OrderStorageDomain extends Domain { 61 | *getOrder(id: string) { 62 | const orders = yield* get(this) 63 | if (id in orders) { 64 | return orders[id] 65 | } 66 | throw yield* Err.throw(new DomainErr(`Order ${id} not found`)) 67 | } 68 | *addOrder(order: OrderEntity) { 69 | const orders = yield* get(this) 70 | if (order.id in orders) { 71 | throw yield* Err.throw(new DomainErr(`Order ${order.id} exists`)) 72 | } 73 | yield* set(this, { ...orders, [order.id]: order }) 74 | } 75 | *addProduct(orderId: string, productId: string) { 76 | const order = yield* this.getOrder(orderId) 77 | yield* set(this, { 78 | ...(yield* get(this)), 79 | [orderId]: { 80 | ...order, 81 | productIds: [...order.productIds, productId], 82 | }, 83 | }) 84 | } 85 | } 86 | 87 | class ProductStorageDomain extends Domain { 88 | *getProduct(id: string) { 89 | const products = yield* get(this) 90 | if (id in products) { 91 | return products[id] 92 | } 93 | throw yield* Err.throw(new DomainErr(`Product ${id} not found`)) 94 | } 95 | *addProduct(product: ProductEntity) { 96 | const products = yield* get(this) 97 | if (product.id in products) { 98 | throw yield* Err.throw(new DomainErr(`Product ${product.id} exists`)) 99 | } 100 | yield* set(this, { ...products, [product.id]: product }) 101 | } 102 | *getCollectors(productId: string) { 103 | const product = yield* this.getProduct(productId) 104 | const users = [] as UserEntity[] 105 | for (const collectorId of product.collectorIds) { 106 | const user = yield* new UserStorageDomain(this.store.domain.prop('users')).getUser(collectorId) 107 | users.push(user) 108 | } 109 | return users 110 | } 111 | } 112 | 113 | describe('Graph Domain Operations', () => { 114 | let store: Store 115 | 116 | let userStorage: UserStorageDomain 117 | let orderStorage: OrderStorageDomain 118 | let productStorage: ProductStorageDomain 119 | 120 | beforeEach(() => { 121 | const initialState: RootState = { 122 | users: { 123 | user1: { 124 | id: 'user1', 125 | name: 'John Doe', 126 | orderIds: ['order1'], 127 | }, 128 | }, 129 | orders: { 130 | order1: { 131 | id: 'order1', 132 | userId: 'user1', 133 | productIds: ['product1'], 134 | }, 135 | }, 136 | products: { 137 | product1: { 138 | id: 'product1', 139 | name: 'iPhone', 140 | price: 999, 141 | collectorIds: ['user1'], 142 | }, 143 | }, 144 | } 145 | store = new Store({ state: initialState }) 146 | 147 | userStorage = new UserStorageDomain(store.domain.prop('users')) 148 | orderStorage = new OrderStorageDomain(store.domain.prop('orders')) 149 | productStorage = new ProductStorageDomain(store.domain.prop('products')) 150 | }) 151 | 152 | describe('User Operations', () => { 153 | it('should get user', () => { 154 | const result = Result.runSync(userStorage.getUser('user1')) 155 | if (result.type === 'err') throw new Error('Expected user but got error') 156 | expect(result.value.name).toBe('John Doe') 157 | }) 158 | 159 | it('should add user', () => { 160 | const newUser = { 161 | id: 'user2', 162 | name: 'Jane Doe', 163 | orderIds: [], 164 | } 165 | Result.runSync(userStorage.addUser(newUser)) 166 | 167 | const result = Result.runSync(userStorage.getUser('user2')) 168 | if (result.type === 'err') throw new Error('Expected user but got error') 169 | expect(result.value.name).toBe('Jane Doe') 170 | }) 171 | }) 172 | 173 | describe('Order Operations', () => { 174 | it('should get order', () => { 175 | const result = Result.runSync(orderStorage.getOrder('order1')) 176 | if (result.type === 'err') throw new Error('Expected order but got error') 177 | expect(result.value.userId).toBe('user1') 178 | }) 179 | 180 | it('should add product to order', () => { 181 | Result.runSync( 182 | productStorage.addProduct({ 183 | id: 'product2', 184 | name: 'MacBook', 185 | price: 1999, 186 | collectorIds: [], 187 | }), 188 | ) 189 | 190 | Result.runSync(orderStorage.addProduct('order1', 'product2')) 191 | 192 | const result = Result.runSync(orderStorage.getOrder('order1')) 193 | if (result.type === 'err') throw new Error('Expected order but got error') 194 | expect(result.value.productIds).toEqual(['product1', 'product2']) 195 | }) 196 | }) 197 | 198 | describe('Product Operations', () => { 199 | it('should get product', () => { 200 | const result = Result.runSync(productStorage.getProduct('product1')) 201 | if (result.type === 'err') throw new Error('Expected product but got error') 202 | expect(result.value.name).toBe('iPhone') 203 | }) 204 | 205 | it('should get collectors', () => { 206 | const result = Result.runSync(productStorage.getCollectors('product1')) 207 | if (result.type === 'err') throw new Error('Expected collectors but got error') 208 | expect(result.value.length).toBe(1) 209 | expect(result.value[0].name).toBe('John Doe') 210 | }) 211 | }) 212 | 213 | describe('Graph Relationships', () => { 214 | it('should maintain user-order relationship', () => { 215 | Result.runSync( 216 | orderStorage.addOrder({ 217 | id: 'order2', 218 | userId: 'user1', 219 | productIds: [], 220 | }), 221 | ) 222 | 223 | Result.runSync(userStorage.addOrder('user1', 'order2')) 224 | 225 | const userResult = Result.runSync(userStorage.getUser('user1')) 226 | if (userResult.type === 'err') throw new Error('Expected user but got error') 227 | expect(userResult.value.orderIds).toEqual(['order1', 'order2']) 228 | }) 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koka Stack 2 | 3 | A monorepo containing lightweight TypeScript libraries for algebraic effects and related utilities. 4 | 5 | ## Packages 6 | 7 | ### [@koka/core](./packages/koka/) 8 | 9 | A lightweight 3kB Effect-TS alternative library based on Algebraic Effects. 10 | 11 | **Features:** 12 | 13 | - **Lightweight**: Only 3kB minified and gzipped 14 | - **Type Safe**: Full TypeScript support with excellent type inference 15 | - **Algebraic Effects**: Based on proven algebraic effects theory 16 | - **Async Support**: Seamless integration with Promises and async/await 17 | - **Error Handling**: Powerful error handling with type safety 18 | - **Context Management**: Dependency injection made simple 19 | - **Task Management**: Concurrent task execution with control 20 | 21 | **Quick Start:** 22 | 23 | ```typescript 24 | import * as Koka from 'koka' 25 | import * as Err from 'koka/err' 26 | import * as Ctx from 'koka/ctx' 27 | 28 | // Define your effects 29 | class UserNotFound extends Err.Err('UserNotFound') {} 30 | class AuthToken extends Ctx.Ctx('AuthToken') {} 31 | 32 | // Write effectful code 33 | function* getUser(id: string) { 34 | const token = yield* Ctx.get(AuthToken) 35 | 36 | if (!token) { 37 | yield* Err.throw(new UserNotFound('No auth token')) 38 | } 39 | 40 | return { id, name: 'John Doe' } 41 | } 42 | 43 | // Handle effects 44 | const program = Koka.try(getUser('123')).handle({ 45 | UserNotFound: (error) => ({ error }), 46 | AuthToken: 'secret-token', 47 | }) 48 | 49 | const result = Koka.run(program) 50 | ``` 51 | 52 | **Documentation:** [📖 Full Documentation](./packages/koka/docs/) 53 | 54 | ### [koka-accessor](./packages/koka-accessor/) 55 | 56 | Accessors library for immutable data manipulation. 57 | 58 | ### [koka-domain](./packages/koka-domain/) 59 | 60 | State management library with algebraic effects. 61 | 62 | ## Documentation Navigation 63 | 64 | Our documentation follows the [Diátaxis](https://diataxis.fr/) framework for comprehensive, user-friendly guides: 65 | 66 | ### 🎓 **New to Koka? Start Here** 67 | 68 | - [Getting Started](./packages/koka/docs/tutorials/getting-started.md) - Your first steps with Koka 69 | - [Core Concepts](./packages/koka/docs/tutorials/core-concepts.md) - Understanding algebraic effects 70 | - [Error Handling](./packages/koka/docs/tutorials/error-handling.md) - Managing errors with type safety 71 | 72 | ### 🔧 **Coming from Effect-TS?** 73 | 74 | - [Migration Guide](./packages/koka/docs/how-to/migrate-from-effect-ts.md) - Step-by-step migration 75 | - [Effect-TS Comparison](./packages/koka/docs/explanations/effect-ts-comparison.md) - Detailed comparison 76 | 77 | ### 📚 **Advanced Topics** 78 | 79 | - [Context Management](./packages/koka/docs/tutorials/context-management.md) - Dependency injection 80 | - [Async Operations](./packages/koka/docs/tutorials/async-operations.md) - Working with Promises 81 | - [Task Management](./packages/koka/docs/tutorials/task-management.md) - Concurrent operations 82 | 83 | ### 🔍 **Reference** 84 | 85 | - [API Reference](./packages/koka/docs/reference/api.md) - Complete API documentation 86 | 87 | ## Key Features 88 | 89 | ### Lightweight & Fast 90 | 91 | - **3kB** minified and gzipped (vs ~50kB for Effect-TS) 92 | - Minimal runtime overhead 93 | - Tree-shakeable for optimal bundle size 94 | 95 | ### Type Safe 96 | 97 | - Full TypeScript support 98 | - Excellent type inference 99 | - Compile-time effect checking 100 | 101 | ### Developer Friendly 102 | 103 | - Familiar generator syntax (`function*`, `yield*`) 104 | - Simple API design 105 | - Gentle learning curve 106 | 107 | ### Production Ready 108 | 109 | - Comprehensive error handling 110 | - Async/await integration 111 | - Concurrent task management 112 | - Dependency injection 113 | 114 | ## Comparison with Effect-TS 115 | 116 | | Aspect | Koka | Effect-TS | 117 | | ------------------ | ---------------- | --------------- | 118 | | **Bundle Size** | ~3kB | ~50kB | 119 | | **API Style** | Object-oriented | Functional | 120 | | **Learning Curve** | Gentle | Steep | 121 | | **Type Safety** | Excellent | Excellent | 122 | | **Performance** | Minimal overhead | Higher overhead | 123 | | **Ecosystem** | Lightweight | Rich ecosystem | 124 | 125 | ## Installation 126 | 127 | ```bash 128 | # Install core package 129 | npm install koka 130 | 131 | # Install additional packages 132 | npm install @koka/accessor @koka/store 133 | ``` 134 | 135 | ## Code Style 136 | 137 | All code examples in our documentation use the `import * as XXX from 'xxx'` style for consistency: 138 | 139 | ```typescript 140 | // ✅ Correct import style (as used in tests) 141 | import * as Koka from 'koka' 142 | import * as Err from 'koka/err' 143 | import * as Ctx from 'koka/ctx' 144 | import * as Async from 'koka/async' 145 | import * as Task from 'koka/task' 146 | import * as Result from 'koka/result' 147 | import * as Opt from 'koka/opt' 148 | import * as Gen from 'koka/gen' 149 | 150 | // ❌ Not used in this documentation 151 | import { try, run } from 'koka' 152 | import { Err, Ctx } from 'koka' 153 | ``` 154 | 155 | ## Quick Examples 156 | 157 | ### Error Handling 158 | 159 | ```typescript 160 | import * as Koka from 'koka' 161 | import * as Err from 'koka/err' 162 | 163 | class ValidationError extends Err.Err('ValidationError')<{ field: string; message: string }> {} 164 | 165 | function* validateUser(user: any) { 166 | if (!user.name) { 167 | yield* Err.throw( 168 | new ValidationError({ 169 | field: 'name', 170 | message: 'Name is required', 171 | }), 172 | ) 173 | } 174 | return user 175 | } 176 | 177 | const program = Koka.try(validateUser({})).handle({ 178 | ValidationError: (error) => ({ error, status: 'error' }), 179 | }) 180 | 181 | const result = Koka.run(program) 182 | ``` 183 | 184 | ### Context Management 185 | 186 | ```typescript 187 | import * as Koka from 'koka' 188 | import * as Ctx from 'koka/ctx' 189 | import * as Async from 'koka/async' 190 | 191 | class Database extends Ctx.Ctx('Database')<{ 192 | query: (sql: string) => Promise 193 | }> {} 194 | 195 | function* getUser(id: string) { 196 | const db = yield* Ctx.get(Database) 197 | const user = yield* Async.await(db.query(`SELECT * FROM users WHERE id = '${id}'`)) 198 | return user 199 | } 200 | 201 | const program = Koka.try(getUser('123')).handle({ 202 | Database: { 203 | query: async (sql) => ({ id: '123', name: 'John Doe' }), 204 | }, 205 | }) 206 | 207 | const result = await Koka.run(program) 208 | ``` 209 | 210 | ### Task Management 211 | 212 | ```typescript 213 | import * as Koka from 'koka' 214 | import * as Task from 'koka/task' 215 | 216 | function* getUserProfile(userId: string) { 217 | const result = yield* Task.object({ 218 | user: () => fetchUser(userId), 219 | posts: () => fetchPosts(userId), 220 | comments: () => fetchComments(userId), 221 | }) 222 | 223 | return result 224 | } 225 | 226 | const profile = await Koka.run(getUserProfile('123')) 227 | ``` 228 | 229 | ## Requirements 230 | 231 | - Node.js >= 22.18 232 | - TypeScript >= 5.0 233 | 234 | ## Browser Support 235 | 236 | Koka requires: 237 | 238 | - ES2015+ (for generators) 239 | - Promise support 240 | - Symbol support 241 | 242 | For older browsers, consider using a polyfill or transpiler. 243 | 244 | ## Development 245 | 246 | ```bash 247 | # Install dependencies 248 | pnpm install 249 | 250 | # Run tests 251 | pnpm test 252 | 253 | # Run tests with coverage 254 | pnpm test:coverage 255 | 256 | # Build all packages 257 | pnpm build 258 | 259 | # Sync repository structure 260 | pnpm sync-repo 261 | ``` 262 | 263 | ## Contributing 264 | 265 | We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details. 266 | 267 | ### Development Workflow 268 | 269 | 1. Fork the repository 270 | 2. Create a feature branch 271 | 3. Make your changes 272 | 4. Add tests for new functionality 273 | 5. Ensure all tests pass 274 | 6. Submit a pull request 275 | 276 | ## License 277 | 278 | MIT License - see [LICENSE](./LICENSE) for details. 279 | 280 | ## Related Projects 281 | 282 | - [Effect-TS](https://effect.website/) - Comprehensive algebraic effects library 283 | - [Algebraic Effects Research](https://en.wikipedia.org/wiki/Algebraic_effect) - Theory behind algebraic effects 284 | 285 | ## Support 286 | 287 | - [GitHub Issues](https://github.com/koka-ts/koka/issues) 288 | - [GitHub Discussions](https://github.com/koka-ts/koka/discussions) 289 | - [Documentation](./packages/koka/docs/) 290 | 291 | --- 292 | 293 | Made with ❤️ by the Koka team 294 | -------------------------------------------------------------------------------- /packages/koka/docs/tutorials/core-concepts.md: -------------------------------------------------------------------------------- 1 | # Core Concepts: Algebraic Effects 2 | 3 | This tutorial dives deep into the fundamental concepts that make Koka powerful and unique. 4 | 5 | ## What Are Algebraic Effects? 6 | 7 | Algebraic effects are a programming paradigm that separates the **what** from the **how**. They allow you to write code that describes what operations you want to perform, without specifying how those operations should be implemented. 8 | 9 | Think of it like this: you write a recipe (the effects), and someone else provides the kitchen equipment (the handlers). 10 | 11 | ## The Three Pillars of Koka 12 | 13 | Koka is built around three core effect types, each serving a specific purpose: 14 | 15 | ### 1. Error Effects (`Err`) 16 | 17 | Error effects represent operations that can fail. They're like throwing exceptions, but with type safety and explicit handling. 18 | 19 | ```typescript 20 | import * as Err from 'koka/err' 21 | 22 | // Define an error effect 23 | class ValidationError extends Err.Err('ValidationError')<{ field: string; message: string }> {} 24 | 25 | // Use the error effect 26 | function* validateUser(user: any) { 27 | if (!user.name) { 28 | yield* Err.throw( 29 | new ValidationError({ 30 | field: 'name', 31 | message: 'Name is required', 32 | }), 33 | ) 34 | } 35 | 36 | if (!user.email) { 37 | yield* Err.throw( 38 | new ValidationError({ 39 | field: 'email', 40 | message: 'Email is required', 41 | }), 42 | ) 43 | } 44 | 45 | return user 46 | } 47 | ``` 48 | 49 | ### 2. Context Effects (`Ctx`) 50 | 51 | Context effects provide values that your code needs to function. They're perfect for dependency injection and configuration. 52 | 53 | ```typescript 54 | import * as Ctx from 'koka/ctx' 55 | 56 | // Define context effects 57 | class DatabaseConnection extends Ctx.Ctx('DatabaseConnection')<{ query: (sql: string) => Promise }> {} 58 | class Config extends Ctx.Ctx('Config')<{ apiUrl: string; timeout: number }> {} 59 | 60 | // Use context effects 61 | function* getUserById(id: string) { 62 | const db = yield* Ctx.get(DatabaseConnection) 63 | const config = yield* Ctx.get(Config) 64 | 65 | const user = await db.query(`SELECT * FROM users WHERE id = '${id}'`) 66 | return user 67 | } 68 | ``` 69 | 70 | ### 3. Optional Effects (`Opt`) 71 | 72 | Optional effects provide values that might not be available. They're great for optional features or debugging. 73 | 74 | ```typescript 75 | import * as Opt from 'koka/opt' 76 | 77 | // Define optional effects 78 | class Logger extends Opt.Opt('Logger')<(message: string) => void> {} 79 | class DebugMode extends Opt.Opt('DebugMode') {} 80 | 81 | // Use optional effects 82 | function* processData(data: any) { 83 | const logger = yield* Opt.get(Logger) 84 | const debugMode = yield* Opt.get(DebugMode) 85 | 86 | logger?.('Processing data...') 87 | 88 | if (debugMode) { 89 | logger?.('Debug mode enabled') 90 | console.log('Data:', data) 91 | } 92 | 93 | return process(data) 94 | } 95 | ``` 96 | 97 | ## The Generator Pattern 98 | 99 | Koka uses TypeScript generators to represent effectful computations. Here's why: 100 | 101 | ```typescript 102 | function* effectfulFunction() { 103 | // This function can perform effects 104 | const value = yield* someEffect() 105 | return value 106 | } 107 | ``` 108 | 109 | The `function*` syntax creates a generator, and `yield*` is used to perform effects. This pattern allows Koka to: 110 | 111 | - **Pause execution** when an effect is performed 112 | - **Resume execution** with the result from the effect handler 113 | - **Maintain type safety** throughout the process 114 | 115 | ## Effect Handling 116 | 117 | The real power comes from how effects are handled. Let's look at a comprehensive example: 118 | 119 | ```typescript 120 | import * as Koka from 'koka' 121 | import * as Err from 'koka/err' 122 | import * as Ctx from 'koka/ctx' 123 | import * as Opt from 'koka/opt' 124 | 125 | // Define our effects 126 | class UserNotFound extends Err.Err('UserNotFound') {} 127 | class DatabaseError extends Err.Err('DatabaseError') {} 128 | class DatabaseConnection extends Ctx.Ctx('DatabaseConnection')<{ query: (sql: string) => Promise }> {} 129 | class Logger extends Opt.Opt('Logger')<(message: string) => void> {} 130 | 131 | // Write effectful code 132 | function* getUserById(id: string) { 133 | const logger = yield* Opt.get(Logger) 134 | const db = yield* Ctx.get(DatabaseConnection) 135 | 136 | logger?.(`Fetching user with ID: ${id}`) 137 | 138 | try { 139 | const user = await db.query(`SELECT * FROM users WHERE id = '${id}'`) 140 | 141 | if (!user) { 142 | yield* Err.throw(new UserNotFound(`User with ID ${id} not found`)) 143 | } 144 | 145 | logger?.(`Successfully fetched user: ${user.name}`) 146 | return user 147 | } catch (error) { 148 | yield* Err.throw(new DatabaseError(`Database error: ${error.message}`)) 149 | } 150 | } 151 | 152 | // Handle effects with different implementations 153 | const program = Koka.try(getUserById('123')).handle({ 154 | // Error handlers 155 | UserNotFound: (error) => ({ error, status: 404 }), 156 | DatabaseError: (error) => ({ error, status: 500 }), 157 | 158 | // Context providers 159 | DatabaseConnection: { 160 | query: async (sql) => { 161 | // Mock database implementation 162 | if (sql.includes('123')) { 163 | return { id: '123', name: 'John Doe', email: 'john@example.com' } 164 | } 165 | return null 166 | }, 167 | }, 168 | 169 | // Optional providers 170 | Logger: (message) => console.log(`[INFO] ${message}`), 171 | }) 172 | 173 | // Run the program 174 | const result = Koka.run(program) 175 | console.log(result) 176 | ``` 177 | 178 | ## Type Safety 179 | 180 | One of Koka's greatest strengths is its type safety. The TypeScript compiler ensures that: 181 | 182 | 1. **All effects are handled**: You can't run a program without handling all its effects 183 | 2. **Effect types match**: The types of your effect handlers must match the effect definitions 184 | 3. **Return types are correct**: The return type of your program is inferred correctly 185 | 186 | ```typescript 187 | // TypeScript will catch these errors at compile time: 188 | 189 | // ❌ Missing effect handler 190 | const program1 = Koka.try(getUserById('123')).handle({ 191 | // Missing UserNotFound handler - TypeScript error! 192 | DatabaseConnection: { query: async () => null }, 193 | }) 194 | 195 | // ❌ Wrong handler type 196 | const program2 = Koka.try(getUserById('123')).handle({ 197 | UserNotFound: (error: number) => {}, // TypeScript error: should be string 198 | DatabaseConnection: { query: async () => null }, 199 | }) 200 | 201 | // ✅ Correct handling 202 | const program3 = Koka.try(getUserById('123')).handle({ 203 | UserNotFound: (error: string) => ({ error }), 204 | DatabaseError: (error: string) => ({ error }), 205 | DatabaseConnection: { query: async () => null }, 206 | Logger: (message: string) => console.log(message), 207 | }) 208 | ``` 209 | 210 | ## Effect Composition 211 | 212 | Effects compose naturally. You can combine multiple effects in a single function: 213 | 214 | ```typescript 215 | function* complexOperation() { 216 | // Use multiple effect types 217 | const config = yield* Ctx.get(Config) 218 | const logger = yield* Opt.get(Logger) 219 | 220 | logger?.('Starting complex operation') 221 | 222 | try { 223 | const result = yield* someRiskyOperation() 224 | logger?.('Operation completed successfully') 225 | return result 226 | } catch (error) { 227 | yield* Err.throw(new OperationError(error.message)) 228 | } 229 | } 230 | ``` 231 | 232 | ## The Power of Separation 233 | 234 | The key insight of algebraic effects is **separation of concerns**: 235 | 236 | - **Business logic** focuses on what needs to be done 237 | - **Effect handlers** focus on how it should be done 238 | - **Testing** becomes easier because you can swap implementations 239 | - **Reusability** increases because the same logic can work with different handlers 240 | 241 | ## Next Steps 242 | 243 | Now that you understand the core concepts, explore: 244 | 245 | - [Error Handling](./error-handling.md) - Advanced error management patterns 246 | - [Context Management](./context-management.md) - Dependency injection strategies 247 | - [Async Operations](./async-operations.md) - Working with asynchronous code 248 | - [Task Management](./task-management.md) - Concurrent operations 249 | 250 | The power of algebraic effects lies in their simplicity and expressiveness. Once you grasp these concepts, you'll see how they can transform your code architecture! 251 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode, useState } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import * as Domain from 'koka-domain' 4 | import { PrettyLogger } from 'koka-domain/pretty-browser-logger' 5 | import { useDomainState } from 'koka-react' 6 | import { type TodoApp, TodoAppDomain } from './domain' 7 | import './index.css' 8 | import App from './App.tsx' 9 | 10 | type AppState = { 11 | todoApp: TodoApp 12 | todoAppList: TodoApp[] 13 | } 14 | 15 | type MainProps = { 16 | domain: Domain.Domain 17 | } 18 | 19 | function Main(props: MainProps) { 20 | const count = useDomainState(props.domain.select((app) => app.todoAppList.length)) 21 | const [mode, setMode] = useState<'todo-app' | 'todo-app-list'>('todo-app') 22 | 23 | return ( 24 |
25 | {/* Header Section */} 26 |
27 |
28 |

29 | Koka Todo Demo 30 |

31 |

32 | Experience a modern Todo application built with Koka framework, supporting both single and 33 | multiple Todo list management 34 |

35 |
36 | 37 | {/* Mode Toggle Buttons */} 38 |
39 |
40 | 53 | 66 |
67 |
68 |
69 | 70 | {/* Content Section */} 71 |
72 | {mode === 'todo-app-list' ? ( 73 |
74 | {Array.from({ length: count }).map((_, index) => { 75 | const todoApp$ = new TodoAppDomain(props.domain.select((app) => app.todoAppList[index])) 76 | 77 | return ( 78 |
82 |
83 |
84 | #{index + 1} 85 |
86 | 87 |
88 |
89 | ) 90 | })} 91 |
92 | ) : ( 93 |
94 |
95 | 96 |
97 |
98 | )} 99 |
100 | 101 | {/* Footer */} 102 |
103 |
104 |

105 | 🚀 Built with Koka framework | 💡 Powered 106 | by React +{' '} 107 | Tailwind CSS 108 |

109 |
110 |
111 |
112 | ) 113 | } 114 | 115 | const initialState: AppState = { 116 | todoApp: { 117 | todos: [ 118 | { id: 101, text: 'Learn koka-domain framework', done: true }, 119 | { id: 102, text: 'Build React todo app', done: true }, 120 | { id: 103, text: 'Write comprehensive documentation', done: false }, 121 | { id: 104, text: 'Add unit tests', done: false }, 122 | { id: 105, text: 'Optimize performance', done: false }, 123 | { id: 106, text: 'Deploy to production', done: false }, 124 | ], 125 | input: '', 126 | filter: 'all', 127 | }, 128 | todoAppList: [ 129 | { 130 | todos: [ 131 | { id: 101, text: 'Learn koka-domain framework', done: true }, 132 | { id: 102, text: 'Build React todo app', done: true }, 133 | { id: 103, text: 'Write comprehensive documentation', done: false }, 134 | { id: 104, text: 'Add unit tests', done: false }, 135 | { id: 105, text: 'Optimize performance', done: false }, 136 | { id: 106, text: 'Deploy to production', done: false }, 137 | ], 138 | input: '', 139 | filter: 'all', 140 | }, 141 | { 142 | todos: [ 143 | { id: 201, text: 'Buy groceries', done: false }, 144 | { id: 202, text: 'Cook dinner', done: false }, 145 | { id: 203, text: 'Clean the house', done: true }, 146 | { id: 204, text: 'Do laundry', done: false }, 147 | { id: 205, text: 'Take out trash', done: true }, 148 | ], 149 | input: '', 150 | filter: 'undone', 151 | }, 152 | { 153 | todos: [ 154 | { id: 301, text: 'Read "Clean Code" book', done: true }, 155 | { id: 302, text: 'Practice coding challenges', done: false }, 156 | { id: 303, text: 'Learn TypeScript advanced features', done: false }, 157 | { id: 304, text: 'Study design patterns', done: true }, 158 | { id: 305, text: 'Contribute to open source', done: false }, 159 | { id: 306, text: 'Attend tech meetup', done: false }, 160 | { id: 307, text: 'Update portfolio', done: true }, 161 | ], 162 | input: '', 163 | filter: 'done', 164 | }, 165 | { 166 | todos: [ 167 | { id: 401, text: 'Morning workout', done: true }, 168 | { id: 402, text: 'Meditation session', done: false }, 169 | { id: 403, text: 'Drink 8 glasses of water', done: false }, 170 | { id: 404, text: 'Take vitamins', done: true }, 171 | { id: 405, text: 'Go for a walk', done: false }, 172 | { id: 406, text: 'Get 8 hours of sleep', done: false }, 173 | ], 174 | input: '', 175 | filter: 'all', 176 | }, 177 | { 178 | todos: [ 179 | { id: 501, text: 'Plan weekend trip', done: false }, 180 | { id: 502, text: 'Book flight tickets', done: false }, 181 | { id: 503, text: 'Reserve hotel room', done: false }, 182 | { id: 504, text: 'Create travel itinerary', done: false }, 183 | { id: 505, text: 'Pack luggage', done: false }, 184 | ], 185 | input: '', 186 | filter: 'all', 187 | }, 188 | ], 189 | } 190 | 191 | const store = new Domain.Store({ 192 | state: initialState, 193 | plugins: [PrettyLogger()], 194 | }) 195 | 196 | createRoot(document.getElementById('root')!).render( 197 | 198 |
199 | , 200 | ) 201 | -------------------------------------------------------------------------------- /packages/koka/README.md: -------------------------------------------------------------------------------- 1 | # Koka 2 | 3 | A lightweight 3kB Effect-TS alternative library based on Algebraic Effects. 4 | 5 | [![npm version](https://badge.fury.io/js/koka.svg)](https://badge.fury.io/js/koka) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![Bundle Size](https://img.shields.io/bundlephobia/minzip/koka)](https://bundlephobia.com/package/koka) 8 | 9 | ## Features 10 | 11 | - **Lightweight**: Only 3kB minified and gzipped 12 | - **Type Safe**: Full TypeScript support with excellent type inference 13 | - **Algebraic Effects**: Based on proven algebraic effects theory 14 | - **Async Support**: Seamless integration with Promises and async/await 15 | - **Error Handling**: Powerful error handling with type safety 16 | - **Context Management**: Dependency injection made simple 17 | - **Task Management**: Concurrent task execution with control 18 | 19 | ## Quick Start 20 | 21 | ### Installation 22 | 23 | ```bash 24 | npm install koka 25 | # or 26 | yarn add koka 27 | # or 28 | pnpm add koka 29 | ``` 30 | 31 | ### Basic Usage 32 | 33 | ```typescript 34 | import * as Koka from 'koka' 35 | import * as Err from 'koka/err' 36 | import * as Ctx from 'koka/ctx' 37 | 38 | // Define your effects 39 | class UserNotFound extends Err.Err('UserNotFound') {} 40 | class AuthToken extends Ctx.Ctx('AuthToken') {} 41 | 42 | // Write effectful code 43 | function* getUser(id: string) { 44 | const token = yield* Ctx.get(AuthToken) 45 | 46 | if (!token) { 47 | yield* Err.throw(new UserNotFound('No auth token')) 48 | } 49 | 50 | return { id, name: 'John Doe' } 51 | } 52 | 53 | // Handle effects 54 | const program = Koka.try(getUser('123')).handle({ 55 | UserNotFound: (error) => ({ error }), 56 | AuthToken: 'secret-token', 57 | }) 58 | 59 | const result = Koka.run(program) 60 | console.log(result) // { id: '123', name: 'John Doe' } 61 | ``` 62 | 63 | ## Core Concepts 64 | 65 | ### Error Effects 66 | 67 | Handle errors with type safety: 68 | 69 | ```typescript 70 | import * as Err from 'koka/err' 71 | 72 | class ValidationError extends Err.Err('ValidationError')<{ field: string; message: string }> {} 73 | 74 | function* validateUser(user: any) { 75 | if (!user.name) { 76 | yield* Err.throw( 77 | new ValidationError({ 78 | field: 'name', 79 | message: 'Name is required', 80 | }), 81 | ) 82 | } 83 | return user 84 | } 85 | 86 | const program = Koka.try(validateUser({})).handle({ 87 | ValidationError: (error) => ({ error, status: 'error' }), 88 | }) 89 | 90 | const result = Koka.run(program) 91 | ``` 92 | 93 | ### Context Effects 94 | 95 | Dependency injection made simple: 96 | 97 | ```typescript 98 | import * as Ctx from 'koka/ctx' 99 | 100 | class Database extends Ctx.Ctx('Database')<{ 101 | query: (sql: string) => Promise 102 | }> {} 103 | 104 | function* getUser(id: string) { 105 | const db = yield* Ctx.get(Database) 106 | const user = yield* Async.await(db.query(`SELECT * FROM users WHERE id = '${id}'`)) 107 | return user 108 | } 109 | 110 | const program = Koka.try(getUser('123')).handle({ 111 | Database: { 112 | query: async (sql) => ({ id: '123', name: 'John Doe' }), 113 | }, 114 | }) 115 | 116 | const result = await Koka.run(program) 117 | ``` 118 | 119 | ### Async Operations 120 | 121 | Seamless async/await integration: 122 | 123 | ```typescript 124 | import * as Async from 'koka/async' 125 | 126 | function* fetchUser(id: string) { 127 | const user = yield* Async.await(fetch(`/api/users/${id}`).then((res) => res.json())) 128 | return user 129 | } 130 | 131 | const result = await Koka.run(fetchUser('123')) 132 | ``` 133 | 134 | ### Task Management 135 | 136 | Concurrent operations with control: 137 | 138 | ```typescript 139 | import * as Task from 'koka/task' 140 | 141 | function* getUserProfile(userId: string) { 142 | const result = yield* Task.object({ 143 | user: () => fetchUser(userId), 144 | posts: () => fetchPosts(userId), 145 | comments: () => fetchComments(userId), 146 | }) 147 | 148 | return result 149 | } 150 | 151 | const profile = await Koka.run(getUserProfile('123')) 152 | ``` 153 | 154 | ## Comparison with Effect-TS 155 | 156 | | Aspect | Koka | Effect-TS | 157 | | ------------------ | ---------------- | --------------- | 158 | | **Bundle Size** | ~3kB | ~50kB | 159 | | **API Style** | Object-oriented | Functional | 160 | | **Learning Curve** | Gentle | Steep | 161 | | **Type Safety** | Excellent | Excellent | 162 | | **Performance** | Minimal overhead | Higher overhead | 163 | 164 | ### Migration from Effect-TS 165 | 166 | ```typescript 167 | // Effect-TS 168 | const getUser = (id: string) => 169 | Effect.gen(function* (_) { 170 | const userService = yield* _(UserService) 171 | const user = yield* _(userService.getUser(id)) 172 | return user 173 | }) 174 | 175 | // Koka 176 | function* getUser(id: string) { 177 | const userService = yield* Ctx.get(UserService) 178 | const user = yield* Async.await(userService.getUser(id)) 179 | return user 180 | } 181 | ``` 182 | 183 | See our [migration guide](./docs/how-to/migrate-from-effect-ts.md) for detailed instructions. 184 | 185 | ## API Reference 186 | 187 | ### Core Functions 188 | 189 | - `Koka.try()` - Create a program that can handle effects 190 | - `Koka.run()` - Run a program 191 | - `Koka.runSync()` - Run a program synchronously 192 | - `Koka.runAsync()` - Run a program asynchronously 193 | 194 | ### Effect Types 195 | 196 | - `Err.Err()` - Error effects 197 | - `Ctx.Ctx()` - Context effects 198 | - `Opt.Opt()` - Optional effects 199 | 200 | ### Task Functions 201 | 202 | - `Task.all()` - Array-based parallel execution 203 | - `Task.tuple()` - Tuple-based parallel execution 204 | - `Task.object()` - Object-based parallel execution (recommended) 205 | - `Task.race()` - Get first result 206 | - `Task.concurrent()` - Controlled concurrency 207 | 208 | ### Utility Functions 209 | 210 | - `Async.await()` - Await values or promises 211 | - `Result.run()` - Run with result handling 212 | - `Result.wrap()` - Wrap generators in results 213 | - `Result.unwrap()` - Unwrap result generators 214 | 215 | ## Documentation 216 | 217 | Our documentation follows the [Diátaxis](https://diataxis.fr/) framework: 218 | 219 | - **[Tutorials](./docs/tutorials/)** - Learning-oriented guides 220 | - **[How-to Guides](./docs/how-to/)** - Task-oriented guides 221 | - **[Reference](./docs/reference/)** - API documentation 222 | - **[Explanations](./docs/explanations/)** - Concept explanations 223 | 224 | ### Quick Links 225 | 226 | - [Getting Started](./docs/tutorials/getting-started.md) 227 | - [Core Concepts](./docs/tutorials/core-concepts.md) 228 | - [API Reference](./docs/reference/api.md) 229 | - [Effect-TS Comparison](./docs/explanations/effect-ts-comparison.md) 230 | - [Migration Guide](./docs/how-to/migrate-from-effect-ts.md) 231 | 232 | ## Examples 233 | 234 | ### Error Handling 235 | 236 | ```typescript 237 | import * as Koka from 'koka' 238 | import * as Err from 'koka/err' 239 | import * as Result from 'koka/result' 240 | 241 | class NetworkError extends Err.Err('NetworkError') {} 242 | 243 | function* fetchData(url: string) { 244 | try { 245 | const response = yield* Async.await(fetch(url)) 246 | const data = yield* Async.await(response.json()) 247 | return data 248 | } catch (error) { 249 | yield* Err.throw(new NetworkError(error.message)) 250 | } 251 | } 252 | 253 | // Handle errors explicitly 254 | const result = Result.run(fetchData('https://api.example.com/data')) 255 | if (result.type === 'ok') { 256 | console.log('Success:', result.value) 257 | } else { 258 | console.log('Error:', result.error) 259 | } 260 | ``` 261 | 262 | ### Context Management 263 | 264 | ```typescript 265 | import * as Koka from 'koka' 266 | import * as Ctx from 'koka/ctx' 267 | import * as Opt from 'koka/opt' 268 | 269 | class Database extends Ctx.Ctx('Database')<{ 270 | query: (sql: string) => Promise 271 | }> {} 272 | 273 | class Logger extends Opt.Opt('Logger')<(message: string) => void> {} 274 | 275 | function* getUser(id: string) { 276 | const db = yield* Ctx.get(Database) 277 | const logger = yield* Opt.get(Logger) 278 | 279 | logger?.('Fetching user...') 280 | const user = yield* Async.await(db.query(`SELECT * FROM users WHERE id = '${id}'`)) 281 | logger?.('User fetched successfully') 282 | 283 | return user 284 | } 285 | 286 | const program = Koka.try(getUser('123')).handle({ 287 | Database: { 288 | query: async (sql) => ({ id: '123', name: 'John Doe' }), 289 | }, 290 | Logger: (message) => console.log(`[INFO] ${message}`), 291 | }) 292 | 293 | const result = await Koka.run(program) 294 | ``` 295 | 296 | ### Task Management 297 | 298 | ```typescript 299 | import * as Koka from 'koka' 300 | import * as Task from 'koka/task' 301 | 302 | function* processUserData(userId: string) { 303 | // Fetch data in parallel 304 | const data = yield* Task.object({ 305 | user: () => fetchUser(userId), 306 | posts: () => fetchPosts(userId), 307 | comments: () => fetchComments(userId), 308 | }) 309 | 310 | // Process data in parallel 311 | const processed = yield* Task.object({ 312 | user: () => processUser(data.user), 313 | posts: () => processPosts(data.posts), 314 | comments: () => processComments(data.comments), 315 | }) 316 | 317 | return processed 318 | } 319 | 320 | const result = await Koka.run(processUserData('123')) 321 | ``` 322 | 323 | ## Requirements 324 | 325 | - Node.js >= 22.18 326 | - TypeScript >= 5.0 327 | 328 | ## Browser Support 329 | 330 | Koka requires: 331 | 332 | - ES2015+ (for generators) 333 | - Promise support 334 | - Symbol support 335 | 336 | For older browsers, consider using a polyfill or transpiler. 337 | 338 | ## Contributing 339 | 340 | We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details. 341 | 342 | ### Development 343 | 344 | ```bash 345 | # Install dependencies 346 | pnpm install 347 | 348 | # Run tests 349 | pnpm test 350 | 351 | # Run tests with coverage 352 | pnpm test:coverage 353 | 354 | # Build the project 355 | pnpm build 356 | ``` 357 | 358 | ## License 359 | 360 | MIT License - see [LICENSE](./LICENSE) for details. 361 | 362 | ## Related Projects 363 | 364 | - [Effect-TS](https://effect.website/) - Comprehensive algebraic effects library 365 | - [Algebraic Effects Research](https://en.wikipedia.org/wiki/Algebraic_effect) - Theory behind algebraic effects 366 | 367 | ## Support 368 | 369 | - [GitHub Issues](https://github.com/koka-ts/koka/issues) 370 | - [GitHub Discussions](https://github.com/koka-ts/koka/discussions) 371 | - [Documentation](./docs/) 372 | 373 | --- 374 | 375 | Made with ❤️ by the Koka team 376 | -------------------------------------------------------------------------------- /packages/koka-accessor/README.md: -------------------------------------------------------------------------------- 1 | # koka-accessor - Get/Set Immutable Data Made Easy 2 | 3 | **Warning: This library is in early development and may change significantly. Do not use in production yet.** 4 | 5 | koka-accessor makes working with immutable data structures effortless, providing a simple and type-safe way to: 6 | 7 | - **Get** deeply nested values 8 | - **Set** values without mutating original data 9 | - **Transform** complex data structures with ease 10 | 11 | Built on composable accessors with full TypeScript type safety and seamless Koka effects integration. 12 | 13 | ## Motivation 14 | 15 | Working with immutable data in JavaScript/TypeScript often leads to verbose code like: 16 | 17 | ```typescript 18 | const newState = { 19 | ...state, 20 | user: { 21 | ...state.user, 22 | profile: { 23 | ...state.user.profile, 24 | name: 'New Name', 25 | }, 26 | }, 27 | } 28 | ``` 29 | 30 | koka-accessor simplifies this to: 31 | 32 | ```typescript 33 | const nameAccessor = Accessor.root().prop('user').prop('profile').prop('name') 34 | 35 | const updateName = Accessor.set(state, nameAccessor, 'New Name') 36 | 37 | const result = Eff.runResult(updateName) // { user: { profile: { name: 'New Name' } } } 38 | ``` 39 | 40 | ### Why koka-accessor? 41 | 42 | - 🚀 **Simpler immutable updates** - No more spread operator hell 43 | - 🔍 **Type-safe access** - Catch errors at compile time 44 | - 🧩 **Composable operations** - Build complex transformations from simple parts 45 | - ⚡ **Performance optimized** - Automatic caching for repeated accesses 46 | 47 | ## Features 48 | 49 | - **Effortless immutable updates**: Modify nested data without mutation 50 | - **Type-safe accessors**: Compile-time checking for all operations 51 | - **Composable API**: Chain operations naturally 52 | - **Smart caching**: Optimized performance for repeated accesses 53 | - **Koka integration**: Works seamlessly with effects system 54 | - **Proxy syntax**: Access nested properties with native dot/array notation 55 | 56 | ## Installation 57 | 58 | ```bash 59 | npm install koka koka-accessor 60 | # or 61 | yarn add koka koka-accessor 62 | # or 63 | pnpm add koka koka-accessor 64 | ``` 65 | 66 | ## Getting Started 67 | 68 | ### Core Concepts 69 | 70 | An **Accessor** is a bidirectional path into your data structure that lets you: 71 | 72 | 1. **Get** values (like a getter) 73 | 2. **Set** values (like a setter) 74 | 3. **Transform** values (like a mapper) 75 | 76 | All while preserving immutability and type safety. 77 | 78 | ```typescript 79 | import { Eff } from 'koka' 80 | import { Accessor } from 'koka-accessor' 81 | 82 | // Create root accessor 83 | const root = Accessor.root() 84 | 85 | // Create object property accessor 86 | const nameAccessor = Accessor.root<{ name: string }>().prop('name') 87 | 88 | // Create array index accessor 89 | const firstItemAccessor = Accessor.root().index(0) 90 | ``` 91 | 92 | ## Basic Usage 93 | 94 | ### Getting Values 95 | 96 | ```typescript 97 | const valueResult = Eff.runResult(Accessor.get({ name: 'Alice' }, nameAccessor)) 98 | if (valueResult.type === 'ok') { 99 | const value = valueResult.value // 'Alice' 100 | } 101 | 102 | const firstResult = Eff.runResult(Accessor.get([1, 2, 3], firstItemAccessor)) 103 | if (firstResult.type === 'ok') { 104 | const first = firstResult.value // 1 105 | } 106 | ``` 107 | 108 | ### Setting Values 109 | 110 | ```typescript 111 | // Set with value 112 | const updatedResult = Eff.runResult(Accessor.set({ name: 'Alice' }, nameAccessor, 'Bob')) 113 | if (updatedResult.type === 'ok') { 114 | const updated = updatedResult.value // {name: 'Bob'} 115 | } 116 | 117 | // Set with updater function 118 | const incrementedResult = Eff.runResult(Accessor.set([1, 2, 3], firstItemAccessor, (n) => n + 1)) 119 | if (incrementedResult.type === 'ok') { 120 | const incremented = incrementedResult.value // [2, 2, 3] 121 | } 122 | ``` 123 | 124 | ## Advanced Accessors 125 | 126 | ### Proxy Syntax 127 | 128 | Access nested properties using familiar dot/array notation: 129 | 130 | ```typescript 131 | // Simple property access 132 | const simpleAccessor = Accessor.root<{ a: number }>().proxy(p => p.a) 133 | const result = Eff.runResult(Accessor.get({ a: 42 }, simpleAccessor)) 134 | // result.value === 42 135 | 136 | // Array index access 137 | const arrayAccessor = Accessor.root().proxy(p => p[0]) 138 | const result = Eff.runResult(Accessor.get([42], arrayAccessor)) 139 | // result.value === 42 140 | 141 | // Chained operations 142 | const chainedAccessor = Accessor.root<{ items: { value: number }[] }>() 143 | .proxy(p => p.items[0].value) 144 | const result = Eff.runResult(Accessor.get({ items: [{ value: 42 }] }, chainedAccessor)) 145 | // result.value === 42 146 | 147 | // Deeply nested access 148 | type ComplexState = { 149 | a: { 150 | b: { 151 | c: { 152 | e: { 153 | f: { 154 | g: { h: string }[] 155 | } 156 | } 157 | }[] 158 | } 159 | } 160 | } 161 | 162 | const deepAccessor = Accessor.root().proxy(p => p.a.b.c[1].e.f.g[2].h) 163 | 164 | const state = { 165 | a: { 166 | b: { 167 | c: [ 168 | { e: { f: { g: [{ h: 'first' }] } }, 169 | { e: { f: { g: [{ h: 'second' }, { h: 'third' }, { h: 'target' }] } } 170 | ] 171 | } 172 | } 173 | } 174 | 175 | const result = Eff.runResult(Accessor.get(state, deepAccessor)) 176 | // result.value === 'target' 177 | ``` 178 | 179 | ### Object Composition 180 | 181 | ```typescript 182 | const userAccessor = Accessor.object({ 183 | name: Accessor.root().prop('name'), 184 | age: Accessor.root().prop('age'), 185 | }) 186 | 187 | const user = { 188 | name: 'Alice', 189 | age: 30, 190 | email: 'alice@example.com', 191 | } 192 | 193 | const result = Eff.runResult(Accessor.get(user, userAccessor)) 194 | if (result.type === 'ok') { 195 | const value = result.value // {name: 'Alice', age: 30} 196 | } 197 | ``` 198 | 199 | ### Array Operations 200 | 201 | ```typescript 202 | // Find item 203 | const foundResult = Eff.runResult( 204 | Accessor.get( 205 | [1, 2, 3, 4], 206 | Accessor.root().find((n) => n > 2), 207 | ), 208 | ) 209 | if (foundResult.type === 'ok') { 210 | const found = foundResult.value // 3 211 | } 212 | 213 | // Filter items 214 | const filteredResult = Eff.runResult( 215 | Accessor.get( 216 | [1, 2, 3, 4], 217 | Accessor.root().filter((n) => n % 2 === 0), 218 | ), 219 | ) 220 | if (filteredResult.type === 'ok') { 221 | const filtered = filteredResult.value // [2, 4] 222 | } 223 | 224 | // Map items 225 | const mappedResult = Eff.runResult( 226 | Accessor.get( 227 | [1, 2, 3], 228 | Accessor.root().map((n) => n * 2), 229 | ), 230 | ) 231 | if (mappedResult.type === 'ok') { 232 | const mapped = mappedResult.value // [2, 4, 6] 233 | } 234 | ``` 235 | 236 | ### Type Narrowing and Value Validation 237 | 238 | #### Type Narrowing with `match` 239 | 240 | ```typescript 241 | const numberAccessor = Accessor.root().match((v): v is number => typeof v === 'number') 242 | 243 | const numberResult = Eff.runResult(Accessor.get(42, numberAccessor)) 244 | if (numberResult.type === 'ok') { 245 | const value = numberResult.value // 42 246 | } 247 | 248 | const stringResult = Eff.runResult(Accessor.get('test', numberAccessor)) 249 | if (stringResult.type === 'err') { 250 | // throws AccessorErr 251 | } 252 | ``` 253 | 254 | #### Value Validation with `refine` 255 | 256 | ```typescript 257 | const refinedAccessor = Accessor.root().refine((n) => n > 0) 258 | 259 | const positiveResult = Eff.runResult(Accessor.get(42, refinedAccessor)) 260 | 261 | if (positiveResult.type === 'ok') { 262 | const value = positiveResult.value // 42 263 | } 264 | 265 | const negativeResult = Eff.runResult(Accessor.get(-1, refinedAccessor)) 266 | 267 | if (negativeResult.type === 'err') { 268 | // throws AccessorErr 269 | } 270 | ``` 271 | 272 | ## Caching Behavior 273 | 274 | koka-accessor automatically caches accessor computations for better performance: 275 | 276 | ```typescript 277 | const user = { name: 'Alice', age: 30 } 278 | const nameAccessor = Accessor.root().prop('name') 279 | 280 | // First access - computes and caches 281 | const name1Result = Eff.runResult(Accessor.get(user, nameAccessor)) 282 | const name2Result = Eff.runResult(Accessor.get(user, nameAccessor)) 283 | 284 | if (name1Result.type === 'ok' && name2Result.type === 'ok') { 285 | const name1 = name1Result.value 286 | const name2 = name2Result.value 287 | name1 === name2 // true 288 | } 289 | ``` 290 | 291 | Caching works for all accessor types including: 292 | 293 | - Object properties 294 | - Array indices 295 | - Find operations 296 | - Filter operations 297 | - Map operations 298 | - Type refinements 299 | 300 | ## Error Handling 301 | 302 | All operations can throw `AccessorErr`: 303 | 304 | ```typescript 305 | const result = Eff.runResult(Accessor.get([], Accessor.root().index(0))) 306 | if (result.type === 'err') { 307 | console.error('Accessor error:', result.error.message) 308 | } 309 | ``` 310 | 311 | Common error cases: 312 | 313 | - Accessing non-existent array indices 314 | - Failed find/filter operations 315 | - Type refinement failures 316 | - Invalid updates 317 | 318 | ## API Reference 319 | 320 | ### Static Methods 321 | 322 | | Method | Description | 323 | | ---------------------------------------------- | ------------------------------- | 324 | | `Accessor.root()` | Create root accessor for type T | 325 | | `Accessor.object(fields)` | Compose object accessors | 326 | | `Accessor.optional(accessor)` | Create optional accessor | 327 | | `Accessor.get(root, accessor)` | Get value through accessor | 328 | | `Accessor.set(root, accessor, valueOrUpdater)` | Set value through accessor | 329 | 330 | ### Instance Methods 331 | 332 | | Method | Description | 333 | | ------------------- | ---------------------- | 334 | | `prop(key)` | Access object property | 335 | | `index(n)` | Access array index | 336 | | `find(predicate)` | Find array item | 337 | | `filter(predicate)` | Filter array | 338 | | `map(transform)` | Transform values | 339 | | `match(predicate)` | Type narrow | 340 | | `refine(predicate)` | Value validation | 341 | | `select(selector)` | Custom selector | 342 | | `proxy(selector)` | Proxy access | 343 | 344 | ## Best Practices 345 | 346 | 1. **Compose accessors** to build complex data access patterns 347 | 2. **Reuse accessors** to benefit from caching 348 | 3. **Combine with effects** for async operations 349 | 4. **Handle errors** for robust code 350 | 5. **Leverage TypeScript** for maximum type safety 351 | 352 | ## Examples 353 | 354 | See the [test cases](packages/koka-accessor/__tests__/koka-accessor.test.ts) for more comprehensive usage examples. 355 | 356 | ## License 357 | 358 | MIT 359 | 360 | ## Contributing 361 | 362 | Contributions are welcome! 363 | -------------------------------------------------------------------------------- /projects/koka-react-demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as Koka from 'koka' 2 | import * as Domain from 'koka-domain' 3 | import * as Result from 'koka/result' 4 | import { useDomainState, useDomainQuery } from 'koka-react' 5 | import './App.css' 6 | import { type TodoFilter, TodoListDomain, TodoFilterDomain, TodoAppDomain, TodoDomain } from './domain' 7 | 8 | type TodoItemProps = { 9 | todo$: TodoDomain 10 | } 11 | 12 | function TodoItem(props: TodoItemProps) { 13 | const todo$ = props.todo$ 14 | const todo = useDomainState(todo$) 15 | 16 | const handleToggle = () => { 17 | // @ts-expect-error for test 18 | Koka.runSync(todo$.toggleTodo()) 19 | } 20 | 21 | const handleRemove = () => { 22 | // @ts-expect-error for test 23 | Koka.runAsync(todo$.removeTodo()) 24 | } 25 | 26 | const handleTextChange = (e: React.ChangeEvent) => { 27 | // @ts-expect-error for test 28 | Koka.runSync(todo$.updateTodoText(e.target.value)) 29 | } 30 | 31 | return ( 32 |
  • 37 | 43 | 49 | 50 | 56 |
  • 57 | ) 58 | } 59 | 60 | type TodoInputProps = { 61 | todoApp$: TodoAppDomain 62 | } 63 | 64 | function TodoInput(props: TodoInputProps) { 65 | const todoApp$ = props.todoApp$ 66 | const input = useDomainState(todoApp$.input$) 67 | 68 | const handleSubmit = async (e: React.FormEvent) => { 69 | e.preventDefault() 70 | 71 | const effector = Koka.try(todoApp$.addTodo()).handle({ 72 | TodoInputErr: (message) => { 73 | alert(message) 74 | }, 75 | }) 76 | 77 | await Result.runAsync(effector) 78 | } 79 | 80 | const handleInputChange = (e: React.ChangeEvent) => { 81 | // @ts-expect-error for test 82 | Koka.runAsync(todoApp$.updateInput(e.target.value)) 83 | } 84 | 85 | return ( 86 |
    87 | 94 | 100 |
    101 | ) 102 | } 103 | 104 | type TodoFilterProps = { 105 | filter$: TodoFilterDomain 106 | } 107 | 108 | function TodoFilter(props: TodoFilterProps) { 109 | const filter$ = props.filter$ 110 | const currentFilter = useDomainState(filter$) 111 | 112 | const handleFilterChange = (filter: TodoFilter) => { 113 | // @ts-expect-error for test 114 | Koka.runSync(filter$.setFilter(filter)) 115 | } 116 | 117 | const filterButtons = [ 118 | { key: 'all' as const, label: 'all' }, 119 | { key: 'undone' as const, label: 'undone' }, 120 | { key: 'done' as const, label: 'done' }, 121 | ] 122 | 123 | return ( 124 |
    125 | {filterButtons.map(({ key, label }) => ( 126 | 137 | ))} 138 |
    139 | ) 140 | } 141 | 142 | type TodoStatsProps = { 143 | todoList$: TodoListDomain 144 | } 145 | 146 | function TodoStats(props: TodoStatsProps) { 147 | const todoList$ = props.todoList$ 148 | const activeCount = useDomainState(todoList$.activeTodoList$.prop('length')) 149 | const completedCount = useDomainState(todoList$.completedTodoList$.prop('length')) 150 | 151 | const totalCount = activeCount + completedCount 152 | 153 | return ( 154 |
    155 |
    156 |
    {totalCount}
    157 |
    total
    158 |
    159 |
    160 |
    {activeCount}
    161 |
    undone
    162 |
    163 |
    164 |
    {completedCount}
    165 |
    done
    166 |
    167 |
    168 | ) 169 | } 170 | 171 | type TodoListHeaderProps = { 172 | todoList$: TodoListDomain 173 | } 174 | 175 | function TodoListHeader(props: TodoListHeaderProps) { 176 | const todoDoneList$ = props.todoList$.map((todo) => todo.prop('done')) 177 | const todoDoneList = useDomainState(todoDoneList$) 178 | const allCompleted = todoDoneList.length > 0 && todoDoneList.every((done) => done) 179 | 180 | const handleToggleAll = () => { 181 | // @ts-expect-error for test 182 | Koka.runSync(props.todoList$.toggleAll()) 183 | } 184 | 185 | return ( 186 |
    187 | 193 | 194 | {allCompleted ? 'unselect all' : 'select all'} 195 | 196 |
    197 | ) 198 | } 199 | 200 | type TodoListItemsProps = { 201 | getFilteredTodoIds: Domain.Query 202 | todoList$: TodoListDomain 203 | } 204 | 205 | function TodoListItems(props: TodoListItemsProps) { 206 | const todoList$ = props.todoList$ 207 | 208 | const filteredTodoIds = useDomainQuery(props.getFilteredTodoIds) 209 | 210 | return ( 211 |
      212 | {filteredTodoIds.map((id) => ( 213 | 214 | ))} 215 |
    216 | ) 217 | } 218 | 219 | type TodoListFooterProps = { 220 | todoList$: TodoListDomain 221 | } 222 | 223 | function TodoListFooter(props: TodoListFooterProps) { 224 | const todoList$ = props.todoList$ 225 | const todoDoneList$ = todoList$.map((todo) => todo.prop('done')) 226 | const todoDoneList = useDomainState(todoDoneList$) 227 | const hasCompleted = todoDoneList.length > 0 && todoDoneList.some((done) => done) 228 | 229 | const handleClearCompleted = () => { 230 | // @ts-expect-error for test 231 | Koka.runSync(todoList$.clearCompleted()) 232 | } 233 | 234 | if (!hasCompleted) { 235 | return null 236 | } 237 | 238 | return ( 239 |
    240 | 246 |
    247 | ) 248 | } 249 | 250 | function EmptyTodoList() { 251 | return ( 252 |
    253 |
    📝
    254 |

    no todos, add one!

    255 |
    256 | ) 257 | } 258 | 259 | type TodoListProps = { 260 | getTodoIds: Domain.Query 261 | todoList$: TodoListDomain 262 | } 263 | 264 | function TodoList(props: TodoListProps) { 265 | const todoList$ = props.todoList$ 266 | 267 | const todoCount = useDomainState(todoList$.prop('length')) 268 | 269 | if (todoCount === 0) { 270 | return 271 | } 272 | 273 | return ( 274 |
    275 | 276 | 277 | 278 |
    279 | ) 280 | } 281 | 282 | type AppProps = { 283 | todoApp$: TodoAppDomain 284 | } 285 | 286 | function App(props: AppProps) { 287 | const todoApp$ = props.todoApp$ 288 | 289 | return ( 290 |
    291 |

    292 | Todo App 293 |

    294 | 295 | 296 | 297 | 298 | 299 |
    300 | ) 301 | } 302 | 303 | export default App 304 | --------------------------------------------------------------------------------