((resolve, reject) => {
+ vite.middlewares(req.raw, reply.raw, (err: any) => {
+ if (err) reject(err);
+ else resolve();
+ });
+ });
+ });
+ } else {
+ /**
+ * ---------------------------------------------------------------
+ * PRODUCTION MODE
+ * ---------------------------------------------------------------
+ * Serves prebuilt static files from /dist.
+ * Plugins still apply since Fastify serves everything.
+ * ---------------------------------------------------------------
+ */
+ await app.register(fastifyStatic, {
+ root: distDir,
+ prefix: "/",
+ index: ["index.html"],
+ wildcard: false,
+ });
+
+ // Frontend fallback route (after /api)
+ app.get("/*", async (req, reply) => {
+ if (req.url.startsWith("/api")) return reply.callNotFound();
+ const html = fs.readFileSync(path.join(distDir, "index.html"), "utf-8");
+ reply.type("text/html").send(html);
+ });
+ }
+
+ /**
+ * ---------------------------------------------------------------
+ * SERVER STARTUP
+ * ---------------------------------------------------------------
+ * Automatically finds an available port and starts listening.
+ * ---------------------------------------------------------------
+ */
+ const port = await getPort({ port: [3000, 3001, 3002, 4000] });
+
+ app.listen({ port, host: "0.0.0.0" }, (err, address) => {
+ if (err) throw err;
+ console.log(`Rivra ${isProd ? "Production" : "Development"} running on ${address}`);
+ });
+
+ return app;
+}
+
+export default StartServer;
diff --git a/ripple-env.d.ts b/ripple-env.d.ts
index 2793237..842324e 100644
--- a/ripple-env.d.ts
+++ b/ripple-env.d.ts
@@ -1,8 +1,8 @@
-
-declare module "*.ripple" {
- import type { Component } from "ripple";
-
- // mimic a JSX-like component type with prop inference
- const component: >(props: P) => ReturnType;
- export default component;
-}
+
+declare module "*.ripple" {
+ import type { Component } from "ripple";
+
+ // mimic a JSX-like component type with prop inference
+ const component: >(props: P) => ReturnType;
+ export default component;
+}
diff --git a/src/App.ripple b/src/App.ripple
index 939f92e..9fababd 100644
--- a/src/App.ripple
+++ b/src/App.ripple
@@ -1,7 +1,7 @@
-import { track } from 'ripple';
-import {PageRoutes} from "ripple-tools-full"
-import { modules } from "./routes";
-
-export component App() {
-
-}
+import { track } from 'ripple';
+import {PageRoutes} from "ripple-tools-full"
+import { modules } from "./routes";
+
+export component App() {
+
+}
diff --git a/src/App2.ripple b/src/App2.ripple
index 9da8875..7a59256 100644
--- a/src/App2.ripple
+++ b/src/App2.ripple
@@ -1,9 +1,9 @@
-// Copy this app's content to your app's App.ripple file
-
-import { track } from 'ripple';
-import {PageRoutes} from "ripple-tools-full"
-import { modules } from "./routes";
-
-export component App() {
-
-}
+// Copy this app's content to your app's App.ripple file
+
+import { track } from 'ripple';
+import {PageRoutes} from "ripple-tools-full"
+import { modules } from "./routes";
+
+export component App() {
+
+}
diff --git a/src/ReadMe.md b/src/ReadMe.md
index f7d6961..b0d95da 100644
--- a/src/ReadMe.md
+++ b/src/ReadMe.md
@@ -1,718 +1,718 @@
-
-
-
-
-
-# Rivra
-**(Minimal Ripple tool kit. Not just a router)**
-
-
-
-# Router + full-stack description:
-Not just a router — Rivra is a complete toolkit combining routing, state management, storage, and full-stack app tooling, all fully accessible
-
-##
-
-
-
-
-
------
-
-> **Update:**
- Rivra middleware now applies to the **client side** as well — not just API routes.
-> Global Middleware and Plugins are now executed **across all routes and resources**, ensuring unified behavior.
-> _(Page-specific middleware or plugin logic isn’t supported yet.)_
-
----
-
-
-##
-
-## Creating New Project
-
-```bash
-npx rivra create my-app // wrapper around npm create ripple my-app.
-
-```
-
-###### For existing ripple projects use the installation and init
-
-## Installation
-
-```bash
-npm install rivra
-npx rivra init
-```
-
-or
-
-```bash
-yarn add rivra
-npx rivra init
-```
-
-
-
-## Quick Start
-
-After initiating **Rivra** which has all the rivra routing components, the pages directory, /api (if selected full stack), the routes.ts file for your app modules and the configured App.ripple file will be visible in your project src dir. The App.ripple is optional to overwrite.
-
-### Directory Structure
-
-Here's an example `src/pages/` directory:
-
-```bash
-pages/
-├── index.ripple # Home page
-├── about.ripple # About page
-└── users/
- └── [id]/
- └── user/
- └── [username]/
- └── comments/
-```
-
-Here's an `api/` directory:
-
-```bash
-api/
-├── index.ts # Root API entry point (can load all modules)
-├── posts.ts # Top-level posts routes
-└── users/
- └── [id]/ # Dynamic user ID
- └── user/
- └── [username]/ # Dynamic username
- └── comments/
- └── index.ts # User comments routes
-
-```
-
-```ts
-import type { Reply, Req, App } from "rivra"
-
-import type {Req, Reply, App} from "mivra"
-export default async function postAPi(app: App) {
- app.get("/", async (req: Req, reply: Reply) => {
-
- const param = req.params
- const queries = req.query
-
-
- return ({ message: "some dynamic route", params: param, query: queries })
- })
-}
-
-```
-
-
-### Rivra middlewares/plugins for fastify
-
-Rivra allows you to customize the server behaviours by leveraging on Fastify hooks and plugins. There are some times you may want to extend function.
-
-----
-```sql
-
-plugins/
- ├── middleware/
- │ ├── cors.ts → global middleware (order=1)
- │ └── helmet.ts → global middleware (order=2)
- ├── auth.md.ts → middleware only for /auth/*
- ├── auth.pg.ts → /auth routes
- ├── users/
- │ ├── users.md.ts → middleware only for /users/*
- │ └── index.ts → /users routes
- └── logger.ts → global plugin
-```
-
-
-* Example of plugin
-```ts
-export const order = 2; // order from 1-10 gives you ability to prioritize the order which your hooks run.
-
-export default async function (req: Req, reply: Reply, app: App) {
- console.log("global plugin triggered")
- reply.header('X-Powered-By', 'Rivra');
-
- if (req.url === "/api/users") {
- reply.send({ message: "Users root route" });
- } else if (req.url === "/api/users/profile") {
- reply.send({ message: "User profile route" });
- }
-
-}
-
-
-```
-
-* Example of middlewre
-```ts
-
-export default function (req: Req, res: Reply,) {
- console.log("Protected middleware Incoming:", req.method, req.url);
- const truthy = true
- if (!truthy) {
- res.code(400).send({error: "Bad request"})
- return
- }
- if (req.url === "/api/blocked") {
- res.code(403).send({ error: "Forbidden" });
- return;
- }
-
-}
-
-
-```
-
-#### Here’s a simple and clear table that explains the difference between a plugin and a middleware and how Rivra handle them.
-
-| **Aspect** | **Plugin** | **Middleware** |
-| ------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
-| **Purpose** | Extends or configures the Fastify instance (adds routes, hooks, decorators, schemas, etc.) | Intercepts and processes requests before or during route handling (authentication, logging, validation, etc.) |
-| **Function Signature** | `(app: FastifyInstance) => void` | `(req: FastifyRequest, reply: FastifyReply, app: FastifyInstance) => void` |
-| **Execution Time** | Runs **once** during server startup to register logic | Runs **on every incoming request** (depending on where it’s applied) |
-| **Common Use Cases** | Adding routes, setting up databases, registering third-party plugins, global configurations | Authentication checks, access control, logging, pre-processing requests` | Using hooks like `app.addHook("preHandler", handler)` or route-level middlewares |
-| **Scope** | Can be **scoped** to a prefix (e.g., `/api/protected`) | Can be **global** or **route-specific** |
-| **Impact** | Modifies or enhances the app’s capabilities | Filters or modifies the request/response cycle |
-| **File Naming Convention (in your system)** | `*.pg.ts` (Scoped Plugin) | `*.md.ts` (Middleware) |
-| **Example (Plugin)** | `export default async function (app) { app.get('/hello', () => 'Hi');}` | |
-| **Example (Middleware)** | | `t export default async function (req, reply, app) { if (!req.headers.auth) reply.code(401).send({ error: 'Unauthorized' });}` |
-
-
-| Type | Example File | Prefix Applied | Loaded As | Order |
-| ----------------- | ------------------------------- | -------------- | ---------- | -------------------------- |
-| Global middleware | `/plugins/middleware/logger.ts` | none | middleware | before all plugins |
-| Route middleware | `/plugins/auth/auth.md.ts` | `/auth` | middleware | after global, before route |
-| Prefixed plugin | `/plugins/auth.pg.ts` | `/auth` | plugin | normal |
-| Folder plugin | `/plugins/payments/index.ts` | `/payments` | plugin | normal |
-| Global plugin | `/plugins/cors.ts` | none | plugin | normal |
-
-
-Dynamic segments use `[param]` notation like `[id]` or `[username]`.
-
----
-
-### App Component
-
-```ts
-import {PageRoutes} from "rivra/router"
-import { modules } from "./routes";
-
-export component App() {
-
-}
-```
-
-That's it! Your routing is now set up. `PageRoutes` automatically reads your `pages/` folder and matches routes, including dynamic parameters.
-
----
-
-### Link Component
-
-Use the `Link` component for navigation:
-
-```ts
-import Link from "rivra/router"
-
-export component Navigation() {
-
-}
-```
-
-#### Props:
-
-| Prop | Type | Default | Description |
-| ------------------ | --------------------- | ------- | --------------------------------------------- |
-| `href` | `string` | — | Path to navigate to |
-| `children` | `Component` | — | Content to render inside link |
-| `onLoading` | `() => void` | — | Callback when navigation starts |
-| `emitEvent` | `boolean` | `true` | Whether to trigger route events for this link |
-| `loadingComponent` | `Component` | — | Optional component to show while loading |
-| `className` | `string` | — | Additional CSS class names for styling |
-| `queries` | `Record` | — | Optional query parameters for URLSearch |
-
-
----
-
-### Router And Events
-
-You can subscribe to router events if you need custom behavior:
-
-```ts
-import { useRouter } from "rivra/router"
-
-const router = useRouter();
-
-router.on("start", path => console.log("Navigating to:", path));
-router.on("complete", path => console.log("Navigation finished:", path));
-router.on("change", path => console.log("Route changed:", path));
-
-
-
-//Guard back navigation
-router.beforePopState((url) => {
- if (url === "/protected") {
- console.log("Navigation blocked:", url);
- return false; // Cancel navigation
- }
-});
-
-// Navigate to a new route
-router.push("/users/42?tab=posts");
-router.push("/users/42?tab=posts", true, false, {name: "John", age: 20}); // path, emitEvent, shallow (partial url change), queries
-
-
-//Replace URL shallowly (no full sync)//
-router.replace("/users/42?tab=profile", true, true);
-
-// Prefetch a route
-router.prefetch("/about");
-
-// Resolve href
-console.log("Resolved href:", router.resolveHref("/contact?ref=home"));
-
-// Access reactive properties
-console.log("Current path:", router.path);
-console.log("Query params:", router.queries);
-console.log("Dynamic params:", router.params);
-console.log("Full URL:", router.asPath);
-
-// Access full URL info
-console.log(router.host);
-console.log(router.hostname);
-console.log(router.origin);
-console.log(router.protocol);
-console.log(router.port);
-console.log(router.href);
-console.log(router.search);
-console.log(router.hash);
-```
-
-* `start`: triggered before navigation begins
-* `complete`: triggered after navigation finishes
-* `change`: triggered on every path change
-
-You can opt out of events per `Link` with `emitEvent={false}`.
-
----
-
-### Dynamic Route Params
-
-Access route params and queries in any component:
-
-```ts
-import { useRouter } from "rivra/router"
-
-export component UserProfile() {
- const router = useRouter();
-
- const id = router.params.id; // dynamic param
- const username = router.params.username;
- const queryName = router.queries.name; // URL query ?name=...
- // or
- const {params, queries} = router;
-
-
- {"User ID: " + id}
- {"Username: " + username}
- {"Query name: " + queryName}
-
-}
-```
-
----
-
-### Global Loading Indicator (Optional)
-you can disable it with props ```ts
-
-```
-
-```ts
-import {PageRoutes} from "rivra/router"
-import { modules } from "./routes";
-
-export component App() {
-
-}
-
-```
-
-
----
-
----
-
-### A minimal reactive store that manages shared state across your app with an intuitive API. It provides reactivity, persistence, and derived state — all in under a few lines of code.
-
-| Feature | Description |
-| ------------------------------ | ---------------------------------------------------------------------------------------- |
-| **`get()`** | Returns the current store value. |
-| **`set(next)`** | Replaces the entire store value. |
-| **`update(partialOrFn)`** | Merges new data into the store. Supports both object patching and callback styles. |
-| **`subscribe(fn, selector?)`** | Reactively listens for state changes, optionally to a selected portion. |
-| **`derive(selector)`** | Creates a new store derived from a specific part of another store (like computed state). |
-| **`delete(keys)`** | Removes one or more keys from the store. |
-| **`clear()`** | Resets store to initial state and removes persisted data. |
-| **`persist` (option)** | Automatically saves and restores state from `localStorage`. |
-
-
-
--------------------- Example Stores --------------------
-```ts
- //* Route store for storing current route path
- //* Persisted in localStorage as "routeStore"
- //*/
-export const routeStore = createStore(
- { path: "/" },
- { persist: true, storageKey: "routeStore" }
-);
-
-/**
- * App store for global state
- * Tracks path, user info, theme
- * Persisted in localStorage as "appStore"
- */
-export const appStore = createStore(
- { path: "/", user: null as null | { name: string }, theme: "light" },
- { persist: true, storageKey: "appStore" }
-);
-
-```
-Here are extra two simple Hello World store examples for getting started and explain things better.
-
-### Store without persist (default)
-```ts
-import { createStore } from "rivra/store"
-
-// Create a simple store
-const helloStore = createStore({ message: "Hello World!" });
-
-// Subscribe to changes
-helloStore.subscribe(state => {
- console.log("Current message:", state.message);
-});
-
-// Get changes anywhere
-const data = helloStore.get();
-console.log(helloStore) // { message: Current message}
-console.log(data.message) // Current message
-
-
-// Update the store
-helloStore.update({ message: "Hello Ripple!" });
-
-// Output:
-// Current message: Hello World!
-// Current message: Hello Ripple!
-
-
-
-```
-
-### Store with persist
-
-```ts
-import { createStore } from "rivra/store"
-import { track } from "ripple"
-
-const message = track("")
-
-// Create a persisted store
-const persistentHelloStore = createStore(
- { message: "Hello Persistent World!" },
- { persist: true, storageKey: "helloStore" }
-);
-
-// Subscribe to changes
-persistentHelloStore.subscribe(state => {
- console.log("Current message:", state.message);
-});
-
-
-// Get changes anywhere
-const data = helloStore.get();
-console.log(helloStore) // { message: Current message}
-console.log(data.message) // Current message
-
-
-// Update the store
-persistentHelloStore.update({ message: "Updated and Persisted!" });
-
-
-// Callback update (safe addition)
-persistentHelloStore.update(prev => ({ message: prev.message + " " + @message }));
-
-
-// Reload the page and subscribe again
-persistentHelloStore.subscribe(state => {
- console.log("After reload:", state.message);
-});
-
-// Output (after reload):
-// After reload: Updated and Persisted!
-
-
-
-export const appStore = createStore(
- {
- user: { name: "Joe", location: "unknown", preferences: [] },
- count: 0,
- theme: "light",
- },
- { persist: true, storageKey: "appStore" }
-);
-
-
-
-// Subscribe to entire state
-appStore.subscribe(s => console.log("State changed:", s));
-
-// Watch a specific value
-appStore.watch(s => s.count, (n, o) => console.log(`Count: ${o} → ${n}`));
-
-// Use middleware for logging
-appStore.use((state, action, payload) =>
- console.log(`[${action}]`, payload, state)
-);
-
-// Partial update
-appStore.update({ count: 1 });
-
-// Callback update (safe addition)
-appStore.update(prev => ({ count: prev.count + 1 }));
-
-// Derived store
-const themeStore = appStore.derive(s => s.theme);
-themeStore.subscribe(theme => console.log("Theme:", theme));
-
-// Clear store
-appStore.clear();
-
-
-```
-
-
-### Here’s a concise side-by-side comparison between Rivra createStore and Zustand:
-
-| Feature / Aspect | **createStore** (Rivra) | **Zustand** |
-| ------------------------ | ------------------------------------ | ---------------------------------------- |
-| **Size / Complexity** | Ultra-light (~2 KB) | Larger, includes middleware and devtools |
-| **Reactivity Model** | Manual `subscribe` / `derive` | React hooks (`useStore`) |
-| **Selectors** | Optional selector argument | Built-in via hooks |
-| **Persistence** | Native `persist` option | Needs middleware plugin |
-| **DevTools Integration** | Coming soon | Built-in Redux DevTools support |
-| **Middleware** | Planned via `use()` | Full middleware API |
-| **Callback Updates** | Supported: `update(prev => {...})` | Supported: `set(state => {...})` |
-| **Derived Stores** | `derive(selector)` | Selectors or derived state |
-| **Performance** | Minimal overhead | Optimized for React, slightly heavier |
-| **Framework Support** | Framework-agnostic | React-only |
-| **TypeScript** | Fully typed generics | Excellent TS support |
-| **Persistence Control** | Built-in localStorage | Plugin required |
-| **Use Case Fit** | Libraries & multi-framework projects | React apps needing global state |
-
----
-
-
-## Minimal IndexDB Manager with Zero Dependencies.
-
- 📘 Example Usage
- --------------------------------------------------------------------------
-
- ✅ Minimal Example
- ```ts
- import createIndexDBStore from "rivra/stores"
-import IndexDBManager from "rivra/stores"
- const userStore = createIndexDBStore({
- storeName: 'users',
- });
- await userStore.add({ id: 'u1', name: 'Joseph', age: 22 });
- const all = await userStore.getAll();
- console.log(all);
- ```
-
- ✅ Full Configuration Example
- ```ts
- interface User {
- id: string;
- name: string;
- age: number;
- }
-
- const userStore = createIndexDBStore({
- dbName: "MyAppDB",
- storeName: "users",
- keyPath: "id",
- version: 1,
- });
-
- // ➕ Add record
- await userStore.add({ id: 'u1', name: 'Ada', age: 45 });
-
- // 🔁 Update record by key or object
- await userStore.update('u1', { age: 46 });
-
- // 🔍 Get a record by key
- const user = await userStore.get('u1');
- console.log('Single user:', user);
-
- // 📦 Get all records
- const allUsers = await userStore.getAll();
- console.log('All users:', allUsers);
-
- // ❌ Remove record by ID
- await userStore.remove('u1');
-
- // 🧹 Clear all records
- await userStore.clear();
-
- // 🔎 Query using a filter function
- const adults = await userStore.query(u => u.age > 30);
- console.log('Adults:', adults);
-
- // 👂 Subscribe to store changes (reactive)
- const unsubscribe = userStore.subscribe(state => {
- console.log('Store changed:', state.items);
- });
-
- // 👁️ Watch specific property or subset of data
- const watchAdults = userStore.deriveQuery(items => items.filter(u => u.age > 30));
- watchAdults.subscribe(adults => console.log('Adults updated:', adults));
-
- // 🔦 Filter (where)
- const namedJohn = await userStore.where({ name: 'John' });
- console.log('Users named John:', namedJohn);
-
- // 🥇 Get first matching record
- const firstUser = await userStore.first({ age: 46 });
- console.log('First matching user:', firstUser);
-
- // 🔍 Find by ID (alias for get)
- const found = await userStore.find('u1');
- console.log('Found user:', found);
-
- // 🧩 Put (alias for update)
- await userStore.put({ id: 'u1', name: 'Ada', age: 50 });
-
- // 💳 Perform custom transaction
- await userStore.transaction(tx => {
- const store = tx.objectStore('users');
- store.add({ id: 'u2', name: 'Ken', age: 35 });
- });
-
- // 🧭 Watch specific user reactively
- const watchUser = userStore.deriveQuery(items => items.find(u => u.id === 'u1') || null);
- watchUser.subscribe(u => console.log('u1 changed:', u));
-
- // 🧹 Unsubscribe from store updates
- unsubscribe();
- ```
-
- ✅ Multi-Store Example (using IndexDBManager)
- ```ts
- interface User {
- id: string;
- name: string;
- }
-
- interface Post {
- id: string;
- title: string;
- }
-
- // Create manager
- const db = new IndexDBManager("MyAppDB");
-
- // Create multiple stores
- const users = db.createStore("users", "id");
- const posts = db.createStore("posts", "id");
-
- // Add data
- await users.add({ id: "u1", name: "Joe" });
- await posts.add({ id: "p1", title: "Hello World" });
-
- // Query data
- const allUsers = await users.getAll();
- const allPosts = await posts.getAll();
-
- console.log(allUsers, allPosts);
-
- // Watch updates
- users.subscribe(state => console.log("Users changed:", state.items));
- posts.subscribe(state => console.log("Posts changed:", state.items));
- ```
-
- ## IndexDB with offline/online live database synchronization
-
- This is experimental currently. This api allows you make your apps offline first.
-
- ```ts
-import createIndexDBStore from "rivra/stores"
-// import IndexDBManager from "rivra/stores"
-
- const userStore = createIndexDBStore({
- dbName: "MyAppDB",
- storeName: "users",
- keyPath: "id",
- version: 1,
- sync: {
- endpoint: "https://api.example.com/users",
- async push(item, action) {
- // simple example using fetch
- if (action === "add") await fetch(this.endpoint!, { method: "POST", body: JSON.stringify(item) });
- if (action === "update") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "PUT", body: JSON.stringify(item) });
- if (action === "remove") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "DELETE" });
- },
- async pull() {
- const res = await fetch("https://api.example.com/users");
- return res.json();
- },
- interval: 15000,
- autoSync: true,
- onOffline: () => console.log("User store offline"),
- onOnline: () => console.log("User store online"),
- },
- });
-
- // Multi-store manager usage with global + per-store callbacks:
-
- const db = new IndexDBManager("MyAppDB", 1, {
- onOffline: () => console.log("Global offline"),
- onOnline: () => console.log("Global online"),
- });
-
- // per-store callbacks override global if provided
- const users = db.createStore("users", "id", {
- sync: {
- onOffline: () => console.log("Users store offline"),
- onOnline: () => console.log("Users store online"),
- }
- });
-
- const posts = db.createStore<{ id: string; title: string }>("posts", "id");
-
- ```
-
-
-### Features
-
-* File-based routing
-* Dynamic route segments `[param]`
-* URL query support
-* Optional per-link router events
-* Reactive `Link` component with optional loading UI
-* Global progress loader
-* Minimal setup—just structure `pages/`
-* Minimal indexDB manager with zero dependencies.
-* Zustand like global state management available in and outside components
-
-
-
-
-
-
-# rivra
+
+
+
+
+
+# Rivra
+**(Minimal Ripple tool kit. Not just a router)**
+
+
+
+# Router + full-stack description:
+Not just a router — Rivra is a complete toolkit combining routing, state management, storage, and full-stack app tooling, all fully accessible
+
+##
+
+
+
+
+
+-----
+
+> **Update:**
+ Rivra middleware now applies to the **client side** as well — not just API routes.
+> Global Middleware and Plugins are now executed **across all routes and resources**, ensuring unified behavior.
+> _(Page-specific middleware or plugin logic isn’t supported yet.)_
+
+---
+
+
+##
+
+## Creating New Project
+
+```bash
+npx rivra create my-app // wrapper around npm create ripple my-app.
+
+```
+
+###### For existing ripple projects use the installation and init
+
+## Installation
+
+```bash
+npm install rivra
+npx rivra init
+```
+
+or
+
+```bash
+yarn add rivra
+npx rivra init
+```
+
+
+
+## Quick Start
+
+After initiating **Rivra** which has all the rivra routing components, the pages directory, /api (if selected full stack), the routes.ts file for your app modules and the configured App.ripple file will be visible in your project src dir. The App.ripple is optional to overwrite.
+
+### Directory Structure
+
+Here's an example `src/pages/` directory:
+
+```bash
+pages/
+├── index.ripple # Home page
+├── about.ripple # About page
+└── users/
+ └── [id]/
+ └── user/
+ └── [username]/
+ └── comments/
+```
+
+Here's an `api/` directory:
+
+```bash
+api/
+├── index.ts # Root API entry point (can load all modules)
+├── posts.ts # Top-level posts routes
+└── users/
+ └── [id]/ # Dynamic user ID
+ └── user/
+ └── [username]/ # Dynamic username
+ └── comments/
+ └── index.ts # User comments routes
+
+```
+
+```ts
+import type { Reply, Req, App } from "rivra"
+
+import type {Req, Reply, App} from "mivra"
+export default async function postAPi(app: App) {
+ app.get("/", async (req: Req, reply: Reply) => {
+
+ const param = req.params
+ const queries = req.query
+
+
+ return ({ message: "some dynamic route", params: param, query: queries })
+ })
+}
+
+```
+
+
+### Rivra middlewares/plugins for fastify
+
+Rivra allows you to customize the server behaviours by leveraging on Fastify hooks and plugins. There are some times you may want to extend function.
+
+----
+```sql
+
+plugins/
+ ├── middleware/
+ │ ├── cors.ts → global middleware (order=1)
+ │ └── helmet.ts → global middleware (order=2)
+ ├── auth.md.ts → middleware only for /auth/*
+ ├── auth.pg.ts → /auth routes
+ ├── users/
+ │ ├── users.md.ts → middleware only for /users/*
+ │ └── index.ts → /users routes
+ └── logger.ts → global plugin
+```
+
+
+* Example of plugin
+```ts
+export const order = 2; // order from 1-10 gives you ability to prioritize the order which your hooks run.
+
+export default async function (req: Req, reply: Reply, app: App) {
+ console.log("global plugin triggered")
+ reply.header('X-Powered-By', 'Rivra');
+
+ if (req.url === "/api/users") {
+ reply.send({ message: "Users root route" });
+ } else if (req.url === "/api/users/profile") {
+ reply.send({ message: "User profile route" });
+ }
+
+}
+
+
+```
+
+* Example of middlewre
+```ts
+
+export default function (req: Req, res: Reply,) {
+ console.log("Protected middleware Incoming:", req.method, req.url);
+ const truthy = true
+ if (!truthy) {
+ res.code(400).send({error: "Bad request"})
+ return
+ }
+ if (req.url === "/api/blocked") {
+ res.code(403).send({ error: "Forbidden" });
+ return;
+ }
+
+}
+
+
+```
+
+#### Here’s a simple and clear table that explains the difference between a plugin and a middleware and how Rivra handle them.
+
+| **Aspect** | **Plugin** | **Middleware** |
+| ------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| **Purpose** | Extends or configures the Fastify instance (adds routes, hooks, decorators, schemas, etc.) | Intercepts and processes requests before or during route handling (authentication, logging, validation, etc.) |
+| **Function Signature** | `(app: FastifyInstance) => void` | `(req: FastifyRequest, reply: FastifyReply, app: FastifyInstance) => void` |
+| **Execution Time** | Runs **once** during server startup to register logic | Runs **on every incoming request** (depending on where it’s applied) |
+| **Common Use Cases** | Adding routes, setting up databases, registering third-party plugins, global configurations | Authentication checks, access control, logging, pre-processing requests` | Using hooks like `app.addHook("preHandler", handler)` or route-level middlewares |
+| **Scope** | Can be **scoped** to a prefix (e.g., `/api/protected`) | Can be **global** or **route-specific** |
+| **Impact** | Modifies or enhances the app’s capabilities | Filters or modifies the request/response cycle |
+| **File Naming Convention (in your system)** | `*.pg.ts` (Scoped Plugin) | `*.md.ts` (Middleware) |
+| **Example (Plugin)** | `export default async function (app) { app.get('/hello', () => 'Hi');}` | |
+| **Example (Middleware)** | | `t export default async function (req, reply, app) { if (!req.headers.auth) reply.code(401).send({ error: 'Unauthorized' });}` |
+
+
+| Type | Example File | Prefix Applied | Loaded As | Order |
+| ----------------- | ------------------------------- | -------------- | ---------- | -------------------------- |
+| Global middleware | `/plugins/middleware/logger.ts` | none | middleware | before all plugins |
+| Route middleware | `/plugins/auth/auth.md.ts` | `/auth` | middleware | after global, before route |
+| Prefixed plugin | `/plugins/auth.pg.ts` | `/auth` | plugin | normal |
+| Folder plugin | `/plugins/payments/index.ts` | `/payments` | plugin | normal |
+| Global plugin | `/plugins/cors.ts` | none | plugin | normal |
+
+
+Dynamic segments use `[param]` notation like `[id]` or `[username]`.
+
+---
+
+### App Component
+
+```ts
+import {PageRoutes} from "rivra/router"
+import { modules } from "./routes";
+
+export component App() {
+
+}
+```
+
+That's it! Your routing is now set up. `PageRoutes` automatically reads your `pages/` folder and matches routes, including dynamic parameters.
+
+---
+
+### Link Component
+
+Use the `Link` component for navigation:
+
+```ts
+import Link from "rivra/router"
+
+export component Navigation() {
+
+}
+```
+
+#### Props:
+
+| Prop | Type | Default | Description |
+| ------------------ | --------------------- | ------- | --------------------------------------------- |
+| `href` | `string` | — | Path to navigate to |
+| `children` | `Component` | — | Content to render inside link |
+| `onLoading` | `() => void` | — | Callback when navigation starts |
+| `emitEvent` | `boolean` | `true` | Whether to trigger route events for this link |
+| `loadingComponent` | `Component` | — | Optional component to show while loading |
+| `className` | `string` | — | Additional CSS class names for styling |
+| `queries` | `Record` | — | Optional query parameters for URLSearch |
+
+
+---
+
+### Router And Events
+
+You can subscribe to router events if you need custom behavior:
+
+```ts
+import { useRouter } from "rivra/router"
+
+const router = useRouter();
+
+router.on("start", path => console.log("Navigating to:", path));
+router.on("complete", path => console.log("Navigation finished:", path));
+router.on("change", path => console.log("Route changed:", path));
+
+
+
+//Guard back navigation
+router.beforePopState((url) => {
+ if (url === "/protected") {
+ console.log("Navigation blocked:", url);
+ return false; // Cancel navigation
+ }
+});
+
+// Navigate to a new route
+router.push("/users/42?tab=posts");
+router.push("/users/42?tab=posts", true, false, {name: "John", age: 20}); // path, emitEvent, shallow (partial url change), queries
+
+
+//Replace URL shallowly (no full sync)//
+router.replace("/users/42?tab=profile", true, true);
+
+// Prefetch a route
+router.prefetch("/about");
+
+// Resolve href
+console.log("Resolved href:", router.resolveHref("/contact?ref=home"));
+
+// Access reactive properties
+console.log("Current path:", router.path);
+console.log("Query params:", router.queries);
+console.log("Dynamic params:", router.params);
+console.log("Full URL:", router.asPath);
+
+// Access full URL info
+console.log(router.host);
+console.log(router.hostname);
+console.log(router.origin);
+console.log(router.protocol);
+console.log(router.port);
+console.log(router.href);
+console.log(router.search);
+console.log(router.hash);
+```
+
+* `start`: triggered before navigation begins
+* `complete`: triggered after navigation finishes
+* `change`: triggered on every path change
+
+You can opt out of events per `Link` with `emitEvent={false}`.
+
+---
+
+### Dynamic Route Params
+
+Access route params and queries in any component:
+
+```ts
+import { useRouter } from "rivra/router"
+
+export component UserProfile() {
+ const router = useRouter();
+
+ const id = router.params.id; // dynamic param
+ const username = router.params.username;
+ const queryName = router.queries.name; // URL query ?name=...
+ // or
+ const {params, queries} = router;
+
+
+ {"User ID: " + id}
+ {"Username: " + username}
+ {"Query name: " + queryName}
+
+}
+```
+
+---
+
+### Global Loading Indicator (Optional)
+you can disable it with props ```ts
+
+```
+
+```ts
+import {PageRoutes} from "rivra/router"
+import { modules } from "./routes";
+
+export component App() {
+
+}
+
+```
+
+
+---
+
+---
+
+### A minimal reactive store that manages shared state across your app with an intuitive API. It provides reactivity, persistence, and derived state — all in under a few lines of code.
+
+| Feature | Description |
+| ------------------------------ | ---------------------------------------------------------------------------------------- |
+| **`get()`** | Returns the current store value. |
+| **`set(next)`** | Replaces the entire store value. |
+| **`update(partialOrFn)`** | Merges new data into the store. Supports both object patching and callback styles. |
+| **`subscribe(fn, selector?)`** | Reactively listens for state changes, optionally to a selected portion. |
+| **`derive(selector)`** | Creates a new store derived from a specific part of another store (like computed state). |
+| **`delete(keys)`** | Removes one or more keys from the store. |
+| **`clear()`** | Resets store to initial state and removes persisted data. |
+| **`persist` (option)** | Automatically saves and restores state from `localStorage`. |
+
+
+
+-------------------- Example Stores --------------------
+```ts
+ //* Route store for storing current route path
+ //* Persisted in localStorage as "routeStore"
+ //*/
+export const routeStore = createStore(
+ { path: "/" },
+ { persist: true, storageKey: "routeStore" }
+);
+
+/**
+ * App store for global state
+ * Tracks path, user info, theme
+ * Persisted in localStorage as "appStore"
+ */
+export const appStore = createStore(
+ { path: "/", user: null as null | { name: string }, theme: "light" },
+ { persist: true, storageKey: "appStore" }
+);
+
+```
+Here are extra two simple Hello World store examples for getting started and explain things better.
+
+### Store without persist (default)
+```ts
+import { createStore } from "rivra/store"
+
+// Create a simple store
+const helloStore = createStore({ message: "Hello World!" });
+
+// Subscribe to changes
+helloStore.subscribe(state => {
+ console.log("Current message:", state.message);
+});
+
+// Get changes anywhere
+const data = helloStore.get();
+console.log(helloStore) // { message: Current message}
+console.log(data.message) // Current message
+
+
+// Update the store
+helloStore.update({ message: "Hello Ripple!" });
+
+// Output:
+// Current message: Hello World!
+// Current message: Hello Ripple!
+
+
+
+```
+
+### Store with persist
+
+```ts
+import { createStore } from "rivra/store"
+import { track } from "ripple"
+
+const message = track("")
+
+// Create a persisted store
+const persistentHelloStore = createStore(
+ { message: "Hello Persistent World!" },
+ { persist: true, storageKey: "helloStore" }
+);
+
+// Subscribe to changes
+persistentHelloStore.subscribe(state => {
+ console.log("Current message:", state.message);
+});
+
+
+// Get changes anywhere
+const data = helloStore.get();
+console.log(helloStore) // { message: Current message}
+console.log(data.message) // Current message
+
+
+// Update the store
+persistentHelloStore.update({ message: "Updated and Persisted!" });
+
+
+// Callback update (safe addition)
+persistentHelloStore.update(prev => ({ message: prev.message + " " + @message }));
+
+
+// Reload the page and subscribe again
+persistentHelloStore.subscribe(state => {
+ console.log("After reload:", state.message);
+});
+
+// Output (after reload):
+// After reload: Updated and Persisted!
+
+
+
+export const appStore = createStore(
+ {
+ user: { name: "Joe", location: "unknown", preferences: [] },
+ count: 0,
+ theme: "light",
+ },
+ { persist: true, storageKey: "appStore" }
+);
+
+
+
+// Subscribe to entire state
+appStore.subscribe(s => console.log("State changed:", s));
+
+// Watch a specific value
+appStore.watch(s => s.count, (n, o) => console.log(`Count: ${o} → ${n}`));
+
+// Use middleware for logging
+appStore.use((state, action, payload) =>
+ console.log(`[${action}]`, payload, state)
+);
+
+// Partial update
+appStore.update({ count: 1 });
+
+// Callback update (safe addition)
+appStore.update(prev => ({ count: prev.count + 1 }));
+
+// Derived store
+const themeStore = appStore.derive(s => s.theme);
+themeStore.subscribe(theme => console.log("Theme:", theme));
+
+// Clear store
+appStore.clear();
+
+
+```
+
+
+### Here’s a concise side-by-side comparison between Rivra createStore and Zustand:
+
+| Feature / Aspect | **createStore** (Rivra) | **Zustand** |
+| ------------------------ | ------------------------------------ | ---------------------------------------- |
+| **Size / Complexity** | Ultra-light (~2 KB) | Larger, includes middleware and devtools |
+| **Reactivity Model** | Manual `subscribe` / `derive` | React hooks (`useStore`) |
+| **Selectors** | Optional selector argument | Built-in via hooks |
+| **Persistence** | Native `persist` option | Needs middleware plugin |
+| **DevTools Integration** | Coming soon | Built-in Redux DevTools support |
+| **Middleware** | Planned via `use()` | Full middleware API |
+| **Callback Updates** | Supported: `update(prev => {...})` | Supported: `set(state => {...})` |
+| **Derived Stores** | `derive(selector)` | Selectors or derived state |
+| **Performance** | Minimal overhead | Optimized for React, slightly heavier |
+| **Framework Support** | Framework-agnostic | React-only |
+| **TypeScript** | Fully typed generics | Excellent TS support |
+| **Persistence Control** | Built-in localStorage | Plugin required |
+| **Use Case Fit** | Libraries & multi-framework projects | React apps needing global state |
+
+---
+
+
+## Minimal IndexDB Manager with Zero Dependencies.
+
+ 📘 Example Usage
+ --------------------------------------------------------------------------
+
+ ✅ Minimal Example
+ ```ts
+ import createIndexDBStore from "rivra/stores"
+import IndexDBManager from "rivra/stores"
+ const userStore = createIndexDBStore({
+ storeName: 'users',
+ });
+ await userStore.add({ id: 'u1', name: 'Joseph', age: 22 });
+ const all = await userStore.getAll();
+ console.log(all);
+ ```
+
+ ✅ Full Configuration Example
+ ```ts
+ interface User {
+ id: string;
+ name: string;
+ age: number;
+ }
+
+ const userStore = createIndexDBStore({
+ dbName: "MyAppDB",
+ storeName: "users",
+ keyPath: "id",
+ version: 1,
+ });
+
+ // ➕ Add record
+ await userStore.add({ id: 'u1', name: 'Ada', age: 45 });
+
+ // 🔁 Update record by key or object
+ await userStore.update('u1', { age: 46 });
+
+ // 🔍 Get a record by key
+ const user = await userStore.get('u1');
+ console.log('Single user:', user);
+
+ // 📦 Get all records
+ const allUsers = await userStore.getAll();
+ console.log('All users:', allUsers);
+
+ // ❌ Remove record by ID
+ await userStore.remove('u1');
+
+ // 🧹 Clear all records
+ await userStore.clear();
+
+ // 🔎 Query using a filter function
+ const adults = await userStore.query(u => u.age > 30);
+ console.log('Adults:', adults);
+
+ // 👂 Subscribe to store changes (reactive)
+ const unsubscribe = userStore.subscribe(state => {
+ console.log('Store changed:', state.items);
+ });
+
+ // 👁️ Watch specific property or subset of data
+ const watchAdults = userStore.deriveQuery(items => items.filter(u => u.age > 30));
+ watchAdults.subscribe(adults => console.log('Adults updated:', adults));
+
+ // 🔦 Filter (where)
+ const namedJohn = await userStore.where({ name: 'John' });
+ console.log('Users named John:', namedJohn);
+
+ // 🥇 Get first matching record
+ const firstUser = await userStore.first({ age: 46 });
+ console.log('First matching user:', firstUser);
+
+ // 🔍 Find by ID (alias for get)
+ const found = await userStore.find('u1');
+ console.log('Found user:', found);
+
+ // 🧩 Put (alias for update)
+ await userStore.put({ id: 'u1', name: 'Ada', age: 50 });
+
+ // 💳 Perform custom transaction
+ await userStore.transaction(tx => {
+ const store = tx.objectStore('users');
+ store.add({ id: 'u2', name: 'Ken', age: 35 });
+ });
+
+ // 🧭 Watch specific user reactively
+ const watchUser = userStore.deriveQuery(items => items.find(u => u.id === 'u1') || null);
+ watchUser.subscribe(u => console.log('u1 changed:', u));
+
+ // 🧹 Unsubscribe from store updates
+ unsubscribe();
+ ```
+
+ ✅ Multi-Store Example (using IndexDBManager)
+ ```ts
+ interface User {
+ id: string;
+ name: string;
+ }
+
+ interface Post {
+ id: string;
+ title: string;
+ }
+
+ // Create manager
+ const db = new IndexDBManager("MyAppDB");
+
+ // Create multiple stores
+ const users = db.createStore("users", "id");
+ const posts = db.createStore("posts", "id");
+
+ // Add data
+ await users.add({ id: "u1", name: "Joe" });
+ await posts.add({ id: "p1", title: "Hello World" });
+
+ // Query data
+ const allUsers = await users.getAll();
+ const allPosts = await posts.getAll();
+
+ console.log(allUsers, allPosts);
+
+ // Watch updates
+ users.subscribe(state => console.log("Users changed:", state.items));
+ posts.subscribe(state => console.log("Posts changed:", state.items));
+ ```
+
+ ## IndexDB with offline/online live database synchronization
+
+ This is experimental currently. This api allows you make your apps offline first.
+
+ ```ts
+import createIndexDBStore from "rivra/stores"
+// import IndexDBManager from "rivra/stores"
+
+ const userStore = createIndexDBStore({
+ dbName: "MyAppDB",
+ storeName: "users",
+ keyPath: "id",
+ version: 1,
+ sync: {
+ endpoint: "https://api.example.com/users",
+ async push(item, action) {
+ // simple example using fetch
+ if (action === "add") await fetch(this.endpoint!, { method: "POST", body: JSON.stringify(item) });
+ if (action === "update") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "PUT", body: JSON.stringify(item) });
+ if (action === "remove") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "DELETE" });
+ },
+ async pull() {
+ const res = await fetch("https://api.example.com/users");
+ return res.json();
+ },
+ interval: 15000,
+ autoSync: true,
+ onOffline: () => console.log("User store offline"),
+ onOnline: () => console.log("User store online"),
+ },
+ });
+
+ // Multi-store manager usage with global + per-store callbacks:
+
+ const db = new IndexDBManager("MyAppDB", 1, {
+ onOffline: () => console.log("Global offline"),
+ onOnline: () => console.log("Global online"),
+ });
+
+ // per-store callbacks override global if provided
+ const users = db.createStore("users", "id", {
+ sync: {
+ onOffline: () => console.log("Users store offline"),
+ onOnline: () => console.log("Users store online"),
+ }
+ });
+
+ const posts = db.createStore<{ id: string; title: string }>("posts", "id");
+
+ ```
+
+
+### Features
+
+* File-based routing
+* Dynamic route segments `[param]`
+* URL query support
+* Optional per-link router events
+* Reactive `Link` component with optional loading UI
+* Global progress loader
+* Minimal setup—just structure `pages/`
+* Minimal indexDB manager with zero dependencies.
+* Zustand like global state management available in and outside components
+
+
+
+
+
+
+# rivra
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 0000000..bd9dc32
--- /dev/null
+++ b/src/cli.ts
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+import { Command } from "commander";
+import { createApp } from "./packages/createApp";
+import { startDevServer } from "./packages/devServer";
+import { buildProject } from "./packages/build";
+import pkg from "../package.json" assert { type: "json" };
+
+const program = new Command();
+
+program
+ .name("rivra")
+ .description("Rivra — Full-stack Ripple framework CLI")
+ .version(pkg.version);
+
+// -----------------------
+// rivra create
+// -----------------------
+program
+ .command("create")
+ .argument("")
+ .description("Create a new Rivra project")
+ .action(async (name: string) => {
+ await createApp(name);
+ });
+
+// -----------------------
+// rivra dev
+// -----------------------
+program
+ .command("dev")
+ .description("Start Rivra dev server")
+ .action(async () => {
+ await startDevServer();
+ });
+
+// -----------------------
+// rivra build
+// -----------------------
+program
+ .command("build")
+ .description("Build production bundle")
+ .action(async () => {
+ await buildProject();
+ });
+
+program.parse(process.argv);
diff --git a/src/createDBStore.ts b/src/createDBStore.ts
index 86e0b35..fa577e5 100644
--- a/src/createDBStore.ts
+++ b/src/createDBStore.ts
@@ -1,768 +1,768 @@
-/**
- * indexdb-store.ts
- *
- * Full implementation of createIndexDBStore + IndexDBManager with:
- * - Full JSDoc
- * - Multi-store manager
- * - Optional sync adapter hooks
- * - Global (manager) + per-store online/offline callbacks
- * - SSR-safe checks
- * - Preserves all existing methods and signatures
- *
- * Drop into your project. Typescript friendly.
- */
-
-import { createStore } from "./store";
-
-/* --------------------------------------------------------------------------
- * Types & Options
- * -------------------------------------------------------------------------- */
-
-/**
- * Live sync & network callback config (part of DBStoreOptions.sync)
- */
-interface SyncOptions {
- /** Base URL or remote DB client (optional helper) */
- endpoint?: string;
- /** Push local change to remote */
- push?: (item: any, action: "add" | "update" | "remove") => Promise;
- /** Pull data from remote */
- pull?: () => Promise;
- /** Auto-sync pull interval (ms) */
- interval?: number;
- /** Automatically push on local changes */
- autoSync?: boolean;
- /** Called when this store detects it's offline */
- onOffline?: () => void;
- /** Called when this store becomes online again */
- onOnline?: () => void;
-}
-
-/**
- * Configuration options for IndexedDB-backed reactive store.
- */
-interface DBStoreOptions {
- /** Database name (defaults to "default_db") */
- dbName?: string;
- /** Store name (required) */
- storeName: string;
- /** Key path for object store (defaults to "id") */
- keyPath?: string;
- /** Database version (defaults to 1) */
- version?: number;
- /** Automatically load data into store on init (default: true) */
- autoLoad?: boolean;
- /** Live sync configuration (optional) */
- sync?: SyncOptions;
-}
-
-/** Filter type for query operations */
-type QueryFilter = Partial> | ((item: T) => boolean);
-
-/* --------------------------------------------------------------------------
- * Utility: SSR-safe feature detection
- * -------------------------------------------------------------------------- */
-const isBrowser =
- typeof window !== "undefined" && typeof navigator !== "undefined";
-const supportsBroadcastChannel = typeof BroadcastChannel !== "undefined";
-
-/* --------------------------------------------------------------------------
- * createIndexDBStore
- * -------------------------------------------------------------------------- */
-
-/**
- * Creates a reactive IndexedDB-backed store with ORM-like methods.
- * Includes reactivity, syncing across tabs, live sync hooks, and offline detection.
- *
- * @template T - The record type stored in IndexedDB.
- * @param {DBStoreOptions} options - Configuration for the store.
- * @returns {object} Reactive data store with CRUD, query, and utility methods.
- * @example
- * // Create multiple stores
- * const userStore = createIndexDBStore({storeName: 'users'});
- */
-export function createIndexDBStore>(
- options: DBStoreOptions
-) {
- const {
- dbName = "default_db",
- storeName,
- keyPath,
- version = 1,
- autoLoad = true,
- sync,
- } = options;
-
- // main reactive store for items
- const store = createStore<{ items: T[] }>({ items: [] });
-
- // database reference and runtime state
- let db: IDBDatabase | null = null;
- let resolvedKeyPath: string | null = keyPath || null;
-
- // cross-tab channel (guarded for SSR / old browsers)
- const bc =
- isBrowser && supportsBroadcastChannel
- ? new BroadcastChannel(`${dbName}_${storeName}_sync`)
- : null;
-
- // network state
- let isOffline = isBrowser ? !navigator.onLine : false;
-
- /* ------------------------------------------------------------------------
- * DB helpers
- * ------------------------------------------------------------------------ */
-
- /**
- * Opens or upgrades the IndexedDB database.
- * @returns {Promise} The database instance.
- */
- async function openDB(): Promise {
- if (!isBrowser)
- throw new Error("IndexedDB not available in this environment (SSR).");
- return new Promise((resolve, reject) => {
- const req = indexedDB.open(dbName, version);
- req.onupgradeneeded = () => {
- const _db = req.result;
- if (!_db.objectStoreNames.contains(storeName)) {
- _db.createObjectStore(storeName, {
- keyPath: resolvedKeyPath || "id",
- });
- }
- };
- req.onsuccess = () => resolve(req.result);
- req.onerror = () => reject(req.error);
- });
- }
-
- async function ensureDB() {
- if (!db) db = await openDB();
- return db!;
- }
-
- /** Initializes the store and loads all data if autoLoad is true. */
- async function init() {
- if (!isBrowser) return; // SSR-safe: do not attempt DB operations on server
- db = await openDB();
- if (autoLoad) {
- const all = await getAll();
- store.set({ items: all });
- }
- setupNetworkListeners();
- setupAutoPull();
- }
-
- function ensureKeyPath(item: T) {
- if (!resolvedKeyPath) {
- const firstKey = Object.keys(item)[0];
- resolvedKeyPath = firstKey;
- }
- return resolvedKeyPath!;
- }
-
- /* ------------------------------------------------------------------------
- * CRUD
- * ------------------------------------------------------------------------ */
-
- /**
- * Adds a new record to the store.
- * @param {T} item - The record to add.
- * @returns {Promise}
- */
- async function add(item: T) {
- const dbRef = await ensureDB();
- ensureKeyPath(item);
- return new Promise((resolve, reject) => {
- const tx = dbRef.transaction(storeName, "readwrite");
- const req = tx.objectStore(storeName).add(item);
- req.onsuccess = async () => {
- store.update((s) => ({ items: [...s.items, item] }));
- bc?.postMessage("sync");
- await maybeSync(item, "add");
- resolve();
- };
- req.onerror = () => reject(req.error);
- });
- }
-
- /**
- * Updates an existing record by ID or object.
- * @param {IDBValidKey | Partial} idOrItem - Record ID or object containing key.
- * @param {Partial} [partial] - Partial data to merge when using ID form.
- * @returns {Promise}
- */
- async function update(
- idOrItem: IDBValidKey | Partial,
- partial?: Partial
- ) {
- const dbRef = await ensureDB();
-
- let id: IDBValidKey;
- let newData: Partial = {};
-
- const isKeyType =
- typeof idOrItem === "string" ||
- typeof idOrItem === "number" ||
- idOrItem instanceof Date ||
- idOrItem instanceof ArrayBuffer ||
- ArrayBuffer.isView(idOrItem) ||
- Array.isArray(idOrItem);
-
- if (isKeyType) {
- id = idOrItem as IDBValidKey;
- newData = partial || {};
- } else if (typeof idOrItem === "object" && idOrItem !== null) {
- ensureKeyPath(idOrItem as T);
- id = (idOrItem as any)[resolvedKeyPath!];
- newData = idOrItem as Partial;
- } else {
- throw new Error("Invalid argument passed to update()");
- }
-
- const existing = await get(id);
- if (!existing) return;
-
- const updated = { ...existing, ...newData } as T;
-
- return new Promise((resolve, reject) => {
- const tx = dbRef.transaction(storeName, "readwrite");
- const req = tx.objectStore(storeName).put(updated);
- req.onsuccess = async () => {
- store.update((s) => ({
- items: s.items.map((i) =>
- (i as any)[resolvedKeyPath!] === id ? updated : i
- ),
- }));
- bc?.postMessage("sync");
- await maybeSync(updated, "update");
- resolve();
- };
- req.onerror = () => reject(req.error);
- });
- }
-
- /**
- * Retrieves a record by ID.
- * @param {IDBValidKey} id - Record key.
- * @returns {Promise} The matching record, if found.
- */
- async function get(id: IDBValidKey): Promise {
- const dbRef = await ensureDB();
- return new Promise((resolve, reject) => {
- const tx = dbRef.transaction(storeName, "readonly");
- const req = tx.objectStore(storeName).get(id);
- req.onsuccess = () => resolve(req.result as T | undefined);
- req.onerror = () => reject(req.error);
- });
- }
-
- /**
- * Retrieves all records.
- * @returns {Promise} All stored records.
- */
- async function getAll(): Promise {
- const dbRef = await ensureDB();
- return new Promise((resolve, reject) => {
- const tx = dbRef.transaction(storeName, "readonly");
- const req = tx.objectStore(storeName).getAll();
- req.onsuccess = () => resolve(req.result as T[]);
- req.onerror = () => reject(req.error);
- });
- }
-
- /**
- * Removes a record by ID.
- * @param {IDBValidKey} id - Record key.
- * @returns {Promise}
- */
- async function remove(id: IDBValidKey) {
- const dbRef = await ensureDB();
- const key = resolvedKeyPath || "id";
- return new Promise((resolve, reject) => {
- const tx = dbRef.transaction(storeName, "readwrite");
- const req = tx.objectStore(storeName).delete(id);
- req.onsuccess = async () => {
- store.update((s) => ({
- items: s.items.filter((i) => (i as any)[key] !== id),
- }));
- bc?.postMessage("sync");
- await maybeSync({ [key]: id } as any, "remove");
- resolve();
- };
- req.onerror = () => reject(req.error);
- });
- }
-
- /**
- * Clears all data from the store.
- * @returns {Promise}
- */
- async function clear() {
- const dbRef = await ensureDB();
- return new Promise((resolve, reject) => {
- const tx = dbRef.transaction(storeName, "readwrite");
- const req = tx.objectStore(storeName).clear();
- req.onsuccess = () => {
- store.set({ items: [] });
- bc?.postMessage("sync");
- resolve();
- };
- req.onerror = () => reject(req.error);
- });
- }
-
- /* ------------------------------------------------------------------------
- * Queries & Utilities
- * ------------------------------------------------------------------------ */
-
- async function where(filter: QueryFilter): Promise {
- const all = await getAll();
- if (typeof filter === "function") return all.filter(filter);
- return all.filter((item) =>
- Object.entries(filter).every(([k, v]) => item[k as keyof T] === v)
- );
- }
-
- async function first(filter?: QueryFilter): Promise {
- const res = filter ? await where(filter) : await getAll();
- return res[0];
- }
-
- async function find(id: IDBValidKey): Promise {
- return get(id);
- }
-
- /**
- * Get the last matching record
- * @example
- * const lastAdult = await User.last(u => u.age >= 18);
- */
- async function last(filter?: QueryFilter): Promise {
- const res = filter ? await where(filter) : await getAll();
- return res[res.length - 1];
- }
-
- /**
- * Count items matching filter
- * @example
- * const adultCount = await User.count(u => u.age >= 18);
- */
- async function count(filter?: QueryFilter): Promise {
- const res = filter ? await where(filter) : await getAll();
- return res.length;
- }
-
- /**
- * Check if any record exists matching filter
- * @example
- * const exists = await User.exists({ name: "John" });
- */
- async function exists(filter: QueryFilter): Promise {
- const res = await where(filter);
- return res.length > 0;
- }
-
- /**
- * Get a random record
- * @example
- * const randomUser = await User.random();
- */
- async function random(filter?: QueryFilter): Promise {
- const res = filter ? await where(filter) : await getAll();
- if (res.length === 0) return undefined;
- return res[Math.floor(Math.random() * res.length)];
- }
-
- async function below(filter: Partial): Promise {
- const all = await getAll();
- return all.filter(item =>
- Object.entries(filter).every(([k, v]) => {
- const val = item[k as keyof T];
- if (typeof val === "number" && typeof v === "number") return (val as number) < (v as number);
- if (typeof val === "string" && typeof v === "string") return (val as string) < (v as string);
- return false;
- })
- );
-}
-
-async function above(filter: Partial): Promise {
- const all = await getAll();
- return all.filter(item =>
- Object.entries(filter).every(([k, v]) => {
- const val = item[k as keyof T];
- if (typeof val === "number" && typeof v === "number") return (val as number) > (v as number);
- if (typeof val === "string" && typeof v === "string") return (val as string) > (v as string);
- return false;
- })
- );
-}
-
-
-
- async function query(predicate: (item: T) => boolean): Promise {
- const all = await getAll();
- return all.filter(predicate);
- }
-
- /**
- * Creates a derived live query that reacts to store changes.
- * @template U
- * @param {(items: T[]) => U} selector - Function that selects part of the data.
- * @returns {{ subscribe(fn: (value: U) => void): () => void; get(): U }}
- */
- function deriveQuery(selector: (items: T[]) => U) {
- const live = createStore({ value: selector(store.get().items) });
-
- store.watch(
- (s) => s.items,
- (items) => {
- const result = selector(items);
- live.set({ value: result });
- }
- );
-
- return {
- subscribe: (fn: (v: U) => void) => live.subscribe((s) => fn(s.value)),
- get: () => live.get().value,
- };
- }
-
- /* ------------------------------------------------------------------------
- * Live sync & network listeners (per-store)
- * ------------------------------------------------------------------------ */
-
- async function maybeSync(item: any, action: "add" | "update" | "remove") {
- // sync only if configured and online
- if (
- sync?.autoSync &&
- typeof isBrowser !== "undefined" &&
- navigator.onLine &&
- sync.push
- ) {
- try {
- await sync.push(item, action);
- } catch (err) {
- // swallow - caller may implement retry
- console.warn(
- `[createIndexDBStore:${storeName}] sync push failed:`,
- err
- );
- }
- }
- }
-
- function setupAutoPull() {
- if (!isBrowser) return;
- if (sync?.interval && sync.pull) {
- setInterval(async () => {
- if (!navigator.onLine) return;
- try {
- const remote = await sync.pull!();
- if (remote) {
- store.set({ items: remote as T[] });
- }
- } catch (err) {
- console.warn(
- `[createIndexDBStore:${storeName}] sync pull failed:`,
- err
- );
- }
- }, sync.interval);
- }
- }
-
- function setupNetworkListeners() {
- if (!isBrowser) return;
-
- // offline
- window.addEventListener("offline", () => {
- isOffline = true;
- try {
- sync?.onOffline?.();
- } catch (err) {
- console.error("onOffline callback error:", err);
- }
- // Trigger store subscribers reactively (announce network change)
- store.update((s) => ({ items: s.items }));
- });
-
- // online
- window.addEventListener("online", async () => {
- const wasOffline = isOffline;
- isOffline = false;
- try {
- sync?.onOnline?.();
- } catch (err) {
- console.error("onOnline callback error:", err);
- }
- // If we were offline and now online, optionally pull latest
- if (wasOffline && sync?.pull) {
- try {
- const remote = await sync.pull();
- if (remote) store.set({ items: remote as T[] });
- } catch (err) {
- console.warn(
- `[createIndexDBStore:${storeName}] sync pull after online failed:`,
- err
- );
- }
- }
- // notify subscribers (reactive)
- store.update((s) => ({ items: s.items }));
- });
- }
-
- // cross-tab broadcast listener (keeps store in sync across tabs)
- if (bc) {
- bc.onmessage = async (msg) => {
- if (msg.data === "sync") {
- const all = await getAll();
- store.set({ items: all });
- }
- };
- }
-
- /* ------------------------------------------------------------------------
- * Transactions
- * ------------------------------------------------------------------------ */
-
- /**
- * Runs a custom transaction operation.
- * @param {(tx: IDBTransaction) => void} fn - Transaction callback.
- * @returns {Promise}
- */
- async function transaction(fn: (tx: IDBTransaction) => void) {
- const dbRef = await ensureDB();
- const tx = dbRef.transaction(storeName, "readwrite");
- fn(tx);
- return new Promise((resolve, reject) => {
- tx.oncomplete = () => resolve();
- tx.onerror = () => reject(tx.error);
- });
- }
-
- // initialize
- init();
-
- /* ------------------------------------------------------------------------
- * Public API (do not remove these keys — kept for compatibility)
- * ------------------------------------------------------------------------ */
- return {
- add,
- update,
- get,
- getAll,
- remove,
- clear,
- query,
- subscribe: store.subscribe,
- watch: store.watch,
- deriveQuery,
- where,
- first,
- find,
- put: update,
- transaction,
- random,
- exists,
- count,
- last,
- above,
- below
- };
-}
-
-/* --------------------------------------------------------------------------
- * IndexDBManager — multi-store manager with global network callbacks
- * -------------------------------------------------------------------------- */
-
-/**
- * Options for IndexDBManager constructor (global network callbacks).
- */
-interface IndexDBManagerOptions {
- onOffline?: () => void;
- onOnline?: () => void;
-}
-
-/**
- * A lightweight IndexedDB manager that can create and manage multiple stores.
- * Supports optional global onOnline/onOffline callbacks. Each store also supports
- * per-store callbacks via its DBStoreOptions.sync.onOnline/onOffline.
- */
-export class IndexDBManager {
- private dbName: string;
- private version: number;
- private stores: Record>> =
- {};
- private globalOnOffline?: () => void;
- private globalOnOnline?: () => void;
-
- /**
- * Create a manager for a named IndexedDB database.
- * @param {string} dbName - database name
- * @param {number} [version=1] - db version
- * @param {IndexDBManagerOptions} [opts] - global network callbacks
- */
- constructor(
- dbName: string,
- version: number = 1,
- opts?: IndexDBManagerOptions
- ) {
- this.dbName = dbName;
- this.version = version;
- this.globalOnOffline = opts?.onOffline;
- this.globalOnOnline = opts?.onOnline;
-
- // If running in browser, hook into global online/offline to call manager-level hooks.
- if (isBrowser) {
- window.addEventListener("offline", () => {
- try {
- this.globalOnOffline?.();
- } catch (err) {
- console.error("IndexDBManager.onOffline error:", err);
- }
- });
- window.addEventListener("online", () => {
- try {
- this.globalOnOnline?.();
- } catch (err) {
- console.error("IndexDBManager.onOnline error:", err);
- }
- });
- }
- }
-
- /**
- * Creates a new store within the database.
- *
- * @template T
- * @param {string} storeName - The name of the object store.
- * @param {string} [keyPath='id'] - The key path used as the primary key.
- * @param {{ sync?: SyncOptions }} [opts] - Optional per-store additions (e.g., per-store onOnline/onOffline).
- * @returns {ReturnType>} The created store instance.
- *
- * @example
- * // Create multiple stores
- * const users = db.createStore("users", "id");
- * const posts = db.createStore("posts", "id");
- */
- createStore>(
- storeName: string,
- keyPath: string = "id",
- opts?: { sync?: SyncOptions }
- ) {
- // merge global and per-store network callbacks: per-store takes precedence
- const mergedSync: SyncOptions | undefined = opts?.sync
- ? {
- ...opts.sync,
- onOffline: opts.sync.onOffline ?? this.globalOnOffline,
- onOnline: opts.sync.onOnline ?? this.globalOnOnline,
- }
- : this.globalOnOffline || this.globalOnOnline
- ? { onOffline: this.globalOnOffline, onOnline: this.globalOnOnline }
- : undefined;
-
- const store = createIndexDBStore({
- dbName: this.dbName,
- storeName,
- keyPath,
- version: this.version,
- sync: mergedSync,
- });
- this.stores[storeName] = store;
- return store;
- }
-
- /**
- * Retrieves a store by its name.
- * @param {string} name - The store name.
- * @returns {ReturnType | undefined} The store instance, if found.
- */
- getStore(name: string) {
- return this.stores[name];
- }
-}
-
-/* --------------------------------------------------------------------------
- * 📘 Example Usage (merged, minimal + full + multi-store)
- * --------------------------------------------------------------------------
- *
- * Minimal:
- *
- * const userStore = createIndexDBStore({
- * storeName: 'users',
- * });
- *
- * await userStore.add({ id: 'u1', name: 'Joseph', age: 22 });
- * const all = await userStore.getAll();
- * console.log(all);
- *
- *
- * Full single-store config:
- *
- * interface User {
- * id: string;
- * name: string;
- * age: number;
- * }
- *
- * const userStore = createIndexDBStore({
- * dbName: "MyAppDB",
- * storeName: "users",
- * keyPath: "id",
- * version: 1,
- * sync: {
- * endpoint: "https://api.example.com/users",
- * async push(item, action) {
- * // simple example using fetch
- * if (action === "add") await fetch(this.endpoint!, { method: "POST", body: JSON.stringify(item) });
- * if (action === "update") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "PUT", body: JSON.stringify(item) });
- * if (action === "remove") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "DELETE" });
- * },
- * async pull() {
- * const res = await fetch("https://api.example.com/users");
- * return res.json();
- * },
- * interval: 15000,
- * autoSync: true,
- * onOffline: () => console.log("User store offline"),
- * onOnline: () => console.log("User store online"),
- * },
- * });
- *
- * // Multi-store manager usage with global + per-store callbacks:
- *
- * const db = new IndexDBManager("MyAppDB", 1, {
- * onOffline: () => console.log("Global offline"),
- * onOnline: () => console.log("Global online"),
- * });
- *
- * // per-store callbacks override global if provided
- * const users = db.createStore("users", "id", {
- * sync: {
- * onOffline: () => console.log("Users store offline"),
- * onOnline: () => console.log("Users store online"),
- * }
- * });
- *
- * const posts = db.createStore<{ id: string; title: string }>("posts", "id");
- *
- * await users.add({ id: "u1", name: "Joe", age: 22 });
- * await posts.add({ id: "p1", title: "Hello World" });
- *
- * users.subscribe(state => console.log("Users changed:", state.items));
- * posts.subscribe(state => console.log("Posts changed:", state.items));
- *
- * // derived example (watch one user)
- * const watchUser = users.deriveQuery(items => items.find(u => u.id === "u1") || null);
- * const unsubscribeWatch = watchUser.subscribe(u => console.log("u1 changed:", u));
- *
- * // manual transaction example
- * await users.transaction(tx => {
- * const s = tx.objectStore("users");
- * s.add({ id: "u2", name: "Ada", age: 45 });
- * });
- *
- * // Note: When running in SSR environment, store operations that touch IndexedDB are safe-guarded.
- *
- * -------------------------------------------------------------------------- */
+/**
+ * indexdb-store.ts
+ *
+ * Full implementation of createIndexDBStore + IndexDBManager with:
+ * - Full JSDoc
+ * - Multi-store manager
+ * - Optional sync adapter hooks
+ * - Global (manager) + per-store online/offline callbacks
+ * - SSR-safe checks
+ * - Preserves all existing methods and signatures
+ *
+ * Drop into your project. Typescript friendly.
+ */
+
+import { createStore } from "./store";
+
+/* --------------------------------------------------------------------------
+ * Types & Options
+ * -------------------------------------------------------------------------- */
+
+/**
+ * Live sync & network callback config (part of DBStoreOptions.sync)
+ */
+interface SyncOptions {
+ /** Base URL or remote DB client (optional helper) */
+ endpoint?: string;
+ /** Push local change to remote */
+ push?: (item: any, action: "add" | "update" | "remove") => Promise;
+ /** Pull data from remote */
+ pull?: () => Promise;
+ /** Auto-sync pull interval (ms) */
+ interval?: number;
+ /** Automatically push on local changes */
+ autoSync?: boolean;
+ /** Called when this store detects it's offline */
+ onOffline?: () => void;
+ /** Called when this store becomes online again */
+ onOnline?: () => void;
+}
+
+/**
+ * Configuration options for IndexedDB-backed reactive store.
+ */
+interface DBStoreOptions {
+ /** Database name (defaults to "default_db") */
+ dbName?: string;
+ /** Store name (required) */
+ storeName: string;
+ /** Key path for object store (defaults to "id") */
+ keyPath?: string;
+ /** Database version (defaults to 1) */
+ version?: number;
+ /** Automatically load data into store on init (default: true) */
+ autoLoad?: boolean;
+ /** Live sync configuration (optional) */
+ sync?: SyncOptions;
+}
+
+/** Filter type for query operations */
+type QueryFilter = Partial> | ((item: T) => boolean);
+
+/* --------------------------------------------------------------------------
+ * Utility: SSR-safe feature detection
+ * -------------------------------------------------------------------------- */
+const isBrowser =
+ typeof window !== "undefined" && typeof navigator !== "undefined";
+const supportsBroadcastChannel = typeof BroadcastChannel !== "undefined";
+
+/* --------------------------------------------------------------------------
+ * createIndexDBStore
+ * -------------------------------------------------------------------------- */
+
+/**
+ * Creates a reactive IndexedDB-backed store with ORM-like methods.
+ * Includes reactivity, syncing across tabs, live sync hooks, and offline detection.
+ *
+ * @template T - The record type stored in IndexedDB.
+ * @param {DBStoreOptions} options - Configuration for the store.
+ * @returns {object} Reactive data store with CRUD, query, and utility methods.
+ * @example
+ * // Create multiple stores
+ * const userStore = createIndexDBStore({storeName: 'users'});
+ */
+export function createIndexDBStore>(
+ options: DBStoreOptions
+) {
+ const {
+ dbName = "default_db",
+ storeName,
+ keyPath,
+ version = 1,
+ autoLoad = true,
+ sync,
+ } = options;
+
+ // main reactive store for items
+ const store = createStore<{ items: T[] }>({ items: [] });
+
+ // database reference and runtime state
+ let db: IDBDatabase | null = null;
+ let resolvedKeyPath: string | null = keyPath || null;
+
+ // cross-tab channel (guarded for SSR / old browsers)
+ const bc =
+ isBrowser && supportsBroadcastChannel
+ ? new BroadcastChannel(`${dbName}_${storeName}_sync`)
+ : null;
+
+ // network state
+ let isOffline = isBrowser ? !navigator.onLine : false;
+
+ /* ------------------------------------------------------------------------
+ * DB helpers
+ * ------------------------------------------------------------------------ */
+
+ /**
+ * Opens or upgrades the IndexedDB database.
+ * @returns {Promise} The database instance.
+ */
+ async function openDB(): Promise {
+ if (!isBrowser)
+ throw new Error("IndexedDB not available in this environment (SSR).");
+ return new Promise((resolve, reject) => {
+ const req = indexedDB.open(dbName, version);
+ req.onupgradeneeded = () => {
+ const _db = req.result;
+ if (!_db.objectStoreNames.contains(storeName)) {
+ _db.createObjectStore(storeName, {
+ keyPath: resolvedKeyPath || "id",
+ });
+ }
+ };
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ async function ensureDB() {
+ if (!db) db = await openDB();
+ return db!;
+ }
+
+ /** Initializes the store and loads all data if autoLoad is true. */
+ async function init() {
+ if (!isBrowser) return; // SSR-safe: do not attempt DB operations on server
+ db = await openDB();
+ if (autoLoad) {
+ const all = await getAll();
+ store.set({ items: all });
+ }
+ setupNetworkListeners();
+ setupAutoPull();
+ }
+
+ function ensureKeyPath(item: T) {
+ if (!resolvedKeyPath) {
+ const firstKey = Object.keys(item)[0];
+ resolvedKeyPath = firstKey;
+ }
+ return resolvedKeyPath!;
+ }
+
+ /* ------------------------------------------------------------------------
+ * CRUD
+ * ------------------------------------------------------------------------ */
+
+ /**
+ * Adds a new record to the store.
+ * @param {T} item - The record to add.
+ * @returns {Promise}
+ */
+ async function add(item: T) {
+ const dbRef = await ensureDB();
+ ensureKeyPath(item);
+ return new Promise((resolve, reject) => {
+ const tx = dbRef.transaction(storeName, "readwrite");
+ const req = tx.objectStore(storeName).add(item);
+ req.onsuccess = async () => {
+ store.update((s) => ({ items: [...s.items, item] }));
+ bc?.postMessage("sync");
+ await maybeSync(item, "add");
+ resolve();
+ };
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /**
+ * Updates an existing record by ID or object.
+ * @param {IDBValidKey | Partial} idOrItem - Record ID or object containing key.
+ * @param {Partial} [partial] - Partial data to merge when using ID form.
+ * @returns {Promise}
+ */
+ async function update(
+ idOrItem: IDBValidKey | Partial,
+ partial?: Partial
+ ) {
+ const dbRef = await ensureDB();
+
+ let id: IDBValidKey;
+ let newData: Partial = {};
+
+ const isKeyType =
+ typeof idOrItem === "string" ||
+ typeof idOrItem === "number" ||
+ idOrItem instanceof Date ||
+ idOrItem instanceof ArrayBuffer ||
+ ArrayBuffer.isView(idOrItem) ||
+ Array.isArray(idOrItem);
+
+ if (isKeyType) {
+ id = idOrItem as IDBValidKey;
+ newData = partial || {};
+ } else if (typeof idOrItem === "object" && idOrItem !== null) {
+ ensureKeyPath(idOrItem as T);
+ id = (idOrItem as any)[resolvedKeyPath!];
+ newData = idOrItem as Partial;
+ } else {
+ throw new Error("Invalid argument passed to update()");
+ }
+
+ const existing = await get(id);
+ if (!existing) return;
+
+ const updated = { ...existing, ...newData } as T;
+
+ return new Promise((resolve, reject) => {
+ const tx = dbRef.transaction(storeName, "readwrite");
+ const req = tx.objectStore(storeName).put(updated);
+ req.onsuccess = async () => {
+ store.update((s) => ({
+ items: s.items.map((i) =>
+ (i as any)[resolvedKeyPath!] === id ? updated : i
+ ),
+ }));
+ bc?.postMessage("sync");
+ await maybeSync(updated, "update");
+ resolve();
+ };
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /**
+ * Retrieves a record by ID.
+ * @param {IDBValidKey} id - Record key.
+ * @returns {Promise} The matching record, if found.
+ */
+ async function get(id: IDBValidKey): Promise {
+ const dbRef = await ensureDB();
+ return new Promise((resolve, reject) => {
+ const tx = dbRef.transaction(storeName, "readonly");
+ const req = tx.objectStore(storeName).get(id);
+ req.onsuccess = () => resolve(req.result as T | undefined);
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /**
+ * Retrieves all records.
+ * @returns {Promise} All stored records.
+ */
+ async function getAll(): Promise {
+ const dbRef = await ensureDB();
+ return new Promise((resolve, reject) => {
+ const tx = dbRef.transaction(storeName, "readonly");
+ const req = tx.objectStore(storeName).getAll();
+ req.onsuccess = () => resolve(req.result as T[]);
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /**
+ * Removes a record by ID.
+ * @param {IDBValidKey} id - Record key.
+ * @returns {Promise}
+ */
+ async function remove(id: IDBValidKey) {
+ const dbRef = await ensureDB();
+ const key = resolvedKeyPath || "id";
+ return new Promise((resolve, reject) => {
+ const tx = dbRef.transaction(storeName, "readwrite");
+ const req = tx.objectStore(storeName).delete(id);
+ req.onsuccess = async () => {
+ store.update((s) => ({
+ items: s.items.filter((i) => (i as any)[key] !== id),
+ }));
+ bc?.postMessage("sync");
+ await maybeSync({ [key]: id } as any, "remove");
+ resolve();
+ };
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /**
+ * Clears all data from the store.
+ * @returns {Promise}
+ */
+ async function clear() {
+ const dbRef = await ensureDB();
+ return new Promise((resolve, reject) => {
+ const tx = dbRef.transaction(storeName, "readwrite");
+ const req = tx.objectStore(storeName).clear();
+ req.onsuccess = () => {
+ store.set({ items: [] });
+ bc?.postMessage("sync");
+ resolve();
+ };
+ req.onerror = () => reject(req.error);
+ });
+ }
+
+ /* ------------------------------------------------------------------------
+ * Queries & Utilities
+ * ------------------------------------------------------------------------ */
+
+ async function where(filter: QueryFilter): Promise {
+ const all = await getAll();
+ if (typeof filter === "function") return all.filter(filter);
+ return all.filter((item) =>
+ Object.entries(filter).every(([k, v]) => item[k as keyof T] === v)
+ );
+ }
+
+ async function first(filter?: QueryFilter): Promise {
+ const res = filter ? await where(filter) : await getAll();
+ return res[0];
+ }
+
+ async function find(id: IDBValidKey): Promise {
+ return get(id);
+ }
+
+ /**
+ * Get the last matching record
+ * @example
+ * const lastAdult = await User.last(u => u.age >= 18);
+ */
+ async function last(filter?: QueryFilter): Promise {
+ const res = filter ? await where(filter) : await getAll();
+ return res[res.length - 1];
+ }
+
+ /**
+ * Count items matching filter
+ * @example
+ * const adultCount = await User.count(u => u.age >= 18);
+ */
+ async function count(filter?: QueryFilter): Promise {
+ const res = filter ? await where(filter) : await getAll();
+ return res.length;
+ }
+
+ /**
+ * Check if any record exists matching filter
+ * @example
+ * const exists = await User.exists({ name: "John" });
+ */
+ async function exists(filter: QueryFilter): Promise {
+ const res = await where(filter);
+ return res.length > 0;
+ }
+
+ /**
+ * Get a random record
+ * @example
+ * const randomUser = await User.random();
+ */
+ async function random(filter?: QueryFilter): Promise {
+ const res = filter ? await where(filter) : await getAll();
+ if (res.length === 0) return undefined;
+ return res[Math.floor(Math.random() * res.length)];
+ }
+
+ async function below(filter: Partial): Promise {
+ const all = await getAll();
+ return all.filter(item =>
+ Object.entries(filter).every(([k, v]) => {
+ const val = item[k as keyof T];
+ if (typeof val === "number" && typeof v === "number") return (val as number) < (v as number);
+ if (typeof val === "string" && typeof v === "string") return (val as string) < (v as string);
+ return false;
+ })
+ );
+}
+
+async function above(filter: Partial): Promise {
+ const all = await getAll();
+ return all.filter(item =>
+ Object.entries(filter).every(([k, v]) => {
+ const val = item[k as keyof T];
+ if (typeof val === "number" && typeof v === "number") return (val as number) > (v as number);
+ if (typeof val === "string" && typeof v === "string") return (val as string) > (v as string);
+ return false;
+ })
+ );
+}
+
+
+
+ async function query(predicate: (item: T) => boolean): Promise {
+ const all = await getAll();
+ return all.filter(predicate);
+ }
+
+ /**
+ * Creates a derived live query that reacts to store changes.
+ * @template U
+ * @param {(items: T[]) => U} selector - Function that selects part of the data.
+ * @returns {{ subscribe(fn: (value: U) => void): () => void; get(): U }}
+ */
+ function deriveQuery(selector: (items: T[]) => U) {
+ const live = createStore({ value: selector(store.get().items) });
+
+ store.watch(
+ (s) => s.items,
+ (items) => {
+ const result = selector(items);
+ live.set({ value: result });
+ }
+ );
+
+ return {
+ subscribe: (fn: (v: U) => void) => live.subscribe((s) => fn(s.value)),
+ get: () => live.get().value,
+ };
+ }
+
+ /* ------------------------------------------------------------------------
+ * Live sync & network listeners (per-store)
+ * ------------------------------------------------------------------------ */
+
+ async function maybeSync(item: any, action: "add" | "update" | "remove") {
+ // sync only if configured and online
+ if (
+ sync?.autoSync &&
+ typeof isBrowser !== "undefined" &&
+ navigator.onLine &&
+ sync.push
+ ) {
+ try {
+ await sync.push(item, action);
+ } catch (err) {
+ // swallow - caller may implement retry
+ console.warn(
+ `[createIndexDBStore:${storeName}] sync push failed:`,
+ err
+ );
+ }
+ }
+ }
+
+ function setupAutoPull() {
+ if (!isBrowser) return;
+ if (sync?.interval && sync.pull) {
+ setInterval(async () => {
+ if (!navigator.onLine) return;
+ try {
+ const remote = await sync.pull!();
+ if (remote) {
+ store.set({ items: remote as T[] });
+ }
+ } catch (err) {
+ console.warn(
+ `[createIndexDBStore:${storeName}] sync pull failed:`,
+ err
+ );
+ }
+ }, sync.interval);
+ }
+ }
+
+ function setupNetworkListeners() {
+ if (!isBrowser) return;
+
+ // offline
+ window.addEventListener("offline", () => {
+ isOffline = true;
+ try {
+ sync?.onOffline?.();
+ } catch (err) {
+ console.error("onOffline callback error:", err);
+ }
+ // Trigger store subscribers reactively (announce network change)
+ store.update((s) => ({ items: s.items }));
+ });
+
+ // online
+ window.addEventListener("online", async () => {
+ const wasOffline = isOffline;
+ isOffline = false;
+ try {
+ sync?.onOnline?.();
+ } catch (err) {
+ console.error("onOnline callback error:", err);
+ }
+ // If we were offline and now online, optionally pull latest
+ if (wasOffline && sync?.pull) {
+ try {
+ const remote = await sync.pull();
+ if (remote) store.set({ items: remote as T[] });
+ } catch (err) {
+ console.warn(
+ `[createIndexDBStore:${storeName}] sync pull after online failed:`,
+ err
+ );
+ }
+ }
+ // notify subscribers (reactive)
+ store.update((s) => ({ items: s.items }));
+ });
+ }
+
+ // cross-tab broadcast listener (keeps store in sync across tabs)
+ if (bc) {
+ bc.onmessage = async (msg) => {
+ if (msg.data === "sync") {
+ const all = await getAll();
+ store.set({ items: all });
+ }
+ };
+ }
+
+ /* ------------------------------------------------------------------------
+ * Transactions
+ * ------------------------------------------------------------------------ */
+
+ /**
+ * Runs a custom transaction operation.
+ * @param {(tx: IDBTransaction) => void} fn - Transaction callback.
+ * @returns {Promise}
+ */
+ async function transaction(fn: (tx: IDBTransaction) => void) {
+ const dbRef = await ensureDB();
+ const tx = dbRef.transaction(storeName, "readwrite");
+ fn(tx);
+ return new Promise((resolve, reject) => {
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+ }
+
+ // initialize
+ init();
+
+ /* ------------------------------------------------------------------------
+ * Public API (do not remove these keys — kept for compatibility)
+ * ------------------------------------------------------------------------ */
+ return {
+ add,
+ update,
+ get,
+ getAll,
+ remove,
+ clear,
+ query,
+ subscribe: store.subscribe,
+ watch: store.watch,
+ deriveQuery,
+ where,
+ first,
+ find,
+ put: update,
+ transaction,
+ random,
+ exists,
+ count,
+ last,
+ above,
+ below
+ };
+}
+
+/* --------------------------------------------------------------------------
+ * IndexDBManager — multi-store manager with global network callbacks
+ * -------------------------------------------------------------------------- */
+
+/**
+ * Options for IndexDBManager constructor (global network callbacks).
+ */
+interface IndexDBManagerOptions {
+ onOffline?: () => void;
+ onOnline?: () => void;
+}
+
+/**
+ * A lightweight IndexedDB manager that can create and manage multiple stores.
+ * Supports optional global onOnline/onOffline callbacks. Each store also supports
+ * per-store callbacks via its DBStoreOptions.sync.onOnline/onOffline.
+ */
+export class IndexDBManager {
+ private dbName: string;
+ private version: number;
+ private stores: Record>> =
+ {};
+ private globalOnOffline?: () => void;
+ private globalOnOnline?: () => void;
+
+ /**
+ * Create a manager for a named IndexedDB database.
+ * @param {string} dbName - database name
+ * @param {number} [version=1] - db version
+ * @param {IndexDBManagerOptions} [opts] - global network callbacks
+ */
+ constructor(
+ dbName: string,
+ version: number = 1,
+ opts?: IndexDBManagerOptions
+ ) {
+ this.dbName = dbName;
+ this.version = version;
+ this.globalOnOffline = opts?.onOffline;
+ this.globalOnOnline = opts?.onOnline;
+
+ // If running in browser, hook into global online/offline to call manager-level hooks.
+ if (isBrowser) {
+ window.addEventListener("offline", () => {
+ try {
+ this.globalOnOffline?.();
+ } catch (err) {
+ console.error("IndexDBManager.onOffline error:", err);
+ }
+ });
+ window.addEventListener("online", () => {
+ try {
+ this.globalOnOnline?.();
+ } catch (err) {
+ console.error("IndexDBManager.onOnline error:", err);
+ }
+ });
+ }
+ }
+
+ /**
+ * Creates a new store within the database.
+ *
+ * @template T
+ * @param {string} storeName - The name of the object store.
+ * @param {string} [keyPath='id'] - The key path used as the primary key.
+ * @param {{ sync?: SyncOptions }} [opts] - Optional per-store additions (e.g., per-store onOnline/onOffline).
+ * @returns {ReturnType>} The created store instance.
+ *
+ * @example
+ * // Create multiple stores
+ * const users = db.createStore("users", "id");
+ * const posts = db.createStore("posts", "id");
+ */
+ createStore>(
+ storeName: string,
+ keyPath: string = "id",
+ opts?: { sync?: SyncOptions }
+ ) {
+ // merge global and per-store network callbacks: per-store takes precedence
+ const mergedSync: SyncOptions | undefined = opts?.sync
+ ? {
+ ...opts.sync,
+ onOffline: opts.sync.onOffline ?? this.globalOnOffline,
+ onOnline: opts.sync.onOnline ?? this.globalOnOnline,
+ }
+ : this.globalOnOffline || this.globalOnOnline
+ ? { onOffline: this.globalOnOffline, onOnline: this.globalOnOnline }
+ : undefined;
+
+ const store = createIndexDBStore({
+ dbName: this.dbName,
+ storeName,
+ keyPath,
+ version: this.version,
+ sync: mergedSync,
+ });
+ this.stores[storeName] = store;
+ return store;
+ }
+
+ /**
+ * Retrieves a store by its name.
+ * @param {string} name - The store name.
+ * @returns {ReturnType | undefined} The store instance, if found.
+ */
+ getStore(name: string) {
+ return this.stores[name];
+ }
+}
+
+/* --------------------------------------------------------------------------
+ * 📘 Example Usage (merged, minimal + full + multi-store)
+ * --------------------------------------------------------------------------
+ *
+ * Minimal:
+ *
+ * const userStore = createIndexDBStore({
+ * storeName: 'users',
+ * });
+ *
+ * await userStore.add({ id: 'u1', name: 'Joseph', age: 22 });
+ * const all = await userStore.getAll();
+ * console.log(all);
+ *
+ *
+ * Full single-store config:
+ *
+ * interface User {
+ * id: string;
+ * name: string;
+ * age: number;
+ * }
+ *
+ * const userStore = createIndexDBStore({
+ * dbName: "MyAppDB",
+ * storeName: "users",
+ * keyPath: "id",
+ * version: 1,
+ * sync: {
+ * endpoint: "https://api.example.com/users",
+ * async push(item, action) {
+ * // simple example using fetch
+ * if (action === "add") await fetch(this.endpoint!, { method: "POST", body: JSON.stringify(item) });
+ * if (action === "update") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "PUT", body: JSON.stringify(item) });
+ * if (action === "remove") await fetch(`${this.endpoint}/${(item as any).id}`, { method: "DELETE" });
+ * },
+ * async pull() {
+ * const res = await fetch("https://api.example.com/users");
+ * return res.json();
+ * },
+ * interval: 15000,
+ * autoSync: true,
+ * onOffline: () => console.log("User store offline"),
+ * onOnline: () => console.log("User store online"),
+ * },
+ * });
+ *
+ * // Multi-store manager usage with global + per-store callbacks:
+ *
+ * const db = new IndexDBManager("MyAppDB", 1, {
+ * onOffline: () => console.log("Global offline"),
+ * onOnline: () => console.log("Global online"),
+ * });
+ *
+ * // per-store callbacks override global if provided
+ * const users = db.createStore("users", "id", {
+ * sync: {
+ * onOffline: () => console.log("Users store offline"),
+ * onOnline: () => console.log("Users store online"),
+ * }
+ * });
+ *
+ * const posts = db.createStore<{ id: string; title: string }>("posts", "id");
+ *
+ * await users.add({ id: "u1", name: "Joe", age: 22 });
+ * await posts.add({ id: "p1", title: "Hello World" });
+ *
+ * users.subscribe(state => console.log("Users changed:", state.items));
+ * posts.subscribe(state => console.log("Posts changed:", state.items));
+ *
+ * // derived example (watch one user)
+ * const watchUser = users.deriveQuery(items => items.find(u => u.id === "u1") || null);
+ * const unsubscribeWatch = watchUser.subscribe(u => console.log("u1 changed:", u));
+ *
+ * // manual transaction example
+ * await users.transaction(tx => {
+ * const s = tx.objectStore("users");
+ * s.add({ id: "u2", name: "Ada", age: 45 });
+ * });
+ *
+ * // Note: When running in SSR environment, store operations that touch IndexedDB are safe-guarded.
+ *
+ * -------------------------------------------------------------------------- */
diff --git a/src/createFileRoutes.ts b/src/createFileRoutes.ts
index a117e02..401bca1 100644
--- a/src/createFileRoutes.ts
+++ b/src/createFileRoutes.ts
@@ -1,30 +1,30 @@
-import type { Component } from "ripple";
-import type { Route } from "./types";
-
-
-
-let NotFoundComponent: Component;
-
-
-export function createFileRoutes(modules: any[]): Route[] {
- // @ts-ignore
- ///const modules = import.meta.glob("/src/pages/**/*.ripple", { eager: true });
- NotFoundComponent = modules["/src/pages/notfound.ripple"]?.default
-
- const routes: Route[] = Object.entries(modules).map(([file, mod]) => {
- const path = file
- .replace("/src/pages", "")
- .replace(/index\.ripple$/, "")
- .replace(/\.ripple$/, "")
- .replace(/\[(.+?)\]/g, ":$1");
-
- return {
- path: path || "/",
- component: (mod as { default: Component }).default,
- };
- });
-
- routes.push({ path: "*", component: NotFoundComponent });
- return routes;
-}
-
+import type { Component } from "ripple";
+import type { Route } from "./types";
+
+
+
+let NotFoundComponent: Component;
+
+
+export function createFileRoutes(modules: any[]): Route[] {
+ // @ts-ignore
+ ///const modules = import.meta.glob("/src/pages/**/*.ripple", { eager: true });
+ NotFoundComponent = modules["/src/pages/notfound.ripple"]?.default
+
+ const routes: Route[] = Object.entries(modules).map(([file, mod]) => {
+ const path = file
+ .replace("/src/pages", "")
+ .replace(/index\.ripple$/, "")
+ .replace(/\.ripple$/, "")
+ .replace(/\[(.+?)\]/g, ":$1");
+
+ return {
+ path: path || "/",
+ component: (mod as { default: Component }).default,
+ };
+ });
+
+ routes.push({ path: "*", component: NotFoundComponent });
+ return routes;
+}
+
diff --git a/src/globe.ts b/src/globe.ts
index 14066fd..6564186 100644
--- a/src/globe.ts
+++ b/src/globe.ts
@@ -1,9 +1,9 @@
-
-
-
-export function loadPages(): any {
- // @ts-ignore
- const modules = import.meta.glob("/src/pages/**/*.ripple", { eager: true });
-return modules
-
+
+
+
+export function loadPages(): any {
+ // @ts-ignore
+ const modules = import.meta.glob("/src/pages/**/*.ripple", { eager: true });
+return modules
+
}
\ No newline at end of file
diff --git a/src/index.d.ts b/src/index.d.ts
index 81ab558..b7a7741 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -1,21 +1,21 @@
-declare module "rivra" {
-import type { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
- import type { Component } from "ripple";
-
- export interface LinkProps {
- href: string;
- className?: string;
- onLoading?: () => void;
- loadingComponent?: Component;
- emitEvent?: boolean;
- children?: any;
- queries?: Record;
- }
-
- export const Link: Component;
- export const Router: Component;
- export const PageRoutes: Component;
- export type Req = FastifyRequest;
-export type Reply = FastifyReply;
-export type App = FastifyInstance;
-}
+declare module "rivra" {
+import type { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
+ import type { Component } from "ripple";
+
+ export interface LinkProps {
+ href: string;
+ className?: string;
+ onLoading?: () => void;
+ loadingComponent?: Component;
+ emitEvent?: boolean;
+ children?: any;
+ queries?: Record;
+ }
+
+ export const Link: Component;
+ export const Router: Component;
+ export const PageRoutes: Component;
+ export type Req = FastifyRequest;
+export type Reply = FastifyReply;
+export type App = FastifyInstance;
+}
diff --git a/src/index.ts b/src/index.ts
index 53b3d37..b721316 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,13 +1,13 @@
-// export * from "./createFileRoutes";
-export * from "./types";
-export * from "./stores";
-export * from "./router"
-
-
-
-import type { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
-
-export type Req = FastifyRequest;
-export type Reply = FastifyReply;
-export type App = FastifyInstance;
-
+// export * from "./createFileRoutes";
+export * from "./types";
+export * from "./stores";
+export * from "./router"
+
+
+
+import type { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
+
+export type Req = FastifyRequest;
+export type Reply = FastifyReply;
+export type App = FastifyInstance;
+
diff --git a/src/packages/build.ts b/src/packages/build.ts
new file mode 100644
index 0000000..041802a
--- /dev/null
+++ b/src/packages/build.ts
@@ -0,0 +1,7 @@
+import { build } from "vite";
+
+export async function buildProject() {
+ console.log("📦 Building Rivra project...");
+ await build();
+ console.log("✅ Build completed.");
+}
diff --git a/src/packages/createApp.ts b/src/packages/createApp.ts
new file mode 100644
index 0000000..d29600a
--- /dev/null
+++ b/src/packages/createApp.ts
@@ -0,0 +1,27 @@
+import fs from "fs";
+import path from "path";
+
+export async function createApp(name: string) {
+ const dest = path.resolve(process.cwd(), name);
+
+ if (fs.existsSync(dest)) {
+ console.log(`❌ Folder '${name}' already exists.`);
+ return;
+ }
+
+ fs.mkdirSync(dest);
+
+ // Copy template (you can adjust this)
+ const templateDir = path.resolve(__dirname, "../../templates/app");
+
+ if (fs.existsSync(templateDir)) {
+ for (const file of fs.readdirSync(templateDir)) {
+ fs.copyFileSync(
+ path.join(templateDir, file),
+ path.join(dest, file)
+ );
+ }
+ }
+
+ console.log(`🎉 Rivra project '${name}' created.`);
+}
diff --git a/src/packages/devServer.ts b/src/packages/devServer.ts
new file mode 100644
index 0000000..5f92351
--- /dev/null
+++ b/src/packages/devServer.ts
@@ -0,0 +1,6 @@
+import { StartServer } from "../../packages/server";
+
+export async function startDevServer() {
+ console.log("🔧 Starting Rivra dev server...");
+ await StartServer({ dev: true });
+}
diff --git a/src/rcomponents/link.d.ts b/src/rcomponents/link.d.ts
index 202c8ef..0ce8699 100644
--- a/src/rcomponents/link.d.ts
+++ b/src/rcomponents/link.d.ts
@@ -1,26 +1,26 @@
-import type { Component } from "ripple";
-export * from "../rcomponents/link.ripple"
-export *from "../rcomponents/page_routes.ripple"
-export * from "../rcomponents/router.ripple"
-declare module "./rcomponents/link.ripple" {
- export default function Link(
- href: string,
- children: Component,
- emitEvent?: boolean,
- onLoading?: () => void,
- loadingComponent?: Component,
- className?: string,
- queries?: Record
- ): void;
-}
-
-
- export declare function Link(
- href: string,
- children: Component,
- emitEvent?: boolean,
- onLoading?: () => void,
- loadingComponent?: Component,
- className?: string,
- queries?: Record
+import type { Component } from "ripple";
+export * from "../rcomponents/link.ripple"
+export *from "../rcomponents/page_routes.ripple"
+export * from "../rcomponents/router.ripple"
+declare module "./rcomponents/link.ripple" {
+ export default function Link(
+ href: string,
+ children: Component,
+ emitEvent?: boolean,
+ onLoading?: () => void,
+ loadingComponent?: Component,
+ className?: string,
+ queries?: Record
+ ): void;
+}
+
+
+ export declare function Link(
+ href: string,
+ children: Component,
+ emitEvent?: boolean,
+ onLoading?: () => void,
+ loadingComponent?: Component,
+ className?: string,
+ queries?: Record
): void;
\ No newline at end of file
diff --git a/src/rcomponents/link.ripple b/src/rcomponents/link.ripple
index 74b3a2f..b3b85ab 100644
--- a/src/rcomponents/link.ripple
+++ b/src/rcomponents/link.ripple
@@ -1,78 +1,78 @@
-import type { Component } from 'ripple';
-import { useRouter } from '../userouter';
-import { track, effect } from 'ripple';
-import { LinkProps } from "../types.ts";
-
-/**
- * @component
- * A navigation component for client-side routing.
- *
- * @param {string} href - The URL path to navigate to.
- * @param {Component} children - The content to render inside the link.
- * @param {() => void} [onLoading] - Optional callback triggered when navigation starts.
- * @param {string} [className] - Additional class names for styling.
- * @param {Component} [loadingComponent] - Optional component to display while loading.
- * @param {boolean} [emitEvent=true] - Whether to emit route events during navigation.
- * @param {Record} [queries] - Optional query parameters for URLSearch.
- *
- * @example
- * console.log('Loading...')}>
- * {"Go to Dashboard"}
- *
- */
-export component Link({
- href,
- children,
- onLoading,
- loadingComponent,
- className,
- queries,
- emitEvent = true,
-}: LinkProps) {
- const router = useRouter();
- let isLoading = track(false); // reactive per-Link loading state
- const currentUrl = track(router.path); // track current path
-
-
-
-const handleClick = (e: MouseEvent) => {
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
- e.preventDefault();
-
- if (!href) return;
-
- // handle in-page anchor links
- if (href.startsWith("#")) {
- const el = document.querySelector(href);
- if (el) {
- // scroll to section smoothly
- el.scrollIntoView({ behavior: "smooth" });
-
- // update hash in URL without page reload
- history.pushState({}, "", href);
- }
- return;
- }
-
- if (@currentUrl === href) {
- window.scrollTo(0, {behavior: "auto" })
- return
- };
- if (onLoading) onLoading();
-
- window.scrollTo(0, {behavior: "auto" })
- router.push(href, emitEvent, false, queries);
-};
-
-
-
-
-
-
-
- if (@isLoading && loadingComponent) {
-
- }
-}
-
-export default Link;
+import type { Component } from 'ripple';
+import { useRouter } from '../userouter';
+import { track, effect } from 'ripple';
+import { LinkProps } from "../types.ts";
+
+/**
+ * @component
+ * A navigation component for client-side routing.
+ *
+ * @param {string} href - The URL path to navigate to.
+ * @param {Component} children - The content to render inside the link.
+ * @param {() => void} [onLoading] - Optional callback triggered when navigation starts.
+ * @param {string} [className] - Additional class names for styling.
+ * @param {Component} [loadingComponent] - Optional component to display while loading.
+ * @param {boolean} [emitEvent=true] - Whether to emit route events during navigation.
+ * @param {Record} [queries] - Optional query parameters for URLSearch.
+ *
+ * @example
+ * console.log('Loading...')}>
+ * {"Go to Dashboard"}
+ *
+ */
+export component Link({
+ href,
+ children,
+ onLoading,
+ loadingComponent,
+ className,
+ queries,
+ emitEvent = true,
+}: LinkProps) {
+ const router = useRouter();
+ let isLoading = track(false); // reactive per-Link loading state
+ const currentUrl = track(router.path); // track current path
+
+
+
+const handleClick = (e: MouseEvent) => {
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
+ e.preventDefault();
+
+ if (!href) return;
+
+ // handle in-page anchor links
+ if (href.startsWith("#")) {
+ const el = document.querySelector(href);
+ if (el) {
+ // scroll to section smoothly
+ el.scrollIntoView({ behavior: "smooth" });
+
+ // update hash in URL without page reload
+ history.pushState({}, "", href);
+ }
+ return;
+ }
+
+ if (@currentUrl === href) {
+ window.scrollTo(0, {behavior: "auto" })
+ return
+ };
+ if (onLoading) onLoading();
+
+ window.scrollTo(0, {behavior: "auto" })
+ router.push(href, emitEvent, false, queries);
+};
+
+
+
+
+
+
+
+ if (@isLoading && loadingComponent) {
+
+ }
+}
+
+export default Link;
diff --git a/src/rcomponents/notfound.ripple b/src/rcomponents/notfound.ripple
index 37255bd..b2d54f7 100644
--- a/src/rcomponents/notfound.ripple
+++ b/src/rcomponents/notfound.ripple
@@ -1,67 +1,67 @@
-// import Link from "./link.ripple"
-export default component NotFound404() {
-
-
-
- {"40"}{"4"}
-
-
{"Oops! The page you are looking for does not exist."}
-
{"Go Home"}
-
-
-
-}
+// import Link from "./link.ripple"
+export default component NotFound404() {
+
+
+
+ {"40"}{"4"}
+
+
{"Oops! The page you are looking for does not exist."}
+
{"Go Home"}
+
+
+
+}
diff --git a/src/rcomponents/page_routes.d.ts b/src/rcomponents/page_routes.d.ts
index 426e059..d508e7a 100644
--- a/src/rcomponents/page_routes.d.ts
+++ b/src/rcomponents/page_routes.d.ts
@@ -1,15 +1,15 @@
-// page_routes.ripple.d.ts
-import type { Component } from "ripple"; // if needed
-
-declare module "./rcomponents/page_routes.ripple" {
- export default function PageRoutes(
- modules?: any,
- enableLoader?: boolean,
- ): void;
-}
-
-
- export declare function PageRoutes(
- modules?: any,
- enableLoader?: boolean,
+// page_routes.ripple.d.ts
+import type { Component } from "ripple"; // if needed
+
+declare module "./rcomponents/page_routes.ripple" {
+ export default function PageRoutes(
+ modules?: any,
+ enableLoader?: boolean,
+ ): void;
+}
+
+
+ export declare function PageRoutes(
+ modules?: any,
+ enableLoader?: boolean,
): void;
\ No newline at end of file
diff --git a/src/rcomponents/page_routes.ripple b/src/rcomponents/page_routes.ripple
index 5540bd0..e460be1 100644
--- a/src/rcomponents/page_routes.ripple
+++ b/src/rcomponents/page_routes.ripple
@@ -1,19 +1,19 @@
-import { useRouter } from '../userouter';
-import { createFileRoutes } from "../createFileRoutes";
-import { Router } from "./router.ripple";
-
-
-interface Props {
- enableLoader?: boolean
- modules: any[]
-}
-
-export component PageRoutes({modules, enableLoader = true}: Props){
- const routes = createFileRoutes(modules);
-
-
-
-}
-
-
+import { useRouter } from '../userouter';
+import { createFileRoutes } from "../createFileRoutes";
+import { Router } from "./router.ripple";
+
+
+interface Props {
+ enableLoader?: boolean
+ modules: any[]
+}
+
+export component PageRoutes({modules, enableLoader = true}: Props){
+ const routes = createFileRoutes(modules);
+
+
+
+}
+
+
export default PageRoutes
\ No newline at end of file
diff --git a/src/rcomponents/router.ripple b/src/rcomponents/router.ripple
index de2bf91..ec07492 100644
--- a/src/rcomponents/router.ripple
+++ b/src/rcomponents/router.ripple
@@ -1,53 +1,53 @@
-import type { Component } from "ripple";
-import { effect, track } from "ripple";
-import { useRouter } from "../userouter";
-import type { Route, RouterProp } from "../types";
-
-export component Router({ routes }: RouterProp) {
- const router = useRouter();
-
- let currentPath = track(null);
- let matched = track(null);
-
- // Client-only logic in an effect
- effect(() => {
- if (typeof window === "undefined") return;
-
- @currentPath = window.location.pathname;
-
- const handler = () => {
- @currentPath = window.location.pathname;
- @matched = null;
- };
-
- window.addEventListener("popstate", handler);
- window.addEventListener("pushstate", handler);
- window.addEventListener("replacestate", handler);
-
- return () => {
- window.removeEventListener("popstate", handler);
- window.removeEventListener("pushstate", handler);
- window.removeEventListener("replacestate", handler);
- };
- });
-
- // Recompute matched route when path changes
- effect(() => {
- if (@currentPath === null) return;
- @matched = routes.find(r => router.match(r.path, @currentPath.toString())) || null;
- });
-
- // Always render a template, SSR-safe
- if (@matched) {
- const Comp = @matched.component;
-
- } else {
-
- }
-}
-
-component DefaultNotFound() {
- {"404 Page not found"}
-}
-
-export default Router;
+import type { Component } from "ripple";
+import { effect, track } from "ripple";
+import { useRouter } from "../userouter";
+import type { Route, RouterProp } from "../types";
+
+export component Router({ routes }: RouterProp) {
+ const router = useRouter();
+
+ let currentPath = track(null);
+ let matched = track(null);
+
+ // Client-only logic in an effect
+ effect(() => {
+ if (typeof window === "undefined") return;
+
+ @currentPath = window.location.pathname;
+
+ const handler = () => {
+ @currentPath = window.location.pathname;
+ @matched = null;
+ };
+
+ window.addEventListener("popstate", handler);
+ window.addEventListener("pushstate", handler);
+ window.addEventListener("replacestate", handler);
+
+ return () => {
+ window.removeEventListener("popstate", handler);
+ window.removeEventListener("pushstate", handler);
+ window.removeEventListener("replacestate", handler);
+ };
+ });
+
+ // Recompute matched route when path changes
+ effect(() => {
+ if (@currentPath === null) return;
+ @matched = routes.find(r => router.match(r.path, @currentPath.toString())) || null;
+ });
+
+ // Always render a template, SSR-safe
+ if (@matched) {
+ const Comp = @matched.component;
+
+ } else {
+
+ }
+}
+
+component DefaultNotFound() {
+ {"404 Page not found"}
+}
+
+export default Router;
diff --git a/src/rcomponents/style.css b/src/rcomponents/style.css
index be07ea9..3753bdb 100644
--- a/src/rcomponents/style.css
+++ b/src/rcomponents/style.css
@@ -1,23 +1,23 @@
-.loader {
- position: fixed;
- top: 0;
- left: 0;
- height: 3px;
- z-index: 9999;
- border-radius: 2px;
- background: linear-gradient(270deg, #1e90ff, #ff1493, #00ff7f);
- background-size: 600% 600%;
- transition: width 0.15s linear;
-
- /* blink/gradient animation */
- animation-name: progress-blink;
- animation-duration: 1.5s;
- animation-timing-function: ease;
- animation-iteration-count: infinite;
-
- /* opacity animation */
- animation-name: fade-in-out; /* this will override the previous one if not careful */
- animation-duration: 0.8s;
- animation-timing-function: ease-in-out;
- animation-iteration-count: infinite;
-}
+.loader {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 3px;
+ z-index: 9999;
+ border-radius: 2px;
+ background: linear-gradient(270deg, #1e90ff, #ff1493, #00ff7f);
+ background-size: 600% 600%;
+ transition: width 0.15s linear;
+
+ /* blink/gradient animation */
+ animation-name: progress-blink;
+ animation-duration: 1.5s;
+ animation-timing-function: ease;
+ animation-iteration-count: infinite;
+
+ /* opacity animation */
+ animation-name: fade-in-out; /* this will override the previous one if not careful */
+ animation-duration: 0.8s;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: infinite;
+}
diff --git a/src/ripple.d.ts b/src/ripple.d.ts
index 168b1ca..8100320 100644
--- a/src/ripple.d.ts
+++ b/src/ripple.d.ts
@@ -1,5 +1,5 @@
-declare module "*.ripple" {
- import type { Component } from "ripple";
- const component: Component;
- export default component;
-}
+declare module "*.ripple" {
+ import type { Component } from "ripple";
+ const component: Component;
+ export default component;
+}
diff --git a/src/router/index.d.ts b/src/router/index.d.ts
index c3670a7..43397e1 100644
--- a/src/router/index.d.ts
+++ b/src/router/index.d.ts
@@ -1,92 +1,92 @@
-import type { Component } from "ripple";
-export declare const Router: (props: {
- routes: any;
-}) => void;
-/**
- * A navigation component for client-side routing.
- *
- * @param {string} href - The URL path to navigate to.
- * @param {Component} children - The content to render inside the link.
- * @param {() => void} [onLoading] - Optional callback triggered when navigation starts.
- * @param {string} [className] - Additional class names for styling.
- * @param {Component} [loadingComponent] - Optional component to display while loading.
- * @param {boolean} [emitEvent=true] - Whether to emit route events during navigation.
- * @param {Record} [queries] - Optional query parameters for URLSearch.
- *
- * @example
- * console.log('Loading...')}>
- * {"Go to Dashboard"}
- *
- */
-declare const Link: (props: {
- href: string;
- children: Component;
- emitEvent?: boolean;
- onLoading?: () => void;
- loadingComponent?: Component;
- className?: string;
- queries?: Record;
-}) => void;
-export declare const PageRoutes: (props: {
- modules?: any;
- enableLoader?: boolean;
-}) => void;
-export { Link };
-
-
-
-
-
- export interface InuseRouter {
- readonly path: string;
- readonly asPath: string;
- readonly queries: Record;
- readonly params: Record;
- readonly loading: boolean;
-
- push(
- url: string,
- announce?: boolean,
- shallow?: boolean,
- queries?: Record
- ): void;
-
- replace(
- url: string,
- announce?: boolean,
- shallow?: boolean,
- queries?: Record
- ): void;
-
- back(announce?: boolean): void;
-
- match(pattern: string, currentPath: string): true | null;
- on(type: "start" | "complete" | "change", cb: (path: string) => void): () => void;
- beforePopState(cb: (url: string) => boolean | void): () => void;
- prefetch(url: string): Promise;
- resolveHref(url: string): string;
- isActive(url: string): boolean;
-
- readonly host: string;
- readonly hostname: string;
- readonly protocol: string;
- readonly origin: string;
- readonly port: string;
- readonly href: string;
- readonly hash: string;
- readonly search: string;
- }
-
- /**
- * useRouter hook
- *
- * Provides client-side navigation and routing utilities.
- *
- * @example
- * ```ts
- * import { useRouter } from "rivra/router";
- * const router = useRouter();
- * router.push("/about");
- * ```
- */
+import type { Component } from "ripple";
+export declare const Router: (props: {
+ routes: any;
+}) => void;
+/**
+ * A navigation component for client-side routing.
+ *
+ * @param {string} href - The URL path to navigate to.
+ * @param {Component} children - The content to render inside the link.
+ * @param {() => void} [onLoading] - Optional callback triggered when navigation starts.
+ * @param {string} [className] - Additional class names for styling.
+ * @param {Component} [loadingComponent] - Optional component to display while loading.
+ * @param {boolean} [emitEvent=true] - Whether to emit route events during navigation.
+ * @param {Record} [queries] - Optional query parameters for URLSearch.
+ *
+ * @example
+ * console.log('Loading...')}>
+ * {"Go to Dashboard"}
+ *
+ */
+declare const Link: (props: {
+ href: string;
+ children: Component;
+ emitEvent?: boolean;
+ onLoading?: () => void;
+ loadingComponent?: Component;
+ className?: string;
+ queries?: Record;
+}) => void;
+export declare const PageRoutes: (props: {
+ modules?: any;
+ enableLoader?: boolean;
+}) => void;
+export { Link };
+
+
+
+
+
+ export interface InuseRouter {
+ readonly path: string;
+ readonly asPath: string;
+ readonly queries: Record;
+ readonly params: Record;
+ readonly loading: boolean;
+
+ push(
+ url: string,
+ announce?: boolean,
+ shallow?: boolean,
+ queries?: Record
+ ): void;
+
+ replace(
+ url: string,
+ announce?: boolean,
+ shallow?: boolean,
+ queries?: Record
+ ): void;
+
+ back(announce?: boolean): void;
+
+ match(pattern: string, currentPath: string): true | null;
+ on(type: "start" | "complete" | "change", cb: (path: string) => void): () => void;
+ beforePopState(cb: (url: string) => boolean | void): () => void;
+ prefetch(url: string): Promise;
+ resolveHref(url: string): string;
+ isActive(url: string): boolean;
+
+ readonly host: string;
+ readonly hostname: string;
+ readonly protocol: string;
+ readonly origin: string;
+ readonly port: string;
+ readonly href: string;
+ readonly hash: string;
+ readonly search: string;
+ }
+
+ /**
+ * useRouter hook
+ *
+ * Provides client-side navigation and routing utilities.
+ *
+ * @example
+ * ```ts
+ * import { useRouter } from "rivra/router";
+ * const router = useRouter();
+ * router.push("/about");
+ * ```
+ */
export function useRouter(): InuseRouter;
\ No newline at end of file
diff --git a/src/router/index.ts b/src/router/index.ts
index 86fea03..d8eaae1 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,53 +1,53 @@
-import type { Component } from "ripple";
-import _Router from "../rcomponents/router.ripple";
-import _PageRoutes from "../rcomponents/page_routes.ripple" ;
-import _Link from "../rcomponents/link.ripple";
-
-
-export { useRouter } from "../userouter";
-
-
-
-
-export const Router = _Router as unknown as (
- props: {
- routes: any
- }
-) => void
-
-/**
- * A navigation component for client-side routing.
- *
- * @param {string} href - The URL path to navigate to.
- * @param {Component} children - The content to render inside the link.
- * @param {() => void} [onLoading] - Optional callback triggered when navigation starts.
- * @param {string} [className] - Additional class names for styling.
- * @param {Component} [loadingComponent] - Optional component to display while loading.
- * @param {boolean} [emitEvent=true] - Whether to emit route events during navigation.
- * @param {Record} [queries] - Optional query parameters for URLSearch.
- *
- * @example
- * console.log('Loading...')}>
- * {"Go to Dashboard"}
- *
- */
- const Link = _Link as unknown as (
- props: {
- href: string;
- children: Component;
- emitEvent?: boolean;
- onLoading?: () => void;
- loadingComponent?: Component;
- className?: string;
- queries?: Record;
- }
-) => void;
-
- export const PageRoutes = _PageRoutes as unknown as (
- props: {
- modules?: any,
- enableLoader?: boolean
-}
- ) => void ;
-
-export { Link}
+import type { Component } from "ripple";
+import _Router from "../rcomponents/router.ripple";
+import _PageRoutes from "../rcomponents/page_routes.ripple" ;
+import _Link from "../rcomponents/link.ripple";
+
+
+export { useRouter } from "../userouter";
+
+
+
+
+export const Router = _Router as unknown as (
+ props: {
+ routes: any
+ }
+) => void
+
+/**
+ * A navigation component for client-side routing.
+ *
+ * @param {string} href - The URL path to navigate to.
+ * @param {Component} children - The content to render inside the link.
+ * @param {() => void} [onLoading] - Optional callback triggered when navigation starts.
+ * @param {string} [className] - Additional class names for styling.
+ * @param {Component} [loadingComponent] - Optional component to display while loading.
+ * @param {boolean} [emitEvent=true] - Whether to emit route events during navigation.
+ * @param {Record} [queries] - Optional query parameters for URLSearch.
+ *
+ * @example
+ * console.log('Loading...')}>
+ * {"Go to Dashboard"}
+ *
+ */
+ const Link = _Link as unknown as (
+ props: {
+ href: string;
+ children: Component;
+ emitEvent?: boolean;
+ onLoading?: () => void;
+ loadingComponent?: Component;
+ className?: string;
+ queries?: Record;
+ }
+) => void;
+
+ export const PageRoutes = _PageRoutes as unknown as (
+ props: {
+ modules?: any,
+ enableLoader?: boolean
+}
+ ) => void ;
+
+export { Link}
diff --git a/src/routes.ts b/src/routes.ts
index bcba773..758d13e 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -1,2 +1,2 @@
-// @ts-ignore
-export const modules = import.meta.glob("./pages/**/*.ripple", { eager: true });
+// @ts-ignore
+export const modules = import.meta.glob("./pages/**/*.ripple", { eager: true });
diff --git a/src/store.ts b/src/store.ts
index a565608..0d7f90f 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -1,250 +1,250 @@
-/**
- * @typedef Subscriber
- * A callback that receives the current store state or derived value.
- * @example
- * const unsub = store.subscribe(state => console.log(state));
- */
-type Subscriber = (value: T) => void;
-
-/**
- * @typedef StoreOptions
- * Options for creating a store.
- * @property persist Whether to persist store in localStorage (default: false)
- * @property storageKey The key to use in localStorage if persist is true
- */
-interface StoreOptions {
- persist?: boolean;
- storageKey?: string;
-}
-
-/**
- * @typedef Middleware
- * A function that runs before or after each update.
- * @param state Current state of the store
- * @param action Optional string describing the event
- * @param payload Optional data passed to update/set
- */
-type Middleware = (state: T, action?: string, payload?: any) => void;
-
-/**
- * @function createStore
- * Creates a reactive store with optional persistence and tools like watch & middleware.
- *
- * @param initial The initial state object
- * @param options Optional store options
- * @returns Store API with get, set, update, delete, subscribe, derive, watch, use, clear
- *
- * @example
- * const counterStore = createStore({ count: 0 });
- */
-export function createStore(
- initial: T,
- options: StoreOptions = {}
-) {
- const { persist = false, storageKey } = options;
-
- // Check if running in browser for SSR safety
- const canUseStorage = typeof window !== "undefined" && persist && storageKey;
-
- // Load persisted state if enabled
- let value: T = { ...initial };
- if (canUseStorage) {
- const stored = localStorage.getItem(storageKey!);
- if (stored) {
- try {
- value = JSON.parse(stored);
- } catch {}
- }
- }
-
- const subs = new Set>();
- const watchers: { selector: (state: T) => any; cb: (newVal: any, oldVal: any) => void }[] = [];
- const middlewares: Middleware[] = [];
-
- let prevValue = { ...value };
-
- /** Notify all subscribers and watchers of a state change */
- function notify(action?: string, payload?: any) {
- subs.forEach(fn => fn(value));
- watchers.forEach(({ selector, cb }) => {
- const newVal = selector(value);
- const oldVal = selector(prevValue);
- if (newVal !== oldVal) cb(newVal, oldVal);
- });
- middlewares.forEach(fn => fn(value, action, payload));
- }
-
- /** Persist current state to localStorage if enabled */
- function persistToStorage() {
- if (canUseStorage) {
- try {
- localStorage.setItem(storageKey!, JSON.stringify(value));
- } catch (err) {
- console.log("error saving to localStorage: ", err);
- }
- }
- }
-
- /** @function get — Returns the current store state */
- function get(): T {
- return value;
- }
-
- /** @function set — Replaces the entire store state */
- function set(next: T, action = "set") {
- prevValue = value;
- value = { ...next };
- persistToStorage();
- notify(action, next);
- }
-
- /**
- * @function update
- * Merges a partial object or uses a callback to modify the current state.
- *
- * - Object patch: `store.update({ count: 1 })`
- * - Callback style: `store.update(prev => ({ count: prev.count + 1 }))`
- */
- function update(partial: Partial): void;
- function update(partialFn: (prev: T) => Partial): void;
- function update(
- partialOrFn: Partial | ((prev: T) => Partial),
- action = "update"
- ) {
- prevValue = value;
- const patch =
- typeof partialOrFn === "function" ? partialOrFn(value) : partialOrFn;
- value = { ...value, ...patch };
- persistToStorage();
- notify(action, patch);
- }
-
- /** @function delete — Removes one or multiple keys from the store */
- function deleteKeys(keys: (keyof T) | (keyof T)[], action = "delete") {
- prevValue = value;
- const kArray = Array.isArray(keys) ? keys : [keys];
- kArray.forEach(k => delete (value as any)[k]);
- persistToStorage();
- notify(action, keys);
- }
-
- /** @function clear — Clears both state and its localStorage record */
- function clear() {
- prevValue = { ...value };
- value = { ...initial };
- if (canUseStorage) {
- try {
- localStorage.removeItem(storageKey!);
- } catch (err) {
- console.warn("Failed to clear persisted store:", err);
- }
- }
- notify("clear");
- }
-
- /**
- * @function subscribe
- * Subscribes to all store changes (full state or a selected part).
- *
- * @param {Function} fn Callback to receive state or selected part
- * @param {Function} [selector] Optional selector to receive subset
- *
- * @example
- * store.subscribe(s => console.log(s)); // full state
- * store.subscribe(s => console.log(s.theme), s => s.theme); // only theme
- */
- function subscribe(fn: Subscriber, selector?: (state: T) => any) {
- const wrapper = selector ? (state: T) => fn(selector(state)) : fn;
- subs.add(wrapper);
- wrapper(value); // initial call
- return () => subs.delete(wrapper);
- }
-
- /**
- * @function watch
- * Watches a specific value in the store and runs callback when it changes.
- * Provides both new and old values.
- *
- * @param {Function} selector Function selecting what to watch
- * @param {Function} callback Called with (newValue, oldValue)
- *
- * @example
- * store.watch(s => s.count, (n, o) => console.log(`count: ${o} → ${n}`));
- */
- function watch(selector: (s: T) => any, callback: (n: any, o: any) => void) {
- watchers.push({ selector, cb: callback });
- return () => {
- const i = watchers.findIndex(w => w.cb === callback);
- if (i >= 0) watchers.splice(i, 1);
- };
- }
-
- /**
- * @function use
- * Adds middleware to run on every state change.
- *
- * Middleware is for logging, analytics, or side effects.
- *
- * @param {Function} middleware Function with (state, action?, payload?)
- *
- * @example
- * store.use((s, action) => console.log(`[${action}]`, s));
- */
- function use(middleware: Middleware) {
- middlewares.push(middleware);
- return () => {
- const i = middlewares.indexOf(middleware);
- if (i >= 0) middlewares.splice(i, 1);
- };
- }
-
- /**
- * @function derive
- * Creates a derived store that reacts to changes in selected parts of the state.
- *
- * @example
- * const themeStore = store.derive(s => s.theme);
- * themeStore.subscribe(v => console.log("theme:", v));
- */
- function derive(selector: (s: T) => K) {
- return {
- subscribe(cb: (v: K) => void) {
- return subscribe(state => cb(selector(state)));
- },
- };
- }
-
- return {
- get,
- set,
- update,
- delete: deleteKeys,
- clear,
- subscribe,
- watch,
- use,
- derive,
- };
-}
-
-export default createStore;
-
-// -------------------- Example Stores --------------------
-
-/**
- * This provides a basic store for your app like theme, user etc.
- * It's an example which you can delete;
- */
-export const appStore = createStore(
- {
- user: { name: "Joe", location: "unknown", preferences: [] },
- count: 0,
- theme: "dark",
- },
- { persist: true, storageKey: "appStore" }
-);
-
-export const routeStore = createStore(
- { path: "/" },
- { persist: true, storageKey: "routeStore" }
-);
+/**
+ * @typedef Subscriber
+ * A callback that receives the current store state or derived value.
+ * @example
+ * const unsub = store.subscribe(state => console.log(state));
+ */
+type Subscriber = (value: T) => void;
+
+/**
+ * @typedef StoreOptions
+ * Options for creating a store.
+ * @property persist Whether to persist store in localStorage (default: false)
+ * @property storageKey The key to use in localStorage if persist is true
+ */
+interface StoreOptions {
+ persist?: boolean;
+ storageKey?: string;
+}
+
+/**
+ * @typedef Middleware
+ * A function that runs before or after each update.
+ * @param state Current state of the store
+ * @param action Optional string describing the event
+ * @param payload Optional data passed to update/set
+ */
+type Middleware = (state: T, action?: string, payload?: any) => void;
+
+/**
+ * @function createStore
+ * Creates a reactive store with optional persistence and tools like watch & middleware.
+ *
+ * @param initial The initial state object
+ * @param options Optional store options
+ * @returns Store API with get, set, update, delete, subscribe, derive, watch, use, clear
+ *
+ * @example
+ * const counterStore = createStore({ count: 0 });
+ */
+export function createStore(
+ initial: T,
+ options: StoreOptions = {}
+) {
+ const { persist = false, storageKey } = options;
+
+ // Check if running in browser for SSR safety
+ const canUseStorage = typeof window !== "undefined" && persist && storageKey;
+
+ // Load persisted state if enabled
+ let value: T = { ...initial };
+ if (canUseStorage) {
+ const stored = localStorage.getItem(storageKey!);
+ if (stored) {
+ try {
+ value = JSON.parse(stored);
+ } catch {}
+ }
+ }
+
+ const subs = new Set>();
+ const watchers: { selector: (state: T) => any; cb: (newVal: any, oldVal: any) => void }[] = [];
+ const middlewares: Middleware[] = [];
+
+ let prevValue = { ...value };
+
+ /** Notify all subscribers and watchers of a state change */
+ function notify(action?: string, payload?: any) {
+ subs.forEach(fn => fn(value));
+ watchers.forEach(({ selector, cb }) => {
+ const newVal = selector(value);
+ const oldVal = selector(prevValue);
+ if (newVal !== oldVal) cb(newVal, oldVal);
+ });
+ middlewares.forEach(fn => fn(value, action, payload));
+ }
+
+ /** Persist current state to localStorage if enabled */
+ function persistToStorage() {
+ if (canUseStorage) {
+ try {
+ localStorage.setItem(storageKey!, JSON.stringify(value));
+ } catch (err) {
+ console.log("error saving to localStorage: ", err);
+ }
+ }
+ }
+
+ /** @function get — Returns the current store state */
+ function get(): T {
+ return value;
+ }
+
+ /** @function set — Replaces the entire store state */
+ function set(next: T, action = "set") {
+ prevValue = value;
+ value = { ...next };
+ persistToStorage();
+ notify(action, next);
+ }
+
+ /**
+ * @function update
+ * Merges a partial object or uses a callback to modify the current state.
+ *
+ * - Object patch: `store.update({ count: 1 })`
+ * - Callback style: `store.update(prev => ({ count: prev.count + 1 }))`
+ */
+ function update(partial: Partial): void;
+ function update(partialFn: (prev: T) => Partial): void;
+ function update(
+ partialOrFn: Partial | ((prev: T) => Partial),
+ action = "update"
+ ) {
+ prevValue = value;
+ const patch =
+ typeof partialOrFn === "function" ? partialOrFn(value) : partialOrFn;
+ value = { ...value, ...patch };
+ persistToStorage();
+ notify(action, patch);
+ }
+
+ /** @function delete — Removes one or multiple keys from the store */
+ function deleteKeys(keys: (keyof T) | (keyof T)[], action = "delete") {
+ prevValue = value;
+ const kArray = Array.isArray(keys) ? keys : [keys];
+ kArray.forEach(k => delete (value as any)[k]);
+ persistToStorage();
+ notify(action, keys);
+ }
+
+ /** @function clear — Clears both state and its localStorage record */
+ function clear() {
+ prevValue = { ...value };
+ value = { ...initial };
+ if (canUseStorage) {
+ try {
+ localStorage.removeItem(storageKey!);
+ } catch (err) {
+ console.warn("Failed to clear persisted store:", err);
+ }
+ }
+ notify("clear");
+ }
+
+ /**
+ * @function subscribe
+ * Subscribes to all store changes (full state or a selected part).
+ *
+ * @param {Function} fn Callback to receive state or selected part
+ * @param {Function} [selector] Optional selector to receive subset
+ *
+ * @example
+ * store.subscribe(s => console.log(s)); // full state
+ * store.subscribe(s => console.log(s.theme), s => s.theme); // only theme
+ */
+ function subscribe(fn: Subscriber, selector?: (state: T) => any) {
+ const wrapper = selector ? (state: T) => fn(selector(state)) : fn;
+ subs.add(wrapper);
+ wrapper(value); // initial call
+ return () => subs.delete(wrapper);
+ }
+
+ /**
+ * @function watch
+ * Watches a specific value in the store and runs callback when it changes.
+ * Provides both new and old values.
+ *
+ * @param {Function} selector Function selecting what to watch
+ * @param {Function} callback Called with (newValue, oldValue)
+ *
+ * @example
+ * store.watch(s => s.count, (n, o) => console.log(`count: ${o} → ${n}`));
+ */
+ function watch(selector: (s: T) => any, callback: (n: any, o: any) => void) {
+ watchers.push({ selector, cb: callback });
+ return () => {
+ const i = watchers.findIndex(w => w.cb === callback);
+ if (i >= 0) watchers.splice(i, 1);
+ };
+ }
+
+ /**
+ * @function use
+ * Adds middleware to run on every state change.
+ *
+ * Middleware is for logging, analytics, or side effects.
+ *
+ * @param {Function} middleware Function with (state, action?, payload?)
+ *
+ * @example
+ * store.use((s, action) => console.log(`[${action}]`, s));
+ */
+ function use(middleware: Middleware) {
+ middlewares.push(middleware);
+ return () => {
+ const i = middlewares.indexOf(middleware);
+ if (i >= 0) middlewares.splice(i, 1);
+ };
+ }
+
+ /**
+ * @function derive
+ * Creates a derived store that reacts to changes in selected parts of the state.
+ *
+ * @example
+ * const themeStore = store.derive(s => s.theme);
+ * themeStore.subscribe(v => console.log("theme:", v));
+ */
+ function derive(selector: (s: T) => K) {
+ return {
+ subscribe(cb: (v: K) => void) {
+ return subscribe(state => cb(selector(state)));
+ },
+ };
+ }
+
+ return {
+ get,
+ set,
+ update,
+ delete: deleteKeys,
+ clear,
+ subscribe,
+ watch,
+ use,
+ derive,
+ };
+}
+
+export default createStore;
+
+// -------------------- Example Stores --------------------
+
+/**
+ * This provides a basic store for your app like theme, user etc.
+ * It's an example which you can delete;
+ */
+export const appStore = createStore(
+ {
+ user: { name: "Joe", location: "unknown", preferences: [] },
+ count: 0,
+ theme: "dark",
+ },
+ { persist: true, storageKey: "appStore" }
+);
+
+export const routeStore = createStore(
+ { path: "/" },
+ { persist: true, storageKey: "routeStore" }
+);
diff --git a/src/stores/index.ts b/src/stores/index.ts
index 638c861..aaee84a 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -1,5 +1,5 @@
-// export * from "../createDBStore";
-// export * from "../store";
-import createStore from "../store";
-import { createIndexDBStore } from "../createDBStore";
-export { createStore, createIndexDBStore };
+// export * from "../createDBStore";
+// export * from "../store";
+import createStore from "../store";
+import { createIndexDBStore } from "../createDBStore";
+export { createStore, createIndexDBStore };
diff --git a/src/types.ts b/src/types.ts
index 889e222..24733f0 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,27 +1,27 @@
-import { Component } from "ripple";
-import type { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
-
-
-
-export type Req = FastifyRequest;
-export type Reply = FastifyReply;
-export type App = FastifyInstance;
-
-export interface Route {
- path: string;
- component: Component;
-}
-
-export interface RouterProp {
- routes: Route[];
-}
-
-export interface LinkProps {
- href: string;
- children: Component;
- onLoading?: () => void;
- emitEvent?: boolean;
- loadingComponent?: Component;
- className?: string;
- queries?: Record;
-}
+import { Component } from "ripple";
+import type { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
+
+
+
+export type Req = FastifyRequest;
+export type Reply = FastifyReply;
+export type App = FastifyInstance;
+
+export interface Route {
+ path: string;
+ component: Component;
+}
+
+export interface RouterProp {
+ routes: Route[];
+}
+
+export interface LinkProps {
+ href: string;
+ children: Component;
+ onLoading?: () => void;
+ emitEvent?: boolean;
+ loadingComponent?: Component;
+ className?: string;
+ queries?: Record;
+}
diff --git a/src/userouter.ts b/src/userouter.ts
index c7e9bd9..6ed7312 100644
--- a/src/userouter.ts
+++ b/src/userouter.ts
@@ -1,376 +1,376 @@
-import type { Component } from "ripple";
-
-type RouterListener = (path: string) => void;
-type BeforePopCallback = (url: string) => boolean | void;
-
-/**
- * RouterStore
- * -----------
- * Client-side router for Ripple apps, inspired by Next.js router.
- * Features:
- * - Path tracking
- * - Query parameters
- * - Dynamic route parameters (params)
- * - Loading state
- * - Router events: start, complete, change
- * - Shallow routing
- * - Back navigation guards
- * - Prefetching of routes
- *
- * Uses HTML5 History API to avoid full page reloads.
- */
-class RouterStore {
- /** Current pathname (e.g., "/users/42") */
- path: string = "";
-
- /** Query parameters (e.g., { tab: "posts" }) */
- queries: Record = {};
-
- /** Dynamic route parameters extracted from matched patterns (e.g., { id: "42" }) */
- params: Record = {};
-
- /** Loading state, true when navigation starts */
- loading = false;
-
- /** Internal event listeners: start, complete, change */
- private listeners: {
- start: RouterListener[];
- complete: RouterListener[];
- change: RouterListener[];
- } = { start: [], complete: [], change: [] };
-
- /** Callbacks that can block back/forward navigation */
- private beforePop: BeforePopCallback[] = [];
-
- constructor() {
- if (typeof window !== "undefined") {
- this.syncWithLocation();
- window.addEventListener("popstate", () => this.handlePopState());
- }
- }
-
- /** Handle browser back/forward events */
- private handlePopState() {
- if (typeof window === "undefined") return;
- for (const fn of this.beforePop) {
- const result = fn(this.asPath);
- if (result === false) return; // Cancel navigation
- }
- this.syncWithLocation();
- }
-
- /**
- * Sync router state with window.location
- * @param announce Whether to emit events (default: true)
- */
- private syncWithLocation(announce = true) {
- if (typeof window === "undefined") return;
- this.path = window.location.pathname;
- this.queries = Object.fromEntries(new URLSearchParams(window.location.search).entries());
- this.params = {};
- this.loading = false;
-
- if (announce) {
- this.emit("change", this.path);
- this.emit("complete", this.path);
- }
- }
-
- /** Emit event to all listeners */
- private emit(type: keyof RouterStore["listeners"], path: string) {
- for (const cb of this.listeners[type]) cb(path);
- }
-
- /**
- * Subscribe to router events
- * @param type "start" | "complete" | "change"
- * @param cb Callback called with path
- * @returns Unsubscribe function
- */
- on(type: keyof RouterStore["listeners"], cb: RouterListener) {
- this.listeners[type].push(cb);
- return () => {
- this.listeners[type] = this.listeners[type].filter(fn => fn !== cb);
- };
- }
-
- /** Builds a complete URL from path + query object */
- private buildUrl(path: string, queries?: Record): string {
- if (!queries || Object.keys(queries).length === 0) return path;
- const params = new URLSearchParams();
- for (const [key, value] of Object.entries(queries)) {
- if (value !== undefined && value !== null) params.set(key, String(value));
- }
- const query = params.toString();
- return query ? `${path}?${query}` : path;
- }
-
- /**
- * Register a callback before back/forward navigation
- * Return false to cancel navigation
- */
- beforePopState(cb: BeforePopCallback) {
- this.beforePop.push(cb);
- return () => {
- this.beforePop = this.beforePop.filter(f => f !== cb);
- };
- }
-
- /**
- * Navigate to a new URL (pushState)
- * @param url Target URL
- * @param announce Emit events? (default: true)
- * @param shallow Update URL without full sync? (default: false)
- */
- push(
- url: string,
- announce = true,
- shallow = false,
- queries?: Record,
- ) {
- const fullUrl = this.buildUrl(url, queries);
- this.navigate(fullUrl, "push", announce, shallow);
- }
-
- /**
- * Replace current URL (replaceState)
- * @param url Target URL
- * @param announce Emit events? (default: true)
- * @param shallow Update URL without full sync? (default: false)
- */
- replace(
- url: string,
- announce = true,
- shallow = false,
- queries?: Record,
- ) {
- const fullUrl = this.buildUrl(url, queries);
- this.navigate(fullUrl, "replace", announce, shallow);
- }
-
- /** Go back in browser history */
- back(announce = true) {
- if (typeof window === "undefined") return;
- this.loading = true;
- if (announce) this.emit("start", this.path);
- history.back();
- window.dispatchEvent(new Event("popstate"));
- }
-
- /** Internal navigation helper */
- private navigate(url: string, method: "push" | "replace", announce = true, shallow = false) {
- if (typeof window === "undefined") return;
-
- this.loading = true;
- if (announce) this.emit("start", url);
-
- const [path, search = ""] = url.split("?");
-
- // Always update internal state
- this.path = path;
- this.queries = Object.fromEntries(new URLSearchParams(search).entries());
-
- // Update browser history
- if (method === "push") window.history.pushState({}, "", url);
- else window.history.replaceState({}, "", url);
-
- // Only run full sync/announce if NOT shallow
- if (!shallow) this.syncWithLocation(announce);
-
- // Trigger popstate so listeners react
- window.dispatchEvent(new Event("popstate"));
- }
-
- /**
- * Match a route pattern to a path
- * @param pathPattern Pattern (e.g., "/users/:id")
- * @param currentPath Path to match (e.g., "/users/42")
- * @returns true if matches, null if not
- * Sets `this.params` with extracted dynamic parameters
- */
- matchRoute(pathPattern: string, currentPath: string) {
- if (!currentPath) return null;
- const normalize = (p: string) => (p === "/" ? "" : p.replace(/\/+$/, ""));
- const pattern = normalize(pathPattern);
- const path = normalize(currentPath);
-
- let wildcardIndex = 0;
- const regexPattern = pattern
- .replace(/:([A-Za-z0-9_]+)/g, (_, key) => `(?<${key}>[^/]+)`)
- .replace(/\*/g, () => {
- wildcardIndex++;
- return `(?.*)`;
- });
-
- const regex = new RegExp(`^${regexPattern}$`);
- const match = path.match(regex);
-
- if (!match) return null;
- this.params = match.groups || {};
- return true;
- }
-
- /** Simulate prefetching a route */
- prefetch(url: string) {
- if (typeof window === "undefined") return Promise.resolve();
- console.log("Prefetching route:", url);
- return Promise.resolve();
- }
-
- /** Resolve relative href to absolute path + query */
- resolveHref(href: string) {
- if (typeof window === "undefined") return href;
- const url = new URL(href, window.location.origin);
- return url.pathname + url.search;
- }
-
- /** Full path with query string */
- get asPath() {
- return typeof window !== "undefined"
- ? window.location.pathname + window.location.search
- : this.path;
- }
-}
-
-export const routerStore = new RouterStore();
-
-export interface Router {
- readonly path: string;
- readonly asPath: string;
- readonly queries: Record;
- readonly params: Record;
- readonly loading: boolean;
- push: (
- url: string,
- announce?: boolean,
- shallow?: boolean,
- queries?: Record
- ) => void;
- replace: (
- url: string,
- announce?: boolean,
- shallow?: boolean,
- queries?: Record
- ) => void;
- back: (announce?: boolean) => void;
- match: (pattern: string, currentPath: string) => true | null;
- on: (type: "start" | "complete" | "change", cb: (path: string) => void) => () => void;
- beforePopState: (cb: (url: string) => boolean | void) => () => void;
- prefetch: (url: string) => Promise;
- resolveHref: (url: string) => string;
- isActive: (url: string) => boolean;
- readonly host: string;
- readonly hostname: string;
- readonly protocol: string;
- readonly origin: string;
- readonly port: string;
- readonly href: string;
- readonly hash: string;
- readonly search: string;
-}
-
-/**
- * useRouter hook
- *
- * Provides full client-side navigation and routing capabilities.
- */
-export function useRouter(): Router {
- return {
- get path() { return routerStore.path; },
- get asPath() { return routerStore.asPath; },
- get queries() { return routerStore.queries; },
- get params() { return routerStore.params; },
- get loading() { return routerStore.loading; },
- push: (...args) => routerStore.push(...args),
- replace: (...args) => routerStore.replace(...args),
- back: (announce = true) => routerStore.back(announce),
- match: (pattern, currentPath) => routerStore.matchRoute(pattern, currentPath),
- on: (type, cb) => routerStore.on(type, cb),
- beforePopState: (cb) => routerStore.beforePopState(cb),
- prefetch: (url) => routerStore.prefetch(url),
- resolveHref: (url) => routerStore.resolveHref(url),
- isActive: (url) => url === routerStore.path,
- get host() { return typeof window !== "undefined" ? window.location.host : ""; },
- get hostname() { return typeof window !== "undefined" ? window.location.hostname : ""; },
- get protocol() { return typeof window !== "undefined" ? window.location.protocol : ""; },
- get origin() { return typeof window !== "undefined" ? window.location.origin : ""; },
- get port() { return typeof window !== "undefined" ? window.location.port : ""; },
- get href() { return typeof window !== "undefined" ? window.location.href : ""; },
- get hash() { return typeof window !== "undefined" ? window.location.hash : ""; },
- get search() { return typeof window !== "undefined" ? window.location.search : ""; }
- };
-}
-
-
-/**
- * useRouter hook
- *
- * Provides full client-side navigation and routing capabilities.
- *
- * @example
- * ```ts
- * import { useRouter } from "rivra/router";
- *
- * const router = useRouter();
- * router.push("/users/42?tab=posts");
- *
- * router.replace("/users/42?tab=profile", true, true);
- * router.prefetch("/about");
- *
- * router.on("start", (path) => console.log("Start navigating to:", path));
- * router.on("change", (path) => console.log("Route changed:", path));
- * router.on("complete", (path) => console.log("Navigation complete:", path));
- *
- * router.beforePopState((url) => {
- * if (url === "/protected") return false; // Block navigation
- * });
- * ```
- */
-
-
-export default useRouter;
-
-// -------------------- Example Usage --------------------
-
-// const router = useRouter();
-
-// // Listen to navigation events
-// router.on("start", (path) => console.log("Start navigating to:", path));
-// router.on("change", (path) => console.log("Route changed to:", path));
-// router.on("complete", (path) => console.log("Navigation complete:", path));
-// router.isActive("/about")
-// Guard back navigation
-// router.beforePopState((url) => {
-// if (url === "/protected") {
-// console.log("Navigation blocked:", url);
-// return false; // Cancel navigation
-// }
-// });
-
-// Navigate to a new route
-//router.push("/users/42?tab=posts");
-
-// Replace URL shallowly (no full sync)//
-//router.replace("/users/42?tab=profile", true, true);
-
-// Prefetch a route
-// router.prefetch("/about");
-
-// Resolve href
-// console.log("Resolved href:", router.resolveHref("/contact?ref=home"));
-
-// Access reactive properties
-// console.log("Current path:", router.path);
-// console.log("Query params:", router.queries);
-// console.log("Dynamic params:", router.params);
-// console.log("Full URL:", router.asPath);
-
-// Access full URL info
-// console.log(router.host);
-// console.log(router.hostname);
-// console.log(router.origin);
-// console.log(router.protocol);
-// console.log(router.port);
-// console.log(router.href);
-// console.log(router.search);
-// console.log(router.hash);
+import type { Component } from "ripple";
+
+type RouterListener = (path: string) => void;
+type BeforePopCallback = (url: string) => boolean | void;
+
+/**
+ * RouterStore
+ * -----------
+ * Client-side router for Ripple apps, inspired by Next.js router.
+ * Features:
+ * - Path tracking
+ * - Query parameters
+ * - Dynamic route parameters (params)
+ * - Loading state
+ * - Router events: start, complete, change
+ * - Shallow routing
+ * - Back navigation guards
+ * - Prefetching of routes
+ *
+ * Uses HTML5 History API to avoid full page reloads.
+ */
+class RouterStore {
+ /** Current pathname (e.g., "/users/42") */
+ path: string = "";
+
+ /** Query parameters (e.g., { tab: "posts" }) */
+ queries: Record = {};
+
+ /** Dynamic route parameters extracted from matched patterns (e.g., { id: "42" }) */
+ params: Record = {};
+
+ /** Loading state, true when navigation starts */
+ loading = false;
+
+ /** Internal event listeners: start, complete, change */
+ private listeners: {
+ start: RouterListener[];
+ complete: RouterListener[];
+ change: RouterListener[];
+ } = { start: [], complete: [], change: [] };
+
+ /** Callbacks that can block back/forward navigation */
+ private beforePop: BeforePopCallback[] = [];
+
+ constructor() {
+ if (typeof window !== "undefined") {
+ this.syncWithLocation();
+ window.addEventListener("popstate", () => this.handlePopState());
+ }
+ }
+
+ /** Handle browser back/forward events */
+ private handlePopState() {
+ if (typeof window === "undefined") return;
+ for (const fn of this.beforePop) {
+ const result = fn(this.asPath);
+ if (result === false) return; // Cancel navigation
+ }
+ this.syncWithLocation();
+ }
+
+ /**
+ * Sync router state with window.location
+ * @param announce Whether to emit events (default: true)
+ */
+ private syncWithLocation(announce = true) {
+ if (typeof window === "undefined") return;
+ this.path = window.location.pathname;
+ this.queries = Object.fromEntries(new URLSearchParams(window.location.search).entries());
+ this.params = {};
+ this.loading = false;
+
+ if (announce) {
+ this.emit("change", this.path);
+ this.emit("complete", this.path);
+ }
+ }
+
+ /** Emit event to all listeners */
+ private emit(type: keyof RouterStore["listeners"], path: string) {
+ for (const cb of this.listeners[type]) cb(path);
+ }
+
+ /**
+ * Subscribe to router events
+ * @param type "start" | "complete" | "change"
+ * @param cb Callback called with path
+ * @returns Unsubscribe function
+ */
+ on(type: keyof RouterStore["listeners"], cb: RouterListener) {
+ this.listeners[type].push(cb);
+ return () => {
+ this.listeners[type] = this.listeners[type].filter(fn => fn !== cb);
+ };
+ }
+
+ /** Builds a complete URL from path + query object */
+ private buildUrl(path: string, queries?: Record): string {
+ if (!queries || Object.keys(queries).length === 0) return path;
+ const params = new URLSearchParams();
+ for (const [key, value] of Object.entries(queries)) {
+ if (value !== undefined && value !== null) params.set(key, String(value));
+ }
+ const query = params.toString();
+ return query ? `${path}?${query}` : path;
+ }
+
+ /**
+ * Register a callback before back/forward navigation
+ * Return false to cancel navigation
+ */
+ beforePopState(cb: BeforePopCallback) {
+ this.beforePop.push(cb);
+ return () => {
+ this.beforePop = this.beforePop.filter(f => f !== cb);
+ };
+ }
+
+ /**
+ * Navigate to a new URL (pushState)
+ * @param url Target URL
+ * @param announce Emit events? (default: true)
+ * @param shallow Update URL without full sync? (default: false)
+ */
+ push(
+ url: string,
+ announce = true,
+ shallow = false,
+ queries?: Record,
+ ) {
+ const fullUrl = this.buildUrl(url, queries);
+ this.navigate(fullUrl, "push", announce, shallow);
+ }
+
+ /**
+ * Replace current URL (replaceState)
+ * @param url Target URL
+ * @param announce Emit events? (default: true)
+ * @param shallow Update URL without full sync? (default: false)
+ */
+ replace(
+ url: string,
+ announce = true,
+ shallow = false,
+ queries?: Record,
+ ) {
+ const fullUrl = this.buildUrl(url, queries);
+ this.navigate(fullUrl, "replace", announce, shallow);
+ }
+
+ /** Go back in browser history */
+ back(announce = true) {
+ if (typeof window === "undefined") return;
+ this.loading = true;
+ if (announce) this.emit("start", this.path);
+ history.back();
+ window.dispatchEvent(new Event("popstate"));
+ }
+
+ /** Internal navigation helper */
+ private navigate(url: string, method: "push" | "replace", announce = true, shallow = false) {
+ if (typeof window === "undefined") return;
+
+ this.loading = true;
+ if (announce) this.emit("start", url);
+
+ const [path, search = ""] = url.split("?");
+
+ // Always update internal state
+ this.path = path;
+ this.queries = Object.fromEntries(new URLSearchParams(search).entries());
+
+ // Update browser history
+ if (method === "push") window.history.pushState({}, "", url);
+ else window.history.replaceState({}, "", url);
+
+ // Only run full sync/announce if NOT shallow
+ if (!shallow) this.syncWithLocation(announce);
+
+ // Trigger popstate so listeners react
+ window.dispatchEvent(new Event("popstate"));
+ }
+
+ /**
+ * Match a route pattern to a path
+ * @param pathPattern Pattern (e.g., "/users/:id")
+ * @param currentPath Path to match (e.g., "/users/42")
+ * @returns true if matches, null if not
+ * Sets `this.params` with extracted dynamic parameters
+ */
+ matchRoute(pathPattern: string, currentPath: string) {
+ if (!currentPath) return null;
+ const normalize = (p: string) => (p === "/" ? "" : p.replace(/\/+$/, ""));
+ const pattern = normalize(pathPattern);
+ const path = normalize(currentPath);
+
+ let wildcardIndex = 0;
+ const regexPattern = pattern
+ .replace(/:([A-Za-z0-9_]+)/g, (_, key) => `(?<${key}>[^/]+)`)
+ .replace(/\*/g, () => {
+ wildcardIndex++;
+ return `(?.*)`;
+ });
+
+ const regex = new RegExp(`^${regexPattern}$`);
+ const match = path.match(regex);
+
+ if (!match) return null;
+ this.params = match.groups || {};
+ return true;
+ }
+
+ /** Simulate prefetching a route */
+ prefetch(url: string) {
+ if (typeof window === "undefined") return Promise.resolve();
+ console.log("Prefetching route:", url);
+ return Promise.resolve();
+ }
+
+ /** Resolve relative href to absolute path + query */
+ resolveHref(href: string) {
+ if (typeof window === "undefined") return href;
+ const url = new URL(href, window.location.origin);
+ return url.pathname + url.search;
+ }
+
+ /** Full path with query string */
+ get asPath() {
+ return typeof window !== "undefined"
+ ? window.location.pathname + window.location.search
+ : this.path;
+ }
+}
+
+export const routerStore = new RouterStore();
+
+export interface Router {
+ readonly path: string;
+ readonly asPath: string;
+ readonly queries: Record;
+ readonly params: Record;
+ readonly loading: boolean;
+ push: (
+ url: string,
+ announce?: boolean,
+ shallow?: boolean,
+ queries?: Record
+ ) => void;
+ replace: (
+ url: string,
+ announce?: boolean,
+ shallow?: boolean,
+ queries?: Record
+ ) => void;
+ back: (announce?: boolean) => void;
+ match: (pattern: string, currentPath: string) => true | null;
+ on: (type: "start" | "complete" | "change", cb: (path: string) => void) => () => void;
+ beforePopState: (cb: (url: string) => boolean | void) => () => void;
+ prefetch: (url: string) => Promise;
+ resolveHref: (url: string) => string;
+ isActive: (url: string) => boolean;
+ readonly host: string;
+ readonly hostname: string;
+ readonly protocol: string;
+ readonly origin: string;
+ readonly port: string;
+ readonly href: string;
+ readonly hash: string;
+ readonly search: string;
+}
+
+/**
+ * useRouter hook
+ *
+ * Provides full client-side navigation and routing capabilities.
+ */
+export function useRouter(): Router {
+ return {
+ get path() { return routerStore.path; },
+ get asPath() { return routerStore.asPath; },
+ get queries() { return routerStore.queries; },
+ get params() { return routerStore.params; },
+ get loading() { return routerStore.loading; },
+ push: (...args) => routerStore.push(...args),
+ replace: (...args) => routerStore.replace(...args),
+ back: (announce = true) => routerStore.back(announce),
+ match: (pattern, currentPath) => routerStore.matchRoute(pattern, currentPath),
+ on: (type, cb) => routerStore.on(type, cb),
+ beforePopState: (cb) => routerStore.beforePopState(cb),
+ prefetch: (url) => routerStore.prefetch(url),
+ resolveHref: (url) => routerStore.resolveHref(url),
+ isActive: (url) => url === routerStore.path,
+ get host() { return typeof window !== "undefined" ? window.location.host : ""; },
+ get hostname() { return typeof window !== "undefined" ? window.location.hostname : ""; },
+ get protocol() { return typeof window !== "undefined" ? window.location.protocol : ""; },
+ get origin() { return typeof window !== "undefined" ? window.location.origin : ""; },
+ get port() { return typeof window !== "undefined" ? window.location.port : ""; },
+ get href() { return typeof window !== "undefined" ? window.location.href : ""; },
+ get hash() { return typeof window !== "undefined" ? window.location.hash : ""; },
+ get search() { return typeof window !== "undefined" ? window.location.search : ""; }
+ };
+}
+
+
+/**
+ * useRouter hook
+ *
+ * Provides full client-side navigation and routing capabilities.
+ *
+ * @example
+ * ```ts
+ * import { useRouter } from "rivra/router";
+ *
+ * const router = useRouter();
+ * router.push("/users/42?tab=posts");
+ *
+ * router.replace("/users/42?tab=profile", true, true);
+ * router.prefetch("/about");
+ *
+ * router.on("start", (path) => console.log("Start navigating to:", path));
+ * router.on("change", (path) => console.log("Route changed:", path));
+ * router.on("complete", (path) => console.log("Navigation complete:", path));
+ *
+ * router.beforePopState((url) => {
+ * if (url === "/protected") return false; // Block navigation
+ * });
+ * ```
+ */
+
+
+export default useRouter;
+
+// -------------------- Example Usage --------------------
+
+// const router = useRouter();
+
+// // Listen to navigation events
+// router.on("start", (path) => console.log("Start navigating to:", path));
+// router.on("change", (path) => console.log("Route changed to:", path));
+// router.on("complete", (path) => console.log("Navigation complete:", path));
+// router.isActive("/about")
+// Guard back navigation
+// router.beforePopState((url) => {
+// if (url === "/protected") {
+// console.log("Navigation blocked:", url);
+// return false; // Cancel navigation
+// }
+// });
+
+// Navigate to a new route
+//router.push("/users/42?tab=posts");
+
+// Replace URL shallowly (no full sync)//
+//router.replace("/users/42?tab=profile", true, true);
+
+// Prefetch a route
+// router.prefetch("/about");
+
+// Resolve href
+// console.log("Resolved href:", router.resolveHref("/contact?ref=home"));
+
+// Access reactive properties
+// console.log("Current path:", router.path);
+// console.log("Query params:", router.queries);
+// console.log("Dynamic params:", router.params);
+// console.log("Full URL:", router.asPath);
+
+// Access full URL info
+// console.log(router.host);
+// console.log(router.hostname);
+// console.log(router.origin);
+// console.log(router.protocol);
+// console.log(router.port);
+// console.log(router.href);
+// console.log(router.search);
+// console.log(router.hash);
diff --git a/src/vite-plugin-add-alias.ts b/src/vite-plugin-add-alias.ts
index 8767a21..a1ae971 100644
--- a/src/vite-plugin-add-alias.ts
+++ b/src/vite-plugin-add-alias.ts
@@ -1,20 +1,20 @@
-import { Plugin } from 'vite';
-
-export function addAliasPlugin(): Plugin {
- return {
- name: 'vite-plugin-add-alias',
- config(config) {
- // Ensure that the alias section exists
- config.resolve = config.resolve || {};
- config.resolve.alias = config.resolve.alias || {};
-
- // Dynamically resolve the src directory
- // In this case, we avoid using `path` entirely to prevent issues in the browser
- const srcPath = `${__dirname}/src`; // Relative path for bundling
-
- // Add the alias for `@src`
-
- return config;
- },
- };
-}
+import { Plugin } from 'vite';
+
+export function addAliasPlugin(): Plugin {
+ return {
+ name: 'vite-plugin-add-alias',
+ config(config) {
+ // Ensure that the alias section exists
+ config.resolve = config.resolve || {};
+ config.resolve.alias = config.resolve.alias || {};
+
+ // Dynamically resolve the src directory
+ // In this case, we avoid using `path` entirely to prevent issues in the browser
+ const srcPath = `${__dirname}/src`; // Relative path for bundling
+
+ // Add the alias for `@src`
+
+ return config;
+ },
+ };
+}
diff --git a/test/some.js b/test/some.js
index 9d0c393..a6015b1 100644
--- a/test/some.js
+++ b/test/some.js
@@ -1,98 +1,98 @@
-import Fastify from "fastify";
-import fastifyMiddie from "@fastify/middie";
-import { createServer as createViteServer } from "vite";
-import fs from "fs";
-import path from "path";
-import { fileURLToPath } from "url";
-import { executeServerFunction } from "ripple/server";
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const projectRoot = process.cwd();
-
-const rpc_modules = new Map();
-
-async function getRequestBody(req) {
- return new Promise((resolve, reject) => {
- let data = '';
- req.on('data', (chunk) => {
- data += chunk;
- if (data.length > 1e6) {
- req.destroy();
- reject(new Error('Request body too large'));
- }
- });
- req.on('end', () => resolve(data));
- req.on('error', reject);
- });
-}
-
-export async function StartServer() {
- const app = Fastify({ logger: true });
-
- await app.register(fastifyMiddie);
-
- const vite = await createViteServer({
- root: projectRoot,
- server: { middlewareMode: true },
- appType: 'custom',
- });
-
- // Use Vite as middleware
- app.use((req, res, next) => {
- vite.middlewares(req, res, next);
- });
-
- // SSR route
- app.all("/*", async (req, reply) => {
- try {
- // Handle RPC requests
- if (req.raw.url?.startsWith('/_$_ripple_rpc_$_/')) {
- const hash = req.raw.url.slice('/_$_ripple_rpc_$_/'.length);
- const module_info = rpc_modules.get(hash);
-
- if (!module_info) {
- reply.status(500).send('RPC module not found');
- return;
- }
-
- const file_path = module_info[0];
- const func_name = module_info[1];
- const { _$_server_$_: server } = await vite.ssrLoadModule(file_path);
- const rpc_arguments = await getRequestBody(req.raw);
- const result = await executeServerFunction(server[func_name], rpc_arguments);
- reply.type("application/json").send(result);
- return;
- }
-
- // SSR HTML
- let template = fs.readFileSync(path.resolve(projectRoot, 'index.html'), 'utf-8');
- template = await vite.transformIndexHtml(req.raw.url, template);
-
- const { render, get_css_for_hashes } = await vite.ssrLoadModule('ripple/server');
- const { App } = await vite.ssrLoadModule('/src/App.ripple');
- const { head, body, css } = await render(App);
-
- let css_tags = '';
- if (css.size > 0) {
- const css_content = get_css_for_hashes(css);
- if (css_content) css_tags = ``;
- }
-
- const html = template
- .replace('', head + css_tags)
- .replace('', body);
-
- reply.type('text/html').send(html);
-
- } catch (err) {
- console.error('SSR Error:', err);
- // reply.status(500).send(String(err.stack || err));
- }
- });
-
- const port = 3000;
- app.listen({ port }, () => console.log(`🚀 Fastify + Vite SSR running at http://localhost:${port}`));
-}
-
-export default StartServer;
+import Fastify from "fastify";
+import fastifyMiddie from "@fastify/middie";
+import { createServer as createViteServer } from "vite";
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import { executeServerFunction } from "ripple/server";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const projectRoot = process.cwd();
+
+const rpc_modules = new Map();
+
+async function getRequestBody(req) {
+ return new Promise((resolve, reject) => {
+ let data = '';
+ req.on('data', (chunk) => {
+ data += chunk;
+ if (data.length > 1e6) {
+ req.destroy();
+ reject(new Error('Request body too large'));
+ }
+ });
+ req.on('end', () => resolve(data));
+ req.on('error', reject);
+ });
+}
+
+export async function StartServer() {
+ const app = Fastify({ logger: true });
+
+ await app.register(fastifyMiddie);
+
+ const vite = await createViteServer({
+ root: projectRoot,
+ server: { middlewareMode: true },
+ appType: 'custom',
+ });
+
+ // Use Vite as middleware
+ app.use((req, res, next) => {
+ vite.middlewares(req, res, next);
+ });
+
+ // SSR route
+ app.all("/*", async (req, reply) => {
+ try {
+ // Handle RPC requests
+ if (req.raw.url?.startsWith('/_$_ripple_rpc_$_/')) {
+ const hash = req.raw.url.slice('/_$_ripple_rpc_$_/'.length);
+ const module_info = rpc_modules.get(hash);
+
+ if (!module_info) {
+ reply.status(500).send('RPC module not found');
+ return;
+ }
+
+ const file_path = module_info[0];
+ const func_name = module_info[1];
+ const { _$_server_$_: server } = await vite.ssrLoadModule(file_path);
+ const rpc_arguments = await getRequestBody(req.raw);
+ const result = await executeServerFunction(server[func_name], rpc_arguments);
+ reply.type("application/json").send(result);
+ return;
+ }
+
+ // SSR HTML
+ let template = fs.readFileSync(path.resolve(projectRoot, 'index.html'), 'utf-8');
+ template = await vite.transformIndexHtml(req.raw.url, template);
+
+ const { render, get_css_for_hashes } = await vite.ssrLoadModule('ripple/server');
+ const { App } = await vite.ssrLoadModule('/src/App.ripple');
+ const { head, body, css } = await render(App);
+
+ let css_tags = '';
+ if (css.size > 0) {
+ const css_content = get_css_for_hashes(css);
+ if (css_content) css_tags = `