diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/.gitignore b/src/new/templates/rust/ui/hyperapp-skeleton/.gitignore new file mode 100644 index 00000000..25b216b3 --- /dev/null +++ b/src/new/templates/rust/ui/hyperapp-skeleton/.gitignore @@ -0,0 +1,9 @@ +*/target/ +/target +pkg/*.wasm +pkg/ui +*.swp +*.swo +*/wasi_snapshot_preview1.wasm +*/wit/ +*/process_env diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/README.md b/src/new/templates/rust/ui/hyperapp-skeleton/README.md index 87c2a5bc..5e773f1c 100644 --- a/src/new/templates/rust/ui/hyperapp-skeleton/README.md +++ b/src/new/templates/rust/ui/hyperapp-skeleton/README.md @@ -3,38 +3,18 @@ A minimal, well-commented skeleton application for the Hyperware platform using the Hyperapp framework. This skeleton provides a starting point for building Hyperware applications with a React/TypeScript frontend and Rust backend. -Either prompt your favorite LLM directly with instructions on how to build your app or add them to `instructions.md`! +Example prompt (works well with Codex): -Recommended usage: -- Create a skeleton repo using a kit template: - ```bash - kit new foo --template hyperapp-skeleton --ui - cd foo - ``` -- Write a detailed document describing what you want your app to do. - Save this in `instructions.md`. -- Prompt your LLM agent (i.e. Claude Code) with something like: - ``` - ## GOAL - - - - ## Instructions - - Read the README.md and follow the Instructions > Create an implementation plan - ``` - -- After creating an implementation plan, clear your LLM agent's context and then prompt it again with something like: - - ``` - ## GOAL - - +``` +Use `kit new myappname --template hyperapp-skeleton --ui`, (replacing myappname with appropriate app name) to make a template in `/desired_folder`, which you will modify to build the following app: - ## Instructions +Insert your app spec here, e.g.: +Todo List with P2P Sync. +A collaborative TODO list where items sync between nodes. - Read the README.md and follow the Instructions > Implement the plan - ``` +Write a spec, and then implement it step by step. Use the README.md given in hyperapp-skeleton to find instructions on specific details. +At the end, I should be able to run `kit bs β€”hyperapp` and manually test that the app works. +``` The rest of this document is aimed at *LLMs* not *humans*. @@ -98,6 +78,18 @@ async fn my_endpoint(&self) -> String { } ``` +#### Remote Requests +All remote requests must use `.expects_response(30)`, where the value 30 sets a 30‑second response timeout. +```rust +let req = Request::to(("friend.os", "some-hyperapp", "some-hyperapp", "publisher.os")) + .expects_response(30) + .blob(LazyLoadBlob { + mime: None, + bytes: message, + }) + .body(body); +``` + #### Frontend API Calls Parameters must be sent as tuples for multi-parameter methods: ```typescript @@ -108,6 +100,14 @@ Parameters must be sent as tuples for multi-parameter methods: { "MethodName": [param1, param2] } ``` +#### Frontend keys in snake_case +All keys in TypeScript need to stay in snake_case (`node_id`), camelCase (`nodeId`) will break the app! +```typescript +export interface StatusSnapshot { + node_id: string; + } +``` + #### The /our.js Script MUST be included in index.html: ```html @@ -120,8 +120,8 @@ Your app's state is automatically persisted based on the `save_config` option: - `OnDiff`: Save when state changes (strongly recommended) - `Never`: No automatic saves - `EveryMessage`: Save after each message (safest; slowest) -- `EveeyNMessage(u64)`: Save every N messages received -- `EveeyNSeconds(u64)`: Save every N seconds +- `EveryNMessage(u64)`: Save every N messages received +- `EveryNSeconds(u64)`: Save every N seconds ## Customization Guide @@ -159,7 +159,11 @@ Add system permissions in `pkg/manifest.json`: These are required to message other local processes. They can also be granted so other local processes can message us. -There is also a `request_networking` field that must be true to send messages over the network p2p. + +If sending messages between nodes, set: +```json +"request_networking": true, +``` ### 4. Update Frontend @@ -167,10 +171,6 @@ There is also a `request_networking` field that must be true to send messages ov 2. Update store in `ui/src/store/hyperapp-skeleton.ts` 3. Modify UI in `ui/src/App.tsx` -### 5. Rename as appropriate - -Change names throughout from `hyperapp-skeleton` (and variants) as appropriate if user describes app name. - ## Common Issues and Solutions ### "Failed to deserialize HTTP request" @@ -187,30 +187,25 @@ Change names throughout from `hyperapp-skeleton` (and variants) as appropriate i - Add #[derive(PartialEq)] to structs ### Import Errors -- Don't add `hyperware_process_lib` to Cargo.toml -- Use imports from `hyperprocess_macro` +- Import the most important structs and functions from `hyperware_process_lib`, e.g. `Request`, `LazyLoadBlob`, `ProcessId` -## Testing Your App +### manifest.json missing +- Run `kit b --hyperapp` to generate it -1. Deploy app to a Hyperware node (after building, if requested): - ```bash - kit start-packages - ``` -2. Your app will be automatically installed and available at `http://localhost:8080` -3. Check the Hyperware homepage for your app icon +### Naming Restrictions +- No struct/enum/interface name is allowed to contain digits or the substring "stream", because WIT doesn't allow it +- No record/variant/enum name is allowed to end with `Request`, `Response`, `RequestWrapper`, `ResponseWrapper`, because TS caller utils are autogenerated with those suffixes ## Instructions ### Create an implementation plan -Carefully read the prompt; look carefully at `instructions.md` (if it exists) and in the resources/ directory. -In particular, note the example applications `resources/example-apps/sign/`, `resources/example-apps/id/`, and `resources/example-apps/file-explorer`. Note that `file-explorer` example contains an `api`, which is generated by the compiler, and not human or LLM written. +Carefully read the prompt; look carefully at `instructions.md` (if it exists) and in the example-apps directory. +In particular, note the example applications `example-apps/sign/`, `example-apps/id/`, and `example-apps/file-explorer`. +Note that `file-explorer` example contains an `api` folder, which is generated by the compiler, and not human or LLM written. `sign` and `id` demonstrate local messaging. `file-explorer` demonstrates VFS interactions. -Expand the prompt and/or `instructions.md` into a detailed implementation plan. -The implementor will be starting from this existing template that exists at `hyperapp-skeleton/` and `ui/`. - Note in particular that bindings for the UI will be generated when the app is built with `kit build --hyperapp`. As such, first design and implement the backend; the interface will be generated from the backend; finally design and implement the frontend to consume the interface. Subsequent changes to the interface must follow this pattern as well: start in backend, generate interface, finish in frontend @@ -219,15 +214,9 @@ Do NOT create the API. The API is machine generated. You create types that end up in the API by defining and using them in functions in the Rust backend "hyperapp" -Do NOT write code: just create a detailed `IMPLEMENTATION_PLAN.md` that will be used by the implementor. -The implementor will have access to `resources/` but will be working from `IMPLEMENTATION_PLAN.md`, so include all relevant context in the PLAN. -You can refer the implementor to `resources/` but do not assume the implementor has read them unless you refer them there. - ### Implement the plan -Look carefully at `IMPLEMENTATION_PLAN.md` and in the `resources/` directory, if relevant. -In particular, note the example applications `resources/example-apps/sign/`, `resources/example-apps/id/`, and `resources/example-apps/file-explorer`. -Use them if useful. +Look carefully at `IMPLEMENTATION_PLAN.md` and in the `example-apps/` directory, if relevant. Work from the existing template that exists at `hyperapp-skeleton/` and `ui/`. @@ -245,5 +234,3 @@ Notice that this all happens within those functions: just take the rust types as If you create a GUI for the app you MUST use target/ui/caller-utils.ts for HTTP requests to the backend. Do NOT edit this file: it is machine generated. Do NOT do `fetch` or other HTTP requests manually to the backend: use the functions in this machine generated interface. - -Implement the application described in the `IMPLEMENTATION_PLAN.md`. diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/Cargo.toml b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/Cargo.toml similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/Cargo.toml rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/Cargo.toml diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/api/file-explorer-sys-v0.wit b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/api/file-explorer-sys-v0.wit similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/api/file-explorer-sys-v0.wit rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/api/file-explorer-sys-v0.wit diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/api/file-explorer.wit b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/api/file-explorer.wit similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/api/file-explorer.wit rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/api/file-explorer.wit diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/api/types-file-explorer-sys-v0.wit b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/api/types-file-explorer-sys-v0.wit similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/api/types-file-explorer-sys-v0.wit rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/api/types-file-explorer-sys-v0.wit diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/explorer/Cargo.toml b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/explorer/Cargo.toml similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/explorer/Cargo.toml rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/explorer/Cargo.toml diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/explorer/src/icon b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/explorer/src/icon similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/explorer/src/icon rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/explorer/src/icon diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/explorer/src/lib.rs b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/explorer/src/lib.rs similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/explorer/src/lib.rs rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/explorer/src/lib.rs diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/metadata.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/metadata.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/metadata.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/metadata.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/pkg/manifest.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/pkg/manifest.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/pkg/manifest.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/pkg/manifest.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/.eslintrc.cjs b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/.eslintrc.cjs similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/.eslintrc.cjs rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/.eslintrc.cjs diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/.gitignore b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/.gitignore similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/.gitignore rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/.gitignore diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/README.md b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/README.md similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/README.md rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/README.md diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/index.html b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/index.html similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/index.html rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/index.html diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/package-lock.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/package-lock.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/package-lock.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/package-lock.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/package.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/package.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/package.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/package.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/App.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/App.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/App.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/App.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/App.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/App.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/App.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/App.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/assets/react.svg b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/assets/react.svg similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/assets/react.svg rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/assets/react.svg diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/assets/vite.svg b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/assets/vite.svg similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/assets/vite.svg rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/assets/vite.svg diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ContextMenu/ContextMenu.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Breadcrumb.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileExplorer.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileItem.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/FileList.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/FileExplorer/Toolbar.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/ShareDialog/ShareDialog.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/Upload/UploadZone.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/Upload/UploadZone.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/Upload/UploadZone.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/Upload/UploadZone.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/Upload/UploadZone.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/Upload/UploadZone.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/components/Upload/UploadZone.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/components/Upload/UploadZone.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/contexts/ThemeContext.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/contexts/ThemeContext.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/contexts/ThemeContext.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/contexts/ThemeContext.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/index.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/index.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/index.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/index.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/lib/api.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/lib/api.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/lib/api.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/lib/api.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/main.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/main.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/main.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/main.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/store/fileExplorer.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/store/fileExplorer.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/store/fileExplorer.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/store/fileExplorer.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/types/api.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/types/api.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/types/api.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/types/api.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/types/global.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/types/global.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/types/global.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/types/global.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/vite-env.d.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/vite-env.d.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/src/vite-env.d.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/src/vite-env.d.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/tsconfig.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/tsconfig.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/tsconfig.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/tsconfig.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/tsconfig.node.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/tsconfig.node.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/tsconfig.node.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/tsconfig.node.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/vite.config.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/vite.config.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/file-explorer/ui/vite.config.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/file-explorer/ui/vite.config.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/Cargo.toml b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/Cargo.toml similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/Cargo.toml rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/Cargo.toml diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/README.md b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/README.md similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/README.md rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/README.md diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/id/Cargo.toml b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/id/Cargo.toml similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/id/Cargo.toml rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/id/Cargo.toml diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/id/src/icon b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/id/src/icon similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/id/src/icon rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/id/src/icon diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/id/src/lib.rs b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/id/src/lib.rs similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/id/src/lib.rs rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/id/src/lib.rs diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/metadata.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/metadata.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/metadata.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/metadata.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/pkg/manifest.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/pkg/manifest.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/pkg/manifest.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/pkg/manifest.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/.eslintrc.cjs b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/.eslintrc.cjs similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/.eslintrc.cjs rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/.eslintrc.cjs diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/.gitignore b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/.gitignore similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/.gitignore rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/.gitignore diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/README.md b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/README.md similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/README.md rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/README.md diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/index.html b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/index.html similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/index.html rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/index.html diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/package-lock.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/package-lock.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/package-lock.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/package-lock.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/package.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/package.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/package.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/package.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/public/assets/vite.svg b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/public/assets/vite.svg similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/public/assets/vite.svg rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/public/assets/vite.svg diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/App.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/App.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/App.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/App.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/App.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/App.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/App.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/App.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/assets/react.svg b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/assets/react.svg similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/assets/react.svg rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/assets/react.svg diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/assets/vite.svg b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/assets/vite.svg similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/assets/vite.svg rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/assets/vite.svg diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/index.css b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/index.css similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/index.css rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/index.css diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/main.tsx b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/main.tsx similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/main.tsx rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/main.tsx diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/store/id.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/store/id.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/store/id.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/store/id.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/types/Id.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/types/Id.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/types/Id.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/types/Id.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/types/global.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/types/global.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/types/global.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/types/global.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/vite-env.d.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/vite-env.d.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/src/vite-env.d.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/src/vite-env.d.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/tsconfig.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/tsconfig.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/tsconfig.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/tsconfig.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/tsconfig.node.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/tsconfig.node.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/tsconfig.node.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/tsconfig.node.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/vite.config.ts b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/vite.config.ts similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/id/ui/vite.config.ts rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/id/ui/vite.config.ts diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/Cargo.toml b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/Cargo.toml similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/Cargo.toml rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/Cargo.toml diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/README.md b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/README.md similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/README.md rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/README.md diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/metadata.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/metadata.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/metadata.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/metadata.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/pkg/manifest.json b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/pkg/manifest.json similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/pkg/manifest.json rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/pkg/manifest.json diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/sign/Cargo.toml b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/sign/Cargo.toml similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/sign/Cargo.toml rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/sign/Cargo.toml diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/sign/src/icon b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/sign/src/icon similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/sign/src/icon rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/sign/src/icon diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/sign/src/lib.rs b/src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/sign/src/lib.rs similarity index 100% rename from src/new/templates/rust/ui/hyperapp-skeleton/resources/example-apps/sign/sign/src/lib.rs rename to src/new/templates/rust/ui/hyperapp-skeleton/example-apps/sign/sign/src/lib.rs diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/README.md b/src/new/templates/rust/ui/hyperapp-skeleton/resources/README.md deleted file mode 100644 index da38ba19..00000000 --- a/src/new/templates/rust/ui/hyperapp-skeleton/resources/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# πŸ“š Hyperware Skeleton App Resources - -This directory contains all the resources needed to transform the skeleton app into any type of Hyperware application. - -## πŸ“– Development Guides - -The [`guides/`](./guides/) directory contains comprehensive documentation for building Hyperware apps: - -- **[Quick Reference](./guides/00-QUICK-REFERENCE.md)** - Essential rules and syntax -- **[Common Patterns](./guides/01-COMMON-PATTERNS.md)** - Ready-to-use code recipes -- **[Troubleshooting](./guides/02-TROUBLESHOOTING.md)** - Fix common errors -- **[WIT Types Guide](./guides/03-WIT-TYPES-DATA-MODELING.md)** - Data modeling constraints -- **[P2P Patterns](./guides/04-P2P-PATTERNS.md)** - Node-to-node communication -- **[Frontend Guide](./guides/05-UI-FRONTEND-GUIDE.md)** - React/TypeScript development -- **[Testing Guide](./guides/06-TESTING-DEBUGGING.md)** - Debug and test strategies -- **[Complete Examples](./guides/07-COMPLETE-EXAMPLES.md)** - Full working apps -- **[Manifest & Deployment](./guides/08-MANIFEST-AND-DEPLOYMENT.md)** - Understanding manifest.json -- **[Capabilities Guide](./guides/09-CAPABILITIES-GUIDE.md)** - System permissions reference - -See the [Guides README](./guides/README.md) for detailed navigation help. - -## πŸ’‘ Example App Ideas - -The [`example-apps/TODO.md`](./example-apps/TODO.md) file contains 12+ app ideas ranging from basic to advanced: - -- Todo lists and notepads -- P2P chat and file sharing -- Collaborative tools -- Games and marketplaces -- System utilities - -Each idea includes implementation notes and key concepts to demonstrate. - -## 🎯 How to Use These Resources - -### Starting a New App -1. Copy the skeleton app -2. Read the Quick Reference guide -3. Find a similar example in Complete Examples -4. Use Common Patterns for specific features - -### When You're Stuck -1. Check Troubleshooting for your error -2. Verify all requirements in Quick Reference -3. Look for working patterns in Complete Examples -4. Test with simpler code first - -### For Specific Features -- **State Management** β†’ Common Patterns section 1 -- **P2P Communication** β†’ P2P Patterns guide -- **File Handling** β†’ Common Patterns section 4 -- **UI Development** β†’ Frontend Guide - -## πŸ”‘ Key Principles - -1. **Start Simple** - Get basic functionality working first -2. **Test Incrementally** - Don't write everything before testing -3. **Follow Patterns** - Use proven patterns from the guides -4. **Handle Errors** - Always provide user feedback -5. **Design for P2P** - Remember there's no central server - -## πŸ“ Quick Reminders - -### Must-Have Requirements -- `` in your HTML -- Tuple format `[p1, p2]` for multi-parameter calls -- `.expects_response(30)` on remote requests - -### Common Fixes -- **Build errors** β†’ Usually missing requirements above -- **Type errors** β†’ Use JSON strings for complex types -- **P2P failures** β†’ Check node names and ProcessId format -- **UI issues** β†’ Verify /our.js is included -- **manifest.json missing** β†’ Run `kit b --hyperapp` to generate it -- **Capability errors** β†’ Check Capabilities Guide for required permissions - -## πŸš€ Next Steps - -1. Review the skeleton app's heavily commented `lib.rs` -2. Pick an example from Complete Examples to study -3. Start modifying the skeleton incrementally -4. Test with multiple nodes for P2P features - -Remember: The skeleton app is designed to compile and run immediately. Build on that working foundation! \ No newline at end of file diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/00-QUICK-REFERENCE.md b/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/00-QUICK-REFERENCE.md deleted file mode 100644 index c5c70cca..00000000 --- a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/00-QUICK-REFERENCE.md +++ /dev/null @@ -1,272 +0,0 @@ -# πŸš€ Hyperware Quick Reference for AI Models - -## Critical Rules - MUST FOLLOW - -### 1. HTTP Endpoints Parameter Handling -```rust -// βœ… Modern approach - Direct type deserialization (requires generated caller-utils) -#[http(method = "POST")] -async fn create_item(&mut self, request: CreateItemReq) -> Result { } - -// βœ… Legacy approach - Manual JSON parsing (still valid) -#[http] -async fn create_item(&mut self, request_body: String) -> Result { - let req: CreateItemReq = serde_json::from_str(&request_body)?; -} -``` - -**Note**: The modern approach requires generated TypeScript caller-utils that wrap requests in method-named objects. - -For detailed explanation and more examples, see [Troubleshooting Guide - Section 1](./02-TROUBLESHOOTING.md#error-failed-to-deserialize-http-request) - -### 2. Frontend MUST Include `/our.js` Script -```html - - - - - -``` - -### 3. API Call Formats -```typescript -// βœ… Modern approach (with generated caller-utils) -// Single parameter - wrapped in method-named object -{ "CreateItem": { name: "foo", value: 42 } } - -// βœ… Legacy approach (manual API calls) -// Single string parameter -{ "CreateItem": "raw string value" } -// Multiple parameters as array (rare) -{ "UpdateItem": ["id123", "new value"] } -``` - -**Note**: Most modern apps use generated caller-utils that handle the wrapping automatically. - -### 4. Remote Calls MUST Set Timeout -```rust -// ❌ WRONG - No timeout -Request::new() - .target(address) - .body(data) - .send(); - -// βœ… CORRECT - Always set expects_response -Request::new() - .target(address) - .body(data) - .expects_response(30) // 30 second timeout - .send_and_await_response(30); -``` - -### 5. WIT-Compatible Types Only -```rust -// βœ… ALLOWED -String, bool, u8-u64, i8-i64, f32, f64 -Vec, Option -Simple structs with public fields - -// ❌ NOT ALLOWED -HashMap β†’ use Vec<(K,V)> -[T; N] β†’ use Vec -Complex enums β†’ use simple enums + separate data - -// πŸ”₯ ESCAPE HATCH: Return JSON strings -#[http] -async fn get_complex(&self, _request_body: String) -> String { - serde_json::to_string(&self.complex_data).unwrap() -} -``` - -## Build Commands - -```bash -# First time build (installs dependencies) -kit bs --hyperapp - -# Regular build -kit b --hyperapp - -# Clean rebuild -rm -rf target/ ui/node_modules ui/dist pkg/ -kit b --hyperapp -``` - -**Note**: `kit b --hyperapp` automatically generates `pkg/manifest.json` - -## Project Structure -``` -hyperapp-skeleton/ -β”œβ”€β”€ Cargo.toml # Workspace config -β”œβ”€β”€ metadata.json # App metadata -β”œβ”€β”€ hyperapp-skeleton/ # Rust backend -β”‚ β”œβ”€β”€ Cargo.toml # DO NOT add hyperware_process_lib here! -β”‚ └── src/ -β”‚ └── lib.rs # Main app logic -β”œβ”€β”€ ui/ # React frontend -β”‚ β”œβ”€β”€ index.html # MUST have - - My App - -``` - -**Debug in Browser Console:** -```javascript -// Check if script loaded -console.log(window.our); -// Should show: { node: "yournode.os", process: "app:package:publisher" } - -// If undefined, check network tab for /our.js request -``` - -### ❌ Error: "Failed to parse ProcessId" - -**Examples:** -``` -Failed to parse ProcessId: InvalidFormat -``` - -**Root Cause:** Incorrect ProcessId format - -**Solution:** -```rust -// ❌ WRONG formats -let pid = "myapp".parse::(); // Missing parts -let pid = "myapp:myapp".parse::(); // Missing publisher -let pid = "myapp-myapp-publisher".parse::(); // Wrong separator - -// βœ… CORRECT format: "process:package:publisher" -let pid = "myapp:myapp:publisher.os".parse::()?; - -// For your app matching remote nodes -let publisher = "template.os"; // Or whatever the remote uses -let pid = format!("hyperapp-skeleton:hyperapp-skeleton:{}", publisher) - .parse::()?; -``` - -### ❌ Error: Parameter format mismatch - -**Symptoms:** Frontend call succeeds but backend receives wrong data - -**Root Cause:** Multi-parameter endpoints need tuple format - -**Solution:** -```typescript -// ❌ WRONG - Object format -const response = await fetch('/api', { - body: JSON.stringify({ - CreateItem: { - name: "Item", - description: "Description" - } - }) -}); - -// βœ… CORRECT - Tuple/array format for multiple params -const response = await fetch('/api', { - body: JSON.stringify({ - CreateItem: ["Item", "Description"] - }) -}); - -// For single parameter, value directly -const response = await fetch('/api', { - body: JSON.stringify({ - GetItem: "item-id-123" - }) -}); -``` - ---- - -## 3. P2P Communication Issues - -### ❌ Error: "SendError" or "Failed to send request" - -**Common Causes:** - -1. **Target node not running:** -```bash -# Check if target node is accessible -# In your node's terminal, you should see incoming requests -``` - -2. **Wrong node name:** -```rust -// ❌ WRONG - Using placeholder -let target = Address::new("placeholder.os", process_id); - -// βœ… CORRECT - Use actual node name -let target = Address::new("alice.os", process_id); // Real node -``` - -3. **Missing timeout:** -```rust -// ❌ WRONG - No timeout set -Request::new() - .target(address) - .body(data) - .send(); - -// βœ… CORRECT - Always set expects_response -Request::new() - .target(address) - .body(data) - .expects_response(30) // REQUIRED! - .send_and_await_response(30)?; -``` - -4. **Wrong request format:** -```rust -// ❌ WRONG - Array format -let wrapper = json!({ - "HandleRequest": [param1, param2] // Arrays don't work -}); - -// βœ… CORRECT - Tuple format for multiple params -let wrapper = json!({ - "HandleRequest": (param1, param2) // Tuple format -}); - -// βœ… CORRECT - Single param -let wrapper = json!({ - "HandleRequest": param -}); -``` - -### ❌ Error: Remote endpoint not found - -**Symptom:** Call succeeds but returns error about missing method - -**Root Cause:** Method name mismatch or missing #[remote] attribute - -**Solution:** -```rust -// On receiving node: -#[remote] // Must have this attribute! -async fn handle_sync(&mut self, data: String) -> Result { - // Implementation -} - -// On calling node: -let wrapper = json!({ - "HandleSync": data // Must match exactly (case-sensitive) -}); -``` - -### ❌ Error: Can't decode remote response - -**Root Cause:** Response type mismatch - -**Solution:** -```rust -// ❌ Expecting wrong type -let response: ComplexType = serde_json::from_slice(&response.body())?; - -// βœ… Match what remote actually returns -let response: String = serde_json::from_slice(&response.body())?; -// Then parse if needed -let data: ComplexType = serde_json::from_str(&response)?; -``` - -### ❌ Error: ProcessId parse errors in P2P apps - -**Symptoms:** -``` -Failed to parse ProcessId: InvalidFormat -``` - -**Common P2P Pattern:** -```rust -// ❌ WRONG - Hardcoded publisher assumption -let pid = "samchat:samchat:publisher.os".parse::()?; - -// βœ… CORRECT - Use consistent publisher across nodes -let publisher = "hpn-testing-beta.os"; // Or get from config -let target_process_id_str = format!("samchat:samchat:{}", publisher); -let target_process_id = target_process_id_str.parse::() - .map_err(|e| format!("Failed to parse ProcessId: {}", e))?; -``` - -### ❌ Error: Node ID not initialized - -**Symptoms:** -``` -Sender node ID not initialized -``` - -**Root Cause:** Trying to use node ID before init - -**Solution:** -```rust -// In state -pub struct AppState { - my_node_id: Option, -} - -// In init -#[init] -async fn initialize(&mut self) { - self.my_node_id = Some(our().node.clone()); -} - -// In handlers -let sender = self.my_node_id.clone() - .ok_or_else(|| "Node ID not initialized".to_string())?; -``` - -### ❌ Error: Group/conversation management issues - -**Common P2P Chat Errors:** -```rust -// Group not found -let conversation = self.conversations.get(&group_id) - .ok_or_else(|| "Group conversation not found".to_string())?; - -// Not a group conversation -if !conversation.is_group { - return Err("Not a group conversation".to_string()); -} - -// Member already exists -if conversation.participants.contains(&new_member) { - return Err("Member already in group".to_string()); -} -``` - -### ❌ Error: Remote file/data fetch failures - -**Complex P2P data retrieval pattern:** -```rust -// Try local first, then remote -match local_result { - Ok(response) => { - if let Some(blob) = response.blob() { - return Ok(blob.bytes); - } - }, - Err(_) => { - // Fetch from remote node - let remote_result = Request::new() - .target(remote_address) - .body(request_body) - .expects_response(30) - .send_and_await_response(30)?; - - match remote_result { - Ok(response) => { - // Parse nested Result - let response_json: serde_json::Value = - serde_json::from_slice(&response.body())?; - - if let Some(data) = response_json.get("Ok") { - // Handle success - } else if let Some(err) = response_json.get("Err") { - return Err(format!("Remote error: {}", err)); - } - }, - Err(e) => return Err(format!("Remote fetch failed: {:?}", e)) - } - } -} -``` - ---- - -## 4. State Management Issues - -### ❌ Error: State not persisting - -**Root Cause:** Wrong save_config or state not serializable - -**Solution:** -```rust -#[hyperprocess( - // ... - save_config = SaveOptions::EveryMessage, // Most reliable - // OR - save_config = SaveOptions::OnInterval(30), // Every 30 seconds -)] - -// Ensure state is serializable -#[derive(Default, Serialize, Deserialize)] -pub struct AppState { - // All fields must be serializable -} -``` - -### ❌ Error: Race conditions in React state - -**Symptom:** Action uses old state value - -**Solution:** -```typescript -// ❌ WRONG - State might not be updated -const handleJoin = async (gameId: string) => { - setSelectedGame(gameId); - await joinGame(); // Uses selectedGame from state - WRONG! -}; - -// βœ… CORRECT - Pass value explicitly -const handleJoin = async (gameId: string) => { - setSelectedGame(gameId); - await joinGame(gameId); // Pass directly -}; - -// βœ… BETTER - Use callback form -const handleUpdate = () => { - setItems(prevItems => { - // Work with prevItems, not items from closure - return [...prevItems, newItem]; - }); -}; -``` - -### ❌ Error: Stale data in UI - -**Root Cause:** Not refreshing after mutations - -**Solution:** -```typescript -// In your store -const createItem = async (data: CreateData) => { - try { - await api.createItem(data); - // βœ… Refresh data after mutation - await get().fetchItems(); - } catch (error) { - // Handle error - } -}; - -// With optimistic updates -const deleteItem = async (id: string) => { - // Optimistic update - set(state => ({ - items: state.items.filter(item => item.id !== id) - })); - - try { - await api.deleteItem(id); - } catch (error) { - // Rollback on error - await get().fetchItems(); - throw error; - } -}; -``` - ---- - -## 5. Manifest & Capability Issues - -### ❌ Error: "failed to open file `pkg/manifest.json`" - -**Full Error:** -``` -ERROR: failed to open file `/path/to/app/pkg/manifest.json` -No such file or directory (os error 2) -``` - -**Root Cause:** manifest.json not generated during build - -**Solutions:** - -1. **Build properly with kit:** -```bash -# This generates manifest.json automatically -kit b --hyperapp -``` - -2. **Check if pkg directory exists:** -```bash -ls -la pkg/ -# Should contain: manifest.json, your-app.wasm, ui/ -``` - -3. **If still missing, check metadata.json:** -```json -// metadata.json must exist and be valid -{ - "package": "hyperapp-skeleton", - "publisher": "template.os" -} -``` - -**See**: [Manifest & Deployment Guide](./08-MANIFEST-AND-DEPLOYMENT.md) for details - -### ❌ Error: "Process does not have capability X" - -**Example:** -``` -Error: Process hyperapp-skeleton:hyperapp-skeleton:user.os does not have capability vfs:distro:sys -``` - -**Root Cause:** Using system feature without requesting capability - -**Solution:** Add to manifest.json: -```json -"request_capabilities": [ - "homepage:homepage:sys", - "http-server:distro:sys", - "vfs:distro:sys" // Add missing capability -] -``` - -**See**: [Capabilities Guide](./09-CAPABILITIES-GUIDE.md) for all capabilities - -### ❌ Error: App doesn't appear on homepage - -**Root Cause:** Missing homepage capability or add_to_homepage call - -**Solution:** -1. Check manifest.json includes: -```json -"request_capabilities": [ - "homepage:homepage:sys" // Required! -] -``` - -2. Check init function calls: -```rust -#[init] -async fn initialize(&mut self) { - add_to_homepage("My App", Some("πŸš€"), Some("/"), None); -} -``` - ---- - -## 6. Development Workflow Issues - -### Clean Build Process -```bash -# When things are really broken -rm -rf target/ -rm -rf ui/node_modules ui/dist -rm -rf pkg/ -rm Cargo.lock - -# Fresh build -kit b --hyperapp -``` - -### Check Generated Files -```bash -# View generated WIT -cat api/*.wit - -# Check built package -ls -la pkg/ - -# Verify UI was built -ls -la pkg/ui/ -``` - -### Test Incrementally -```bash -# 1. Test backend compiles -cd hyperapp-skeleton && cargo check - -# 2. Test UI builds -cd ui && npm run build - -# 3. Full build -cd .. && kit b --hyperapp -``` - ---- - -## 6. Common Patterns That Cause Issues - -### ❌ WebSocket Handler Issues -```rust -// ❌ WRONG - Async WebSocket handler -#[ws] -async fn websocket(&mut self, channel_id: u32, message_type: WsMessageType, blob: LazyLoadBlob) { - // WebSocket handlers must NOT be async! -} - -// βœ… CORRECT - Synchronous handler -#[ws] -fn websocket(&mut self, channel_id: u32, message_type: WsMessageType, blob: LazyLoadBlob) { - match message_type { - WsMessageType::Text => { - // Handle text message - } - WsMessageType::Close => { - // Handle disconnect - } - _ => {} - } -} -``` - -**Common WebSocket Issues:** -1. **Missing endpoint configuration** in hyperprocess macro: -```rust -#[hyperprocess( - endpoints = vec![ - Binding::Ws { - path: "/ws", - config: WsBindingConfig::default().authenticated(false), - }, - ], -)] -``` - -2. **Frontend connection issues:** -```typescript -// ❌ WRONG - Missing authentication -const ws = new WebSocket('ws://localhost:8080/ws'); - -// βœ… CORRECT - Include proper URL -const ws = new WebSocket(`ws://${window.location.host}/${appName}/ws`); -``` - -### ❌ Forgetting async on endpoints -```rust -// ❌ WRONG - Not async -#[http] -fn get_data(&self, _request_body: String) -> String { - // Won't compile -} - -// βœ… CORRECT - Must be async -#[http] -async fn get_data(&self, _request_body: String) -> String { - // Works -} -``` - -### ❌ Wrong imports order -```rust -// ❌ Can cause issues -use serde::{Serialize, Deserialize}; -use hyperprocess_macro::*; - -// βœ… Better order -use hyperprocess_macro::*; -use hyperware_process_lib::{our, Address, ProcessId, Request}; -use serde::{Deserialize, Serialize}; -``` - ---- - -## Debug Checklist - -When nothing works, check: - -1. **Build issues:** - - [ ] All HTTP methods have `_request_body` parameter? - - [ ] No `hyperware_process_lib` in Cargo.toml? - - [ ] All types are WIT-compatible? - - [ ] `#[hyperprocess]` before impl block? - -2. **Runtime issues:** - - [ ] `/our.js` script in HTML head? - - [ ] Node is actually running? - - [ ] Correct ProcessId format? - - [ ] Frontend using tuple format for params? - -3. **P2P issues:** - - [ ] Target node running? - - [ ] Using real node names? - - [ ] `expects_response` timeout set? - - [ ] Method names match exactly? - -4. **State issues:** - - [ ] State is serializable? - - [ ] Refreshing after mutations? - - [ ] Passing values explicitly (not from React state)? - -## 7. Audio/Real-time Data Issues (Voice Apps) - -### ❌ Base64 encoding/decoding issues -```rust -// ❌ WRONG - Manual base64 handling -let decoded = base64::decode(&data)?; - -// βœ… CORRECT - Use proper engine -use base64::{Engine as _, engine::general_purpose}; -let decoded = general_purpose::STANDARD.decode(&data).unwrap_or_default(); -let encoded = general_purpose::STANDARD.encode(&bytes); -``` - -### ❌ Thread safety with audio processing -```rust -// ❌ WRONG - Direct mutation in WebSocket handler -self.audio_buffer.push(audio_data); - -// βœ… CORRECT - Use Arc> for thread-safe access -use std::sync::{Arc, Mutex}; - -// In state -audio_processors: HashMap>>, - -// In handler -if let Ok(mut proc) = processor.lock() { - proc.process_audio(data); -} -``` - -### ❌ WebSocket message sequencing -```rust -// Track sequence numbers for audio streams -#[derive(Serialize, Deserialize)] -struct AudioData { - data: String, - sequence: Option, - timestamp: Option, -} - -// Maintain sequence counters -participant_sequences: HashMap, -``` - -### ❌ Binary data in LazyLoadBlob -```rust -// For binary WebSocket data -let blob = LazyLoadBlob { - mime: Some("application/octet-stream".to_string()), - bytes: audio_bytes, -}; -send_ws_push(channel_id, WsMessageType::Binary, blob); -``` - -## 8. P2P Validation Patterns - -### Common P2P validation errors from samchat: - -**Backend validation:** -```rust -// Empty fields -if recipient_address.trim().is_empty() || message_content.trim().is_empty() { - return Err("Recipient address and message content cannot be empty".to_string()); -} - -// Format validation -if !is_group && !recipient_address.contains('.') { - return Err("Invalid recipient address format (e.g., 'username.os')".to_string()); -} - -// Group constraints -if participants.len() < 2 { - return Err("Group must have at least 2 participants".to_string()); -} -``` - -**Frontend validation:** -```typescript -// In React component -if (!groupName.trim()) { - setError("Please enter a group name"); - return; -} - -// Parse and validate lists -const members = groupMembers.split(',').map(m => m.trim()).filter(m => m); -if (members.length === 0) { - setError("Please enter at least one valid member address"); - return; -} - -// Clear errors on navigation -const handleSelectConversation = useCallback((conversationId: string) => { - fetchMessages(conversationId); - setError(null); - setReplyingTo(null); -}, [fetchMessages]); -``` - -## Still Stuck? - -1. Add logging everywhere: - ```rust - println!("DEBUG: Method called with: {:?}", request_body); - ``` - -2. Check both node consoles for P2P issues - -3. Use browser DevTools: - - Network tab for HTTP/WebSocket - - Console for JavaScript errors - - Application tab for storage - -4. For voice apps: - - Check browser permissions for microphone - - Monitor WebSocket frames in DevTools - - Log audio buffer sizes and timing - -5. Start with minimal example and add complexity - -6. Compare with working examples: - - samchat for P2P chat patterns - - voice for WebSocket/audio patterns \ No newline at end of file diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/03-WIT-TYPES-DATA-MODELING.md b/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/03-WIT-TYPES-DATA-MODELING.md deleted file mode 100644 index 2d3aa391..00000000 --- a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/03-WIT-TYPES-DATA-MODELING.md +++ /dev/null @@ -1,734 +0,0 @@ -# πŸ“Š WIT Types & Data Modeling Guide - -## Understanding WIT (WebAssembly Interface Types) - -WIT is the type system that bridges your Rust code with the frontend. The hyperprocess macro automatically generates WIT files from your Rust types, but it has strict requirements. - -## Type Compatibility Matrix - -| Rust Type | WIT Type | Supported | Notes | -|-----------|----------|-----------|-------| -| `bool` | `bool` | βœ… | | -| `u8`, `u16`, `u32`, `u64` | `u8`, `u16`, `u32`, `u64` | βœ… | | -| `i8`, `i16`, `i32`, `i64` | `s8`, `s16`, `s32`, `s64` | βœ… | | -| `f32`, `f64` | `float32`, `float64` | βœ… | | -| `String` | `string` | βœ… | | -| `Vec` | `list` | βœ… | T must be supported | -| `Option` | `option` | βœ… | T must be supported | -| `(T1, T2, ...)` | `tuple` | βœ… | All T must be supported | -| `HashMap` | - | ❌ | Use `Vec<(K, V)>` | -| `HashSet` | - | ❌ | Use `Vec` | -| `[T; N]` | - | ❌ | Use `Vec` | -| `&str` | - | ❌ | Use `String` | -| `&[T]` | - | ❌ | Use `Vec` | -| Complex enums | - | ⚠️ | Only simple variants | -| Trait objects | - | ❌ | Not supported | - -## Data Modeling Strategies - -### 1. Simple Types - Direct Mapping - -```rust -// βœ… These types map directly to WIT -#[derive(Serialize, Deserialize, PartialEq)] -pub struct User { - pub id: String, - pub name: String, - pub age: u32, - pub active: bool, - pub balance: f64, -} - -#[derive(Serialize, Deserialize, PartialEq)] -pub struct Response { - pub users: Vec, - pub total: u64, - pub page: Option, -} - -// Use in endpoint -#[http] -async fn get_users(&self, _request_body: String) -> Response { - Response { - users: self.users.clone(), - total: self.users.len() as u64, - page: Some(0), - } -} -``` - -### 2. Complex Types - JSON String Pattern - -```rust -// Internal complex type (not exposed via WIT) -#[derive(Serialize, Deserialize)] -struct ComplexGameState { - board: HashMap, - history: Vec, - timers: HashMap, - metadata: serde_json::Value, -} - -// βœ… Return as JSON string -#[http] -async fn get_game_state(&self, _request_body: String) -> String { - serde_json::to_string(&self.game_state).unwrap() -} - -// βœ… Accept as JSON string -#[http] -async fn update_game_state(&mut self, request_body: String) -> Result { - let state: ComplexGameState = serde_json::from_str(&request_body) - .map_err(|e| format!("Invalid game state: {}", e))?; - - self.game_state = state; - Ok("Updated".to_string()) -} -``` - -### 3. Enum Handling - -```rust -// ❌ WRONG - Complex enum variants not supported by WIT directly -pub enum GameEvent { - PlayerJoined { player_id: String, timestamp: u64 }, - MoveMade { from: Position, to: Position }, - GameEnded { winner: Option, reason: EndReason }, -} - -// βœ… PATTERN 1: Simple enum + data struct (WIT-compatible) -#[derive(Serialize, Deserialize, PartialEq)] -pub enum EventType { - PlayerJoined, - MoveMade, - GameEnded, -} - -#[derive(Serialize, Deserialize, PartialEq)] -pub struct GameEvent { - pub event_type: EventType, - pub player_id: Option, - pub from_position: Option, - pub to_position: Option, - pub winner: Option, - pub timestamp: u64, -} - -// βœ… PATTERN 2: Complex enums with mixed variants (JSON-only) -#[derive(Serialize, Deserialize)] -pub enum WsMessage { - // Simple variants work fine - Heartbeat, - Disconnect, - - // Complex variants with nested serde attributes - #[serde(rename_all = "camelCase")] - JoinRoom { - room_id: String, - auth_token: Option, - user_settings: UserSettings, - }, - - // Single data variants - Chat(String), - UpdateStatus(Status), -} - -// βœ… PATTERN 3: Tagged unions via JSON -#[derive(Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum GameEvent { - PlayerJoined { player_id: String, timestamp: u64 }, - MoveMade { from: Position, to: Position }, - GameEnded { winner: Option }, -} - -// Return as JSON string -#[http] -async fn get_events(&self, _request_body: String) -> String { - serde_json::to_string(&self.events).unwrap() -} -``` - -### 4. HashMap Replacement Patterns - -```rust -// ❌ WRONG - HashMap not supported -pub struct GameData { - pub players: HashMap, - pub scores: HashMap, -} - -// βœ… PATTERN 1: Use Vec of tuples -#[derive(Serialize, Deserialize, PartialEq)] -pub struct GameData { - pub players: Vec<(String, Player)>, - pub scores: Vec<(String, u32)>, -} - -// βœ… PATTERN 2: Separate key-value struct -#[derive(Serialize, Deserialize, PartialEq)] -pub struct PlayerEntry { - pub id: String, - pub player: Player, -} - -#[derive(Serialize, Deserialize, PartialEq)] -pub struct ScoreEntry { - pub player_id: String, - pub score: u32, -} - -#[derive(Serialize, Deserialize, PartialEq)] -pub struct GameData { - pub players: Vec, - pub scores: Vec, -} - -// βœ… PATTERN 3: Internal HashMap, external Vec -#[derive(Default, Serialize, Deserialize)] -pub struct AppState { - // Internal representation (not exposed) - players_map: HashMap, -} - -// Exposed via endpoints -#[http] -async fn get_players(&self, _request_body: String) -> Vec { - self.players_map.values().cloned().collect() -} - -#[http] -async fn get_player(&self, request_body: String) -> Result { - let id: String = serde_json::from_str(&request_body)?; - self.players_map.get(&id) - .cloned() - .ok_or_else(|| "Player not found".to_string()) -} -``` - -### 5. Nested Type Visibility - -```rust -// ❌ PROBLEM: WIT generator can't find NestedData -pub struct Response { - pub data: NestedData, -} - -pub struct NestedData { - pub items: Vec, -} - -pub struct Item { - pub id: String, -} - -// βœ… FIX 1: Ensure all types are referenced in endpoints -#[http] -async fn get_response(&self, _request_body: String) -> Response { ... } - -#[http] -async fn get_nested_data(&self, _request_body: String) -> NestedData { ... } - -#[http] -async fn get_item(&self, _request_body: String) -> Item { ... } - -// βœ… FIX 2: Flatten the structure -#[derive(Serialize, Deserialize, PartialEq)] -pub struct Response { - pub items: Vec, - pub metadata: ResponseMetadata, -} -``` - -## Design Patterns for Data Modeling - -### 1. Command Pattern for Complex Operations - -```rust -// Instead of complex parameters, use command objects -#[derive(Deserialize)] -pub struct CreateGameCommand { - pub name: String, - pub max_players: u8, - pub settings: GameSettings, -} - -#[derive(Deserialize)] -pub struct GameSettings { - pub time_limit: Option, - pub allow_spectators: bool, - pub game_mode: String, -} - -// βœ… Modern approach - Direct type deserialization -#[http(method = "POST")] -async fn create_game(&mut self, command: CreateGameCommand) -> Result { - // Process command directly - let game_id = self.create_game_from_command(command)?; - - Ok(GameInfo { - id: game_id, - status: GameStatus::Waiting, - }) -} - -// βœ… Legacy approach - Manual JSON parsing -#[http] -async fn create_game_legacy(&mut self, request_body: String) -> Result { - let command: CreateGameCommand = serde_json::from_str(&request_body)?; - - // Process command - let game_id = self.create_game_from_command(command)?; - - Ok(serde_json::json!({ "game_id": game_id }).to_string()) -} -``` - -### 2. View Pattern for Complex Queries - -```rust -// Internal complex state -struct Game { - id: String, - players: HashMap, - board: BoardState, - history: Vec, - // ... many more fields -} - -// Simplified view for API -#[derive(Serialize, Deserialize, PartialEq)] -pub struct GameView { - pub id: String, - pub player_count: u8, - pub current_turn: String, - pub status: GameStatus, -} - -#[derive(Serialize, Deserialize, PartialEq)] -pub struct GameDetailView { - pub id: String, - pub players: Vec, - pub board_state: String, // Serialized board - pub last_move: Option, -} - -// Expose views, not internal state -#[http] -async fn list_games(&self, _request_body: String) -> Vec { - self.games.values() - .map(|game| game.to_view()) - .collect() -} - -#[http] -async fn get_game_detail(&self, request_body: String) -> Result { - let id: String = serde_json::from_str(&request_body)?; - self.games.get(&id) - .map(|game| game.to_detail_view()) - .ok_or_else(|| "Game not found".to_string()) -} -``` - -### 3. Event Sourcing Pattern - -```rust -// Events as simple data -#[derive(Serialize, Deserialize, PartialEq)] -pub struct Event { - pub id: String, - pub timestamp: String, - pub event_type: String, - pub data: String, // JSON encoded event data -} - -// Store events, rebuild state -#[derive(Default, Serialize, Deserialize)] -pub struct AppState { - events: Vec, - // Cached current state (rebuilt from events) - #[serde(skip)] - current_state: Option, -} - -impl AppState { - fn rebuild_state(&mut self) { - let mut state = ComputedState::default(); - for event in &self.events { - state.apply_event(event); - } - self.current_state = Some(state); - } -} - -#[http] -async fn add_event(&mut self, request_body: String) -> Result { - let event: Event = serde_json::from_str(&request_body)?; - self.events.push(event); - self.rebuild_state(); - Ok("Event added".to_string()) -} -``` - -## Real-World Patterns from P2P Apps - -### Timestamp Handling (from samchat) - -```rust -// ❌ WRONG - chrono types not WIT-compatible -use chrono::{DateTime, Utc}; -pub struct Message { - pub timestamp: DateTime, -} - -// βœ… CORRECT - RFC3339 strings (sorts lexicographically!) -pub struct ChatMessage { - pub timestamp: String, // RFC3339 string for WIT compatibility -} - -// Usage -let current_time_str = Utc::now().to_rfc3339(); - -// Sorting works naturally with RFC3339 strings -conversation.messages.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); -``` - -### Complex Message Types with Optionals - -```rust -// P2P chat pattern: One type handles multiple scenarios -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] -pub struct ChatMessage { - pub id: String, - pub conversation_id: String, - pub sender: String, - pub recipient: Option, // None for group messages - pub recipients: Option>, // Some for group messages - pub content: String, - pub timestamp: String, - pub delivered: bool, - pub file_info: Option, // Optional attachment - pub reply_to: Option, // Optional reply -} - -// This avoids complex enums while supporting: -// - Direct messages (recipient = Some, recipients = None) -// - Group messages (recipient = None, recipients = Some) -// - Messages with/without files -// - Messages with/without replies -``` - -### HashMap in State, Vec in API - -```rust -// Internal state uses HashMap for efficiency -#[derive(Default, Serialize, Deserialize)] -pub struct SamchatState { - conversations: HashMap, - my_node_id: Option, -} - -// But expose as Vec through endpoints -#[http] -async fn get_conversations(&self, _request_body: String) -> Vec { - self.conversations.values() - .map(|conv| ConversationSummary { - id: conv.id.clone(), - participants: conv.participants.clone(), - last_updated: conv.last_updated.clone(), - is_group: conv.is_group, - group_name: conv.group_name.clone(), - }) - .collect() -} -``` - -### Binary Data Transfer - -```rust -// Backend: Vec for file data -#[http] -async fn upload_file(&mut self, file_name: String, mime_type: String, file_data: Vec) -> Result { - // Process binary data -} - -// Frontend TypeScript: number[] maps to Vec -export interface UploadFileRequest { - UploadFile: [string, string, number[]]; // file_name, mime_type, file_data -} -``` - -## TypeScript/JavaScript Compatibility - -### camelCase Serialization - -When your frontend uses TypeScript/JavaScript conventions, use serde's rename attributes: - -```rust -// βœ… Rust snake_case -> TypeScript camelCase -#[derive(Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct UserProfile { - pub user_id: String, // -> userId - pub display_name: String, // -> displayName - pub created_at: u64, // -> createdAt - pub is_active: bool, // -> isActive -} - -// βœ… Works with enums too -#[derive(Serialize, Deserialize)] -pub enum ApiMessage { - #[serde(rename_all = "camelCase")] - UserJoined { - user_id: String, - joined_at: u64, - }, - - #[serde(rename_all = "camelCase")] - MessageSent { - message_id: String, - sender_id: String, - sent_at: u64, - }, -} - -// βœ… Different rename patterns -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] // For C# style -pub struct ConfigData { - pub app_name: String, // -> AppName - pub version: String, // -> Version -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] // For constants -pub struct Constants { - pub max_users: u32, // -> MAX_USERS - pub timeout_ms: u64, // -> TIMEOUT_MS -} -``` - -### Skip Serialization - -For internal fields that shouldn't be exposed: - -```rust -#[derive(Default, Serialize, Deserialize)] -pub struct AppState { - // Public fields - pub users: Vec, - pub settings: Settings, - - // Internal cache - not serialized - #[serde(skip)] - user_cache: HashMap, - - // Skip with default value on deserialize - #[serde(skip_deserializing, default)] - computed_stats: Stats, - - // Custom default function - #[serde(skip, default = "default_processors")] - processors: HashMap, -} - -fn default_processors() -> HashMap { - HashMap::new() -} -``` - -## Best Practices - -### 1. Always Add PartialEq - -```rust -// WIT-exposed types need PartialEq -#[derive(Serialize, Deserialize, PartialEq)] -pub struct MyType { - pub field: String, -} -``` - -### 2. Use Builder Pattern for Complex Types - -```rust -#[derive(Default)] -pub struct GameBuilder { - name: Option, - max_players: Option, - settings: GameSettings, -} - -impl GameBuilder { - pub fn name(mut self, name: String) -> Self { - self.name = Some(name); - self - } - - pub fn max_players(mut self, max: u8) -> Self { - self.max_players = Some(max); - self - } - - pub fn build(self) -> Result { - Ok(Game { - id: uuid::Uuid::new_v4().to_string(), - name: self.name.ok_or("Name required")?, - max_players: self.max_players.unwrap_or(4), - settings: self.settings, - // ... initialize other fields - }) - } -} -``` - -### 3. Version Your Data Models - -```rust -#[derive(Serialize, Deserialize)] -pub struct SaveData { - pub version: u32, - pub data: serde_json::Value, -} - -impl SaveData { - pub fn migrate(self) -> Result { - match self.version { - 1 => migrate_v1_to_v2(self.data), - 2 => Ok(serde_json::from_value(self.data)?), - _ => Err(format!("Unknown version: {}", self.version)), - } - } -} -``` - -### 4. Document Your Types - -```rust -/// Represents a player in the game -#[derive(Serialize, Deserialize, PartialEq)] -pub struct Player { - /// Unique identifier for the player - pub id: String, - - /// Display name chosen by the player - pub name: String, - - /// Current score in the game - pub score: u32, - - /// Whether the player is currently active - pub active: bool, -} -``` - -## Common Patterns Reference - -### Pattern 1: ID-based Lookups -```rust -// Store as HashMap internally, expose as list -pub struct AppState { - items_map: HashMap, -} - -#[http] -async fn get_item(&self, request_body: String) -> Result { - let id: String = serde_json::from_str(&request_body)?; - self.items_map.get(&id).cloned() - .ok_or_else(|| "Not found".to_string()) -} - -#[http] -async fn list_items(&self, _request_body: String) -> Vec { - self.items_map.values().cloned().collect() -} -``` - -### Pattern 2: Pagination -```rust -#[derive(Deserialize)] -pub struct PageRequest { - pub page: usize, - pub per_page: usize, -} - -#[derive(Serialize, PartialEq)] -pub struct PageResponse { - pub items: Vec, - pub total: usize, - pub page: usize, - pub per_page: usize, -} - -#[http] -async fn list_paginated(&self, request_body: String) -> PageResponse { - let req: PageRequest = serde_json::from_str(&request_body) - .unwrap_or(PageRequest { page: 0, per_page: 20 }); - - let start = req.page * req.per_page; - let items: Vec<_> = self.items - .iter() - .skip(start) - .take(req.per_page) - .cloned() - .collect(); - - PageResponse { - items, - total: self.items.len(), - page: req.page, - per_page: req.per_page, - } -} -``` - -### Pattern 3: Result Types -```rust -#[derive(Serialize, Deserialize, PartialEq)] -pub struct ApiResult { - pub success: bool, - pub data: Option, - pub error: Option, -} - -impl ApiResult { - pub fn ok(data: T) -> Self { - Self { - success: true, - data: Some(data), - error: None, - } - } - - pub fn err(error: String) -> Self { - Self { - success: false, - data: None, - error: Some(error), - } - } -} - -#[http] -async fn safe_operation(&mut self, request_body: String) -> ApiResult { - match self.do_operation(request_body) { - Ok(result) => ApiResult::ok(result), - Err(e) => ApiResult::err(e.to_string()), - } -} -``` - -## Remember - -1. **When in doubt, use JSON strings** - They always work -2. **All public fields** - WIT needs to see them -3. **Test incrementally** - Build often to catch type issues early -4. **Keep it simple** - Complex types cause problems -5. **Document patterns** - Future you will thank you - -## See Also - -- [Troubleshooting Guide](./02-TROUBLESHOOTING.md#error-found-types-used-that-are-neither-wit-built-ins-nor-defined-locally) - For WIT type errors -- [Common Patterns](./01-COMMON-PATTERNS.md) - For implementation examples -- [Complete Examples](./07-COMPLETE-EXAMPLES.md) - For real-world usage \ No newline at end of file diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/04-P2P-PATTERNS.md b/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/04-P2P-PATTERNS.md deleted file mode 100644 index 4829206a..00000000 --- a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/04-P2P-PATTERNS.md +++ /dev/null @@ -1,1079 +0,0 @@ -# 🌐 P2P Communication Patterns Guide - -## Core Concepts - -In Hyperware, every user runs their own node. P2P communication allows nodes to: -- Share data directly without central servers -- Coordinate actions across the network -- Build collaborative applications -- Maintain distributed state - -## Endpoint Attributes - -Understanding the different endpoint types: - -- **`#[http]`** - HTTP endpoints accessible via frontend API calls -- **`#[remote]`** - Endpoints callable by other nodes via P2P -- **`#[local]`** - Internal endpoints callable within the same node -- **`#[local] #[remote]`** - Endpoints callable both locally and remotely - -```rust -// HTTP only - frontend calls -#[http] -async fn get_data(&self, _request_body: String) -> Vec { } - -// Remote only - other nodes call this -#[remote] -async fn sync_data(&mut self, data: String) -> Result { } - -// Both local and remote - flexible access -#[local] -#[remote] -async fn process_request(&mut self, req: Request) -> Result { } -``` - -## Essential Components - -### 1. Node Identity -```rust -// Get your own node identity -let my_node = our().node.clone(); // e.g., "alice.os" - -// Node identity comes from the user, not hardcoded -#[http] -async fn connect_to_node(&mut self, request_body: String) -> Result { - let target_node: String = serde_json::from_str(&request_body)?; - // Use target_node for communication -} -``` - -### 2. Process Identity -```rust -// ProcessId format: "process-name:package-name:publisher" -// Note: publisher must match between communicating nodes -let process_id = "myapp:myapp:publisher.os" - .parse::() - .map_err(|e| format!("Invalid ProcessId: {}", e))?; - -// For your app to talk to itself on other nodes -// IMPORTANT: All nodes must use the same publisher! -let my_process_id = format!("{}:{}:{}", - "hyperapp-skeleton", // process name (from metadata.json) - "hyperapp-skeleton", // package name (from metadata.json) - "template.os" // publisher (must be consistent across nodes) -).parse::()?; -``` - -### 3. Address Construction -```rust -// Combine node + process to create full address -let target_address = Address::new( - "bob.os".to_string(), // target node - process_id // target process -); -``` - -### 4. Request Patterns - -Two ways to make P2P requests: - -**Traditional Pattern (hyperware_process_lib::Request):** -```rust -let response = Request::new() - .target(target_address) - .body(serde_json::to_vec(&data).unwrap()) - .expects_response(30) - .send_and_await_response(30)?; -``` - -**Modern Pattern (hyperware_app_common::send):** -```rust -use hyperware_app_common::send; - -// Type-safe request with automatic deserialization -let request = Request::to(&target_address) - .body(serde_json::to_vec(&data).unwrap()); - -match send::>(request).await { - Ok(Ok(response)) => { - // Use response directly - already deserialized - } - Ok(Err(e)) => { - // Remote returned an error - } - Err(e) => { - // Network/communication error - } -} -``` - -## P2P Communication Patterns - -### Pattern 1: Direct Request-Response - -**Use Case:** Query data from another node - -```rust -// On the requesting node -#[http] -async fn get_remote_data(&self, request_body: String) -> Result { - let target_node: String = serde_json::from_str(&request_body)?; - - // Build address - let process_id = "hyperapp-skeleton:hyperapp-skeleton:template.os".parse::()?; - let target = Address::new(target_node, process_id); - - // Create request - let request_data = json!({ - "since": self.last_sync_time, - "limit": 100 - }); - - // Wrap for remote method - let wrapper = json!({ - "GetDataSince": serde_json::to_string(&request_data).unwrap() - }); - - // Send and await response - let response = Request::new() - .target(target) - .body(serde_json::to_vec(&wrapper).unwrap()) - .expects_response(30) // 30 second timeout - .send_and_await_response(30) - .map_err(|e| format!("Remote request failed: {:?}", e))?; - - // Parse response - if let Ok(body) = response.body() { - Ok(String::from_utf8_lossy(&body).to_string()) - } else { - Err("No response body".to_string()) - } -} - -// On the receiving node -#[remote] -async fn get_data_since(&self, request_json: String) -> Result { - #[derive(Deserialize)] - struct DataRequest { - since: String, - limit: usize, - } - - let req: DataRequest = serde_json::from_str(&request_json)?; - - // Get requested data - let data: Vec<_> = self.data.iter() - .filter(|d| d.timestamp > req.since) - .take(req.limit) - .cloned() - .collect(); - - Ok(serde_json::to_string(&data).unwrap()) -} -``` - -### Pattern 2: Fire-and-Forget Notifications - -**Use Case:** Notify other nodes without waiting for response - -```rust -// Broadcast notification to multiple nodes -#[http] -async fn broadcast_event(&mut self, request_body: String) -> Result { - #[derive(Deserialize)] - struct BroadcastRequest { - event_type: String, - data: serde_json::Value, - } - - let req: BroadcastRequest = serde_json::from_str(&request_body)?; - - let notification = json!({ - "event": req.event_type, - "data": req.data, - "from": our().node, - "timestamp": chrono::Utc::now().to_rfc3339(), - }); - - let wrapper = json!({ - "HandleNotification": serde_json::to_string(¬ification).unwrap() - }); - - let mut sent = 0; - let mut failed = 0; - - // Send to all known nodes - for node in &self.connected_nodes { - let process_id = "hyperapp-skeleton:hyperapp-skeleton:template.os".parse::()?; - let target = Address::new(node.clone(), process_id); - - // Fire and forget - still set timeout for reliability - match Request::new() - .target(target) - .body(serde_json::to_vec(&wrapper).unwrap()) - .expects_response(5) // Short timeout - .send() { - Ok(_) => sent += 1, - Err(e) => { - println!("Failed to notify {}: {:?}", node, e); - failed += 1; - } - } - } - - Ok(json!({ - "sent": sent, - "failed": failed - }).to_string()) -} - -// Receiving node -#[remote] -async fn handle_notification(&mut self, notification_json: String) -> Result { - let notification: serde_json::Value = serde_json::from_str(¬ification_json)?; - - // Process notification - self.notifications.push(notification); - - // Just acknowledge receipt - Ok("ACK".to_string()) -} -``` - -### Pattern 3: Distributed State Synchronization - -**Use Case:** Keep state synchronized across multiple nodes - -```rust -// State sync request -#[derive(Serialize, Deserialize)] -pub struct SyncRequest { - pub node_id: String, - pub state_hash: String, - pub last_update: String, -} - -#[derive(Serialize, Deserialize)] -pub struct SyncResponse { - pub updates: Vec, - pub full_sync_needed: bool, -} - -// Periodic sync with peers -impl AppState { - async fn sync_with_peer(&mut self, peer_node: String) -> Result<(), String> { - let process_id = "hyperapp-skeleton:hyperapp-skeleton:template.os".parse::()?; - let target = Address::new(peer_node.clone(), process_id); - - // Send our state info - let sync_req = SyncRequest { - node_id: our().node.clone(), - state_hash: self.calculate_state_hash(), - last_update: self.last_update_time.clone(), - }; - - let wrapper = json!({ - "HandleSyncRequest": serde_json::to_string(&sync_req).unwrap() - }); - - let response = Request::new() - .target(target) - .body(serde_json::to_vec(&wrapper).unwrap()) - .expects_response(30) - .send_and_await_response(30)?; - - if let Ok(body) = response.body() { - let sync_resp: SyncResponse = serde_json::from_slice(&body)?; - - if sync_resp.full_sync_needed { - self.request_full_sync(peer_node).await?; - } else { - self.apply_updates(sync_resp.updates); - } - } - - Ok(()) - } -} - -#[remote] -async fn handle_sync_request(&mut self, request_json: String) -> Result { - let req: SyncRequest = serde_json::from_str(&request_json)?; - - // Check if we have newer data - let response = if req.state_hash != self.calculate_state_hash() { - SyncResponse { - updates: self.get_updates_since(&req.last_update), - full_sync_needed: self.updates_since(&req.last_update) > 100, - } - } else { - SyncResponse { - updates: vec![], - full_sync_needed: false, - } - }; - - Ok(serde_json::to_string(&response).unwrap()) -} -``` - -### Pattern 4: Collaborative Editing - -**Use Case:** Multiple nodes editing shared data - -```rust -// Operation-based CRDT pattern -#[derive(Serialize, Deserialize)] -pub enum Operation { - Insert { pos: usize, text: String, id: String }, - Delete { pos: usize, len: usize, id: String }, - Update { item_id: String, field: String, value: serde_json::Value }, -} - -#[derive(Default, Serialize, Deserialize)] -pub struct SharedDocument { - operations: Vec, - content: String, - version: u64, -} - -// Local edit creates operation -#[http] -async fn edit_document(&mut self, request_body: String) -> Result { - let op: Operation = serde_json::from_str(&request_body)?; - - // Apply locally - self.document.apply_operation(&op); - self.document.version += 1; - - // Broadcast to peers - self.broadcast_operation(op).await?; - - Ok("Applied".to_string()) -} - -// Broadcast operation to all peers -impl AppState { - async fn broadcast_operation(&self, op: Operation) -> Result<(), String> { - let wrapper = json!({ - "ApplyOperation": serde_json::to_string(&op).unwrap() - }); - - let process_id = "hyperapp-skeleton:hyperapp-skeleton:template.os".parse::()?; - - for peer in &self.peers { - let target = Address::new(peer.clone(), process_id); - - // Best effort delivery - let _ = Request::new() - .target(target) - .body(serde_json::to_vec(&wrapper).unwrap()) - .expects_response(5) - .send(); - } - - Ok(()) - } -} - -// Receive operation from peer -#[remote] -async fn apply_operation(&mut self, op_json: String) -> Result { - let op: Operation = serde_json::from_str(&op_json)?; - - // Check if we've already seen this operation - if !self.document.has_operation(&op) { - self.document.apply_operation(&op); - self.document.version += 1; - - // Forward to other peers (gossip protocol) - self.broadcast_operation(op).await?; - } - - Ok("Applied".to_string()) -} -``` - -### Pattern 5: Node Authentication & Handshake - -**Use Case:** Authenticate nodes before allowing access to resources - -```rust -use hyperware_app_common::{send, source}; - -// Authentication request/response types -#[derive(Serialize, Deserialize)] -pub struct NodeHandshakeReq { - pub resource_id: String, -} - -#[derive(Serialize, Deserialize)] -pub struct NodeHandshakeResp { - pub auth_token: String, -} - -// Client initiates handshake -#[http(method = "POST")] -async fn start_handshake(&mut self, url: String) -> Result { - // Extract node from URL (e.g., "https://bob.os/app/resource/123") - let parts: Vec<&str> = url.split('/').collect(); - let host_node = parts.get(2) - .ok_or("Invalid URL format")? - .split(':').next() - .ok_or("No host found")?; - - // Extract resource ID - let resource_id = parts.last() - .ok_or("No resource ID in URL")? - .to_string(); - - // Build target address - let target = Address::new(host_node, ("app", "app", "publisher.os")); - - // Create handshake request - let handshake_req = NodeHandshakeReq { resource_id }; - - // Use typed send from hyperware_app_common - let body = json!({"NodeHandshake": handshake_req}); - let request = Request::to(&target).body(serde_json::to_vec(&body).unwrap()); - - match send::>(request).await { - Ok(Ok(resp)) => { - // Store token and redirect with auth - self.auth_tokens.insert(host_node.to_string(), resp.auth_token.clone()); - Ok(format!("{}?auth={}", url, resp.auth_token)) - } - Ok(Err(e)) => Err(format!("Handshake failed: {}", e)), - Err(e) => Err(format!("Request failed: {:?}", e)), - } -} - -// Server handles handshake - both local and remote calls -#[local] -#[remote] -async fn node_handshake(&mut self, req: NodeHandshakeReq) -> Result { - // Verify resource exists - if !self.resources.contains_key(&req.resource_id) { - return Err("Resource not found".to_string()); - } - - // Get caller identity using source() - let caller_node = source().node; - - // Generate unique auth token - let auth_token = generate_auth_token(); - - // Store token -> node mapping - self.node_auth.insert(auth_token.clone(), NodeAuth { - node_id: caller_node.clone(), - resource_id: req.resource_id, - granted_at: chrono::Utc::now().to_rfc3339(), - }); - - Ok(NodeHandshakeResp { auth_token }) -} - -// Verify token on subsequent requests -fn verify_auth(&self, token: &str) -> Result { - self.node_auth.get(token) - .cloned() - .ok_or_else(|| "Invalid auth token".to_string()) -} -``` - -### Pattern 6: Node Discovery & Presence - -**Use Case:** Find and track active nodes - -```rust -// Heartbeat/presence system -#[derive(Serialize, Deserialize)] -pub struct NodeInfo { - pub node_id: String, - pub app_version: String, - pub capabilities: Vec, - pub last_seen: String, -} - -// Announce presence to known nodes -impl AppState { - async fn announce_presence(&self) -> Result<(), String> { - let my_info = NodeInfo { - node_id: our().node.clone(), - app_version: env!("CARGO_PKG_VERSION").to_string(), - capabilities: vec!["sync".to_string(), "chat".to_string()], - last_seen: chrono::Utc::now().to_rfc3339(), - }; - - let wrapper = json!({ - "RegisterNode": serde_json::to_string(&my_info).unwrap() - }); - - let process_id = "hyperapp-skeleton:hyperapp-skeleton:template.os".parse::()?; - - // Announce to bootstrap nodes - for bootstrap in &self.bootstrap_nodes { - let target = Address::new(bootstrap.clone(), process_id); - - match Request::new() - .target(target) - .body(serde_json::to_vec(&wrapper).unwrap()) - .expects_response(10) - .send_and_await_response(10) { - Ok(response) => { - // Bootstrap node returns list of other nodes - if let Ok(body) = response.body() { - let nodes: Vec = serde_json::from_slice(&body)?; - self.discovered_nodes.extend(nodes); - } - }, - Err(e) => println!("Bootstrap {} unreachable: {:?}", bootstrap, e), - } - } - - Ok(()) - } -} - -#[remote] -async fn register_node(&mut self, info_json: String) -> Result { - let info: NodeInfo = serde_json::from_str(&info_json)?; - - // Update our node registry - self.known_nodes.insert(info.node_id.clone(), info); - - // Return other known nodes - let other_nodes: Vec = self.known_nodes.values() - .filter(|n| n.node_id != info.node_id) - .cloned() - .collect(); - - Ok(serde_json::to_string(&other_nodes).unwrap()) -} -``` - -### Pattern 7: Distributed Transactions - -**Use Case:** Coordinate actions across multiple nodes - -```rust -// Two-phase commit pattern -#[derive(Serialize, Deserialize)] -pub enum TransactionPhase { - Prepare, - Commit, - Abort, -} - -#[derive(Serialize, Deserialize)] -pub struct Transaction { - pub id: String, - pub operation: String, - pub data: serde_json::Value, - pub participants: Vec, -} - -// Coordinator node initiates transaction -#[http] -async fn start_transaction(&mut self, request_body: String) -> Result { - let mut tx: Transaction = serde_json::from_str(&request_body)?; - tx.id = uuid::Uuid::new_v4().to_string(); - - // Phase 1: Prepare - let prepare_wrapper = json!({ - "PrepareTransaction": serde_json::to_string(&tx).unwrap() - }); - - let process_id = "hyperapp-skeleton:hyperapp-skeleton:template.os".parse::()?; - let mut votes = HashMap::new(); - - for participant in &tx.participants { - let target = Address::new(participant.clone(), process_id); - - match Request::new() - .target(target) - .body(serde_json::to_vec(&prepare_wrapper).unwrap()) - .expects_response(10) - .send_and_await_response(10) { - Ok(response) => { - if let Ok(body) = response.body() { - let vote: bool = serde_json::from_slice(&body)?; - votes.insert(participant.clone(), vote); - } - }, - Err(_) => { - votes.insert(participant.clone(), false); - } - } - } - - // Phase 2: Commit or Abort - let all_voted_yes = votes.values().all(|&v| v); - let decision = if all_voted_yes { "Commit" } else { "Abort" }; - - let decision_wrapper = json!({ - decision: tx.id.clone() - }); - - // Notify all participants of decision - for participant in &tx.participants { - let target = Address::new(participant.clone(), process_id); - let _ = Request::new() - .target(target) - .body(serde_json::to_vec(&decision_wrapper).unwrap()) - .expects_response(5) - .send(); - } - - Ok(json!({ - "transaction_id": tx.id, - "decision": decision, - "votes": votes, - }).to_string()) -} - -// Participant node handlers -#[remote] -async fn prepare_transaction(&mut self, tx_json: String) -> Result { - let tx: Transaction = serde_json::from_str(&tx_json)?; - - // Check if we can commit - let can_commit = self.validate_transaction(&tx); - - if can_commit { - // Save to pending - self.pending_transactions.insert(tx.id.clone(), tx); - } - - Ok(can_commit) -} - -#[remote] -async fn commit(&mut self, tx_id: String) -> Result { - if let Some(tx) = self.pending_transactions.remove(&tx_id) { - self.apply_transaction(tx)?; - Ok("Committed".to_string()) - } else { - Err("Transaction not found".to_string()) - } -} - -#[remote] -async fn abort(&mut self, tx_id: String) -> Result { - self.pending_transactions.remove(&tx_id); - Ok("Aborted".to_string()) -} -``` - -## Error Handling & Resilience - -### Retry with Exponential Backoff -```rust -// Note: This pattern requires the "timer:distro:sys" capability! -async fn reliable_remote_call( - target: Address, - method: &str, - data: String, -) -> Result { - let wrapper = json!({ method: data }); - let body = serde_json::to_vec(&wrapper).unwrap(); - - for attempt in 0..3 { - if attempt > 0 { - // Exponential backoff: 100ms, 200ms, 400ms - let delay_ms = 100 * (1 << attempt); - timer::set_timer(delay_ms, None); - } - - match Request::new() - .target(target.clone()) - .body(body.clone()) - .expects_response(30) - .send_and_await_response(30) { - Ok(response) => { - if let Ok(body) = response.body() { - return Ok(String::from_utf8_lossy(&body).to_string()); - } - }, - Err(e) if attempt < 2 => { - println!("Attempt {} failed: {:?}, retrying...", attempt + 1, e); - continue; - }, - Err(e) => return Err(format!("Failed after 3 attempts: {:?}", e)), - } - } - - Err("Max retries exceeded".to_string()) -} -``` - -### Circuit Breaker Pattern -```rust -// Note: HashMap is used here for internal state only - not exposed via WIT -#[derive(Default)] -pub struct CircuitBreaker { - failures: HashMap, - last_failure: HashMap, - threshold: u32, - timeout_secs: u64, -} - -impl CircuitBreaker { - pub fn can_call(&self, node: &str) -> bool { - if let Some(&failures) = self.failures.get(node) { - if failures >= self.threshold { - if let Some(&last) = self.last_failure.get(node) { - return last.elapsed().as_secs() > self.timeout_secs; - } - } - } - true - } - - pub fn record_success(&mut self, node: &str) { - self.failures.remove(node); - self.last_failure.remove(node); - } - - pub fn record_failure(&mut self, node: &str) { - *self.failures.entry(node.to_string()).or_insert(0) += 1; - self.last_failure.insert(node.to_string(), std::time::Instant::now()); - } -} -``` - -## Best Practices - -### 1. Always Set Timeouts -```rust -// βœ… Good -.expects_response(30) -.send_and_await_response(30) - -// ❌ Bad - Can hang forever -.send() -``` - -### 2. Handle Network Partitions -```rust -// Track node availability -pub struct NodeTracker { - nodes: HashMap, -} - -pub struct NodeStatus { - last_successful_contact: String, - consecutive_failures: u32, - is_reachable: bool, -} -``` - -### 3. Use Idempotent Operations -```rust -// Include operation ID to prevent duplicates -#[derive(Serialize, Deserialize)] -pub struct Operation { - pub id: String, // Unique ID - pub action: Action, -} - -impl AppState { - fn apply_operation(&mut self, op: Operation) -> Result<(), String> { - // Check if already applied - if self.applied_operations.contains(&op.id) { - return Ok(()); // Idempotent - } - - // Apply operation - self.execute_action(op.action)?; - self.applied_operations.insert(op.id); - Ok(()) - } -} -``` - -### 4. Design for Eventual Consistency -```rust -// Use vector clocks or timestamps -#[derive(Serialize, Deserialize)] -pub struct VersionedData { - pub data: serde_json::Value, - pub version: VectorClock, - pub last_modified: String, -} - -// Resolve conflicts -impl VersionedData { - fn merge(self, other: Self) -> Self { - if self.version.happens_before(&other.version) { - other - } else if other.version.happens_before(&self.version) { - self - } else { - // Concurrent updates - need resolution strategy - self.resolve_conflict(other) - } - } -} -``` - -## Testing P2P Features - -### Local Testing Setup -```bash -# Terminal 1 -kit s --fake-node alice.os - -# Terminal 2 -kit s --fake-node bob.os - -# Terminal 3 (optional) -kit s --fake-node charlie.os -``` - -### Test Scenarios -1. **Basic connectivity** - Can nodes find each other? -2. **Data sync** - Do all nodes eventually see the same data? -3. **Partition tolerance** - What happens when a node goes offline? -4. **Conflict resolution** - How are concurrent updates handled? -5. **Performance** - How does latency affect the user experience? - -### Debug Output -```rust -// Add comprehensive logging -println!("[P2P] Sending {} to {}", method, target_node); -println!("[P2P] Response: {:?}", response); -println!("[P2P] State after sync: {:?}", self.state); -``` - -## Real-World P2P Patterns from samchat - -### Pattern 8: Group Membership Notifications - -**Use Case:** Notify all members when a group is created or modified - -```rust -// Create group and notify all members -#[http] -async fn create_group(&mut self, group_name: String, initial_members: Vec) -> Result { - let creator = self.my_node_id.clone() - .ok_or_else(|| "Creator node ID not initialized".to_string())?; - - // Ensure creator is included - let mut participants = initial_members; - if !participants.contains(&creator) { - participants.push(creator.clone()); - } - - let group_id = format!("group_{}", Uuid::new_v4()); - - // Create group locally - let conversation = Conversation { - id: group_id.clone(), - participants: participants.clone(), - is_group: true, - group_name: Some(group_name.clone()), - created_by: Some(creator.clone()), - // ... - }; - self.conversations.insert(group_id.clone(), conversation); - - // Notify all other members - let publisher = "hpn-testing-beta.os"; // Consistent across all nodes! - let target_process_id = format!("samchat:samchat:{}", publisher) - .parse::()?; - - for participant in &participants { - if participant != &creator { - let target_address = Address::new(participant.clone(), target_process_id.clone()); - let notification = GroupJoinNotification { - group_id: group_id.clone(), - group_name: group_name.clone(), - participants: participants.clone(), - created_by: creator.clone(), - }; - - let request_wrapper = json!({ "HandleGroupJoin": notification }); - - // Fire-and-forget but still set expects_response for reliability - let _ = Request::new() - .target(target_address) - .body(serde_json::to_vec(&request_wrapper).unwrap()) - .expects_response(30) - .send(); - } - } - - Ok(group_id) -} - -// Handle notification on receiving nodes -#[remote] -async fn handle_group_join(&mut self, notification: GroupJoinNotification) -> Result { - // Create the group conversation locally - let conversation = Conversation { - id: notification.group_id.clone(), - participants: notification.participants, - is_group: true, - group_name: Some(notification.group_name), - created_by: Some(notification.created_by), - // ... - }; - - self.conversations.insert(notification.group_id, conversation); - Ok(true) -} -``` - -### Pattern 9: Remote Data Retrieval with Local Caching - -**Use Case:** Fetch files or data from remote nodes with fallback - -```rust -// Try local first, then remote -#[http] -async fn download_file(&mut self, file_id: String, sender_node: String) -> Result, String> { - // Try local VFS first - let file_path = format!("/samchat:hpn-testing-beta.os/files/{}", file_id); - let vfs_address = Address::new(our().node.clone(), "vfs:distro:sys".parse::()?); - - let local_result = Request::new() - .target(vfs_address.clone()) - .body(json!({ "path": file_path, "action": "Read" })) - .expects_response(5) - .send_and_await_response(5); - - if let Ok(response) = local_result { - if let Some(blob) = response.blob() { - return Ok(blob.bytes); - } - } - - // Not found locally, fetch from remote - if sender_node != our().node { - let target = Address::new(sender_node, - "samchat:samchat:hpn-testing-beta.os".parse::()?); - - let remote_result = Request::new() - .target(target) - .body(json!({ "GetRemoteFile": file_id })) - .expects_response(30) - .send_and_await_response(30)?; - - // Parse nested Result from remote - let response_json: serde_json::Value = - serde_json::from_slice(&remote_result.body())?; - - if let Some(file_data) = response_json.get("Ok") { - let bytes: Vec = serde_json::from_value(file_data.clone())?; - - // Cache locally for future use - let _ = Request::new() - .target(vfs_address) - .body(json!({ "path": file_path, "action": "Write" })) - .blob(LazyLoadBlob::new(Some("file"), bytes.clone())) - .expects_response(5) - .send_and_await_response(5); - - return Ok(bytes); - } - } - - Err("File not found".to_string()) -} -``` - -### Pattern 10: Message Distribution to Multiple Recipients - -**Use Case:** Send messages to group members without blocking - -```rust -// Distribute message to all group members -async fn send_group_message(&mut self, group_id: String, content: String) -> Result { - let sender = self.my_node_id.clone() - .ok_or_else(|| "Node ID not initialized".to_string())?; - - let conversation = self.conversations.get(&group_id) - .ok_or_else(|| "Group not found".to_string())?; - - // Get all recipients except sender - let recipients: Vec = conversation.participants.iter() - .filter(|p| *p != &sender) - .cloned() - .collect(); - - let message = ChatMessage { - id: Uuid::new_v4().to_string(), - conversation_id: group_id, - sender, - recipients: Some(recipients.clone()), - content, - timestamp: Utc::now().to_rfc3339(), - // ... - }; - - // Save locally first - self.conversations.get_mut(&group_id).unwrap() - .messages.push(message.clone()); - - // Distribute to all recipients - let target_process_id = "samchat:samchat:hpn-testing-beta.os" - .parse::()?; - - for recipient in recipients { - let target = Address::new(recipient, target_process_id.clone()); - - // Fire-and-forget pattern but WITH expects_response - let _ = Request::new() - .target(target) - .body(json!({ "ReceiveMessage": message.clone() })) - .expects_response(30) // Still set timeout! - .send(); // Don't await response - } - - Ok(true) -} -``` - -### Key Patterns from samchat: - -1. **Consistent Publisher**: Always use the same publisher across all nodes - ```rust - let publisher = "hpn-testing-beta.os"; // Same for ALL nodes! - let process_id = format!("samchat:samchat:{}", publisher); - ``` - -2. **Fire-and-Forget WITH Timeout**: Even when not awaiting responses, set expects_response - ```rust - Request::new() - .expects_response(30) // Important for reliability - .send(); // Not awaiting - ``` - -3. **Node ID in State**: Store your node ID at initialization - ```rust - #[init] - async fn initialize(&mut self) { - self.my_node_id = Some(our().node.clone()); - } - ``` - -4. **Optional Fields for Flexibility**: Use Option for fields that vary by message type - ```rust - pub struct ChatMessage { - recipient: Option, // Direct messages - recipients: Option>, // Group messages - file_info: Option, // File attachments - reply_to: Option, // Replies - } - ``` - -## Remember - -1. **No central authority** - Design for peer equality -2. **Expect failures** - Networks are unreliable -3. **Plan for conflicts** - Concurrent updates will happen -4. **Test with multiple nodes** - Single node testing misses P2P issues -5. **Document protocols** - Other developers need to understand your P2P design -6. **Consistent naming** - Use the same publisher/process names across all nodes -7. **Always set timeouts** - Even for fire-and-forget patterns \ No newline at end of file diff --git a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/05-UI-FRONTEND-GUIDE.md b/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/05-UI-FRONTEND-GUIDE.md deleted file mode 100644 index c78443ae..00000000 --- a/src/new/templates/rust/ui/hyperapp-skeleton/resources/guides/05-UI-FRONTEND-GUIDE.md +++ /dev/null @@ -1,1553 +0,0 @@ -# πŸ’» UI/Frontend Development Guide - -## Frontend Stack Overview - -- **React 18** - UI framework -- **TypeScript** - Type safety -- **Zustand** - State management -- **Vite** - Build tool -- **CSS Modules** or plain CSS - Styling - -## Critical Setup Requirements - -### 1. The `/our.js` Script (MANDATORY) - -```html - - - - - - - - - - My Hyperware App - - -
- - - -``` - -### 2. Global Types Setup - -```typescript -// src/types/global.ts -declare global { - interface Window { - our?: { - node: string; // e.g., "alice.os" - process: string; // e.g., "myapp:myapp:publisher.os" - }; - } -} - -export const BASE_URL = ''; // Empty in production - -export const isHyperwareEnvironment = (): boolean => { - return typeof window !== 'undefined' && window.our !== undefined; -}; - -export const getNodeId = (): string | null => { - return window.our?.node || null; -}; -``` - -## API Communication Patterns - -### 1. Basic API Service - -```typescript -// src/utils/api.ts -import { BASE_URL } from '../types/global'; - -// IMPORTANT: Backend HTTP methods return String or Result -// Complex data is serialized as JSON strings that must be parsed on frontend - -// Generic API call function -export async function makeApiCall( - method: string, - data?: TRequest -): Promise { - const body = data !== undefined - ? { [method]: data } - : { [method]: "" }; // Empty string for no params - - const response = await fetch(`${BASE_URL}/api`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`API Error: ${response.status} - ${error}`); - } - - return response.json(); -} - -// Typed API methods -export const api = { - // No parameters - backend returns JSON string - async getStatus() { - const response = await makeApiCall('GetStatus', ""); - return JSON.parse(response) as StatusResponse; - }, - - // Single parameter - backend returns JSON string - async getItem(id: string) { - const response = await makeApiCall('GetItem', id); - return JSON.parse(response) as Item; - }, - - // Multiple parameters (as JSON object - common pattern) - async createItem(name: string, description: string) { - const response = await makeApiCall( - 'CreateItem', - JSON.stringify({ name, description }) - ); - return JSON.parse(response) as CreateResponse; - }, - - // Complex object (send as JSON string) - async updateSettings(settings: Settings) { - return makeApiCall( - 'UpdateSettings', - JSON.stringify(settings) - ); - }, -}; -``` - -### 2. Error Handling - -```typescript -// src/utils/errors.ts -export class ApiError extends Error { - constructor( - message: string, - public status?: number, - public details?: unknown - ) { - super(message); - this.name = 'ApiError'; - } -} - -export function getErrorMessage(error: unknown): string { - if (error instanceof ApiError) { - return error.message; - } - if (error instanceof Error) { - return error.message; - } - if (typeof error === 'string') { - return error; - } - return 'An unknown error occurred'; -} - -// Wrapper with error handling -export async function apiCallWithRetry( - apiCall: () => Promise, - maxRetries = 3 -): Promise { - let lastError: unknown; - - for (let i = 0; i < maxRetries; i++) { - try { - return await apiCall(); - } catch (error) { - lastError = error; - if (i < maxRetries - 1) { - // Exponential backoff - await new Promise(resolve => - setTimeout(resolve, Math.pow(2, i) * 1000) - ); - } - } - } - - throw lastError; -} -``` - -## State Management with Zustand - -### 1. Store Structure - -```typescript -// src/store/app.ts -import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; - -interface AppState { - // Connection - nodeId: string | null; - isConnected: boolean; - - // Data - items: Item[]; - currentItem: Item | null; - - // UI State - isLoading: boolean; - error: string | null; - - // Filters/Settings - filters: { - search: string; - category: string | null; - sortBy: 'name' | 'date' | 'priority'; - }; -} - -interface AppActions { - // Connection - initialize: () => void; - - // Data operations - fetchItems: () => Promise; - createItem: (data: CreateItemData) => Promise; - updateItem: (id: string, updates: Partial) => Promise; - deleteItem: (id: string) => Promise; - selectItem: (id: string | null) => void; - - // UI operations - setError: (error: string | null) => void; - clearError: () => void; - setFilter: (filter: Partial) => void; - - // P2P operations - syncWithNode: (nodeId: string) => Promise; -} - -export const useAppStore = create()( - devtools( - persist( - immer((set, get) => ({ - // Initial state - nodeId: null, - isConnected: false, - items: [], - currentItem: null, - isLoading: false, - error: null, - filters: { - search: '', - category: null, - sortBy: 'name', - }, - - // Actions - initialize: () => { - const nodeId = getNodeId(); - set(state => { - state.nodeId = nodeId; - state.isConnected = nodeId !== null; - }); - - if (nodeId) { - get().fetchItems(); - } - }, - - fetchItems: async () => { - set(state => { - state.isLoading = true; - state.error = null; - }); - - try { - const items = await api.getItems(); - set(state => { - state.items = items; - state.isLoading = false; - }); - } catch (error) { - set(state => { - state.error = getErrorMessage(error); - state.isLoading = false; - }); - } - }, - - createItem: async (data) => { - set(state => { state.isLoading = true; }); - - try { - const response = await api.createItem(data); - - // Optimistic update - const newItem: Item = { - id: response.id, - ...data, - createdAt: new Date().toISOString(), - }; - - set(state => { - state.items.push(newItem); - state.currentItem = newItem; - state.isLoading = false; - }); - - // Refresh to ensure consistency - await get().fetchItems(); - } catch (error) { - set(state => { - state.error = getErrorMessage(error); - state.isLoading = false; - }); - throw error; // Re-throw for form handling - } - }, - - // ... other actions - })), - { - name: 'app-storage', - partialize: (state) => ({ - // Only persist UI preferences, not data - filters: state.filters, - }), - } - ) - ) -); - -// Selector hooks -export const useItems = () => { - const { items, filters } = useAppStore(); - - return items.filter(item => { - if (filters.search && !item.name.toLowerCase().includes(filters.search.toLowerCase())) { - return false; - } - if (filters.category && item.category !== filters.category) { - return false; - } - return true; - }).sort((a, b) => { - switch (filters.sortBy) { - case 'name': - return a.name.localeCompare(b.name); - case 'date': - return b.createdAt.localeCompare(a.createdAt); - case 'priority': - return b.priority - a.priority; - } - }); -}; - -export const useCurrentItem = () => useAppStore(state => state.currentItem); -export const useIsLoading = () => useAppStore(state => state.isLoading); -export const useError = () => useAppStore(state => state.error); -``` - -### 2. React Components - -```typescript -// src/components/ItemList.tsx -import React, { useEffect } from 'react'; -import { useAppStore, useItems } from '../store/app'; -import { ErrorMessage } from './ErrorMessage'; -import { LoadingSpinner } from './LoadingSpinner'; - -export const ItemList: React.FC = () => { - const items = useItems(); - const { isLoading, error, selectItem, currentItem } = useAppStore(); - - if (error) return ; - if (isLoading && items.length === 0) return ; - - return ( -
- {items.map(item => ( -
selectItem(item.id)} - > -

{item.name}

-

{item.description}

- - {new Date(item.createdAt).toLocaleDateString()} - -
- ))} - - {items.length === 0 && ( -
-

No items found

- -
- )} -
- ); -}; -``` - -### 3. Forms with Validation - -```typescript -// src/components/CreateItemForm.tsx -import React, { useState } from 'react'; -import { useAppStore } from '../store/app'; - -interface FormData { - name: string; - description: string; - category: string; -} - -interface FormErrors { - name?: string; - description?: string; - category?: string; -} - -export const CreateItemForm: React.FC<{ onClose: () => void }> = ({ onClose }) => { - const { createItem, isLoading } = useAppStore(); - const [formData, setFormData] = useState({ - name: '', - description: '', - category: '', - }); - const [errors, setErrors] = useState({}); - const [submitError, setSubmitError] = useState(null); - - const validate = (): boolean => { - const newErrors: FormErrors = {}; - - if (!formData.name.trim()) { - newErrors.name = 'Name is required'; - } else if (formData.name.length < 3) { - newErrors.name = 'Name must be at least 3 characters'; - } - - if (!formData.description.trim()) { - newErrors.description = 'Description is required'; - } - - if (!formData.category) { - newErrors.category = 'Please select a category'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!validate()) return; - - setSubmitError(null); - - try { - await createItem(formData); - onClose(); - } catch (error) { - setSubmitError(getErrorMessage(error)); - } - }; - - const handleChange = (field: keyof FormData) => ( - e: React.ChangeEvent - ) => { - setFormData(prev => ({ ...prev, [field]: e.target.value })); - // Clear error when user types - if (errors[field]) { - setErrors(prev => ({ ...prev, [field]: undefined })); - } - }; - - return ( -
-

Create New Item

- - {submitError && ( -
{submitError}
- )} - -
- - - {errors.name && {errors.name}} -
- -
- -