{"id":35419844,"url":"https://github.com/cloudcanal/connect","last_synced_at":"2026-01-13T19:51:21.359Z","repository":{"id":330758979,"uuid":"1123849321","full_name":"cloudcanal/connect","owner":"cloudcanal","description":"A JavaScript framework for building web applications with PocketBase, including state management and event-driven architecture.","archived":false,"fork":false,"pushed_at":"2026-01-07T20:59:47.000Z","size":165,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-08T21:30:00.501Z","etag":null,"topics":["javascript-framework","pocketbase"],"latest_commit_sha":null,"homepage":"https://www.cloudcanal.io/connect","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cloudcanal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-27T18:58:56.000Z","updated_at":"2026-01-07T20:59:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cloudcanal/connect","commit_stats":null,"previous_names":["cloudcanal/connect"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/cloudcanal/connect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloudcanal%2Fconnect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloudcanal%2Fconnect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloudcanal%2Fconnect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloudcanal%2Fconnect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cloudcanal","download_url":"https://codeload.github.com/cloudcanal/connect/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloudcanal%2Fconnect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28397981,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript-framework","pocketbase"],"created_at":"2026-01-02T16:17:27.863Z","updated_at":"2026-01-13T19:51:21.352Z","avatar_url":"https://github.com/cloudcanal.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Connect\n\nA JavaScript library for building browser applications with [PocketBase](https://pocketbase.io/). Provides unified state management, event handling, and database operations through a simple API.\n\n## Installation\n\nInclude via CDN (jsDelivr):\n\n```html\n\u003c!-- Development --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/gh/cloudcanal/connect@main/dist/connect.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Production (minified) --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/gh/cloudcanal/connect@main/dist/connect.min.js\"\u003e\u003c/script\u003e\n```\n\nThe library automatically attaches to `window.cc` and auto-initializes with `window.location.origin` as the PocketBase URL.\n\n## Quick Start\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\n        \u003cscript src=\"https://cdn.jsdelivr.net/gh/cloudcanal/connect@main/dist/connect.min.js\"\u003e\u003c/script\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n        \u003cbutton id=\"login-btn\"\u003eLogin\u003c/button\u003e\n        \u003cdiv id=\"user-info\"\u003e\u003c/div\u003e\n\n        \u003cscript\u003e\n            // Listen for auth changes\n            cc.events.on('auth:change', ({ user, isAuthenticated }) =\u003e {\n                document.getElementById('user-info').textContent =\n                    isAuthenticated\n                        ? `Welcome, ${user.email}`\n                        : 'Not logged in';\n            });\n\n            // Handle login button click\n            document.getElementById('login-btn').addEventListener('click', async () =\u003e {\n                try {\n                    await cc.db.login('user@example.com', 'password123');\n                } catch (e) {\n                    console.error('Login failed:', e);\n                }\n            });\n        \u003c/script\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\n---\n\n## Core Concepts\n\nConnect consists of three modules:\n\n| Module      | Purpose                                                                        |\n| ----------- | ------------------------------------------------------------------------------ |\n| `cc.state`  | Key-value store with optional persistence and TTL                              |\n| `cc.events` | Unified event system for custom events and realtime subscriptions |\n| `cc.db`     | PocketBase wrapper for auth, CRUD operations, and file handling                |\n\nAll modules work together seamlessly. State changes emit events. Database operations emit events. Subscribing to database events automatically enables realtime updates.\n\n---\n\n## API Reference\n\n### cc.state\n\nA reactive key-value store with optional persistence to `sessionStorage` or `localStorage`.\n\n#### `state.get\u003cT\u003e(key: string): T | undefined`\n\nRetrieve a value from state.\n\n```javascript\nconst username = cc.state.get('username');\nconst user = cc.state.get('currentUser'); // Returns object if stored\n```\n\n#### `state.set(key: string, value: unknown, options?: StateOptions): void`\n\nStore a value. Emits `state:{key}` event on change.\n\n```javascript\n// Memory only (cleared on page refresh)\ncc.state.set('tempData', { foo: 'bar' });\n\n// Persist to sessionStorage (cleared when tab closes)\ncc.state.set('sessionData', 'value', { persist: 'session' });\n\n// Persist to localStorage (persists across sessions)\ncc.state.set('preferences', { theme: 'dark' }, { persist: 'local' });\n\n// With TTL (auto-expires after 5 minutes)\ncc.state.set('cache', data, { ttl: 300000 });\n\n// Combine persistence and TTL\ncc.state.set('token', 'abc123', { persist: 'local', ttl: 3600000 });\n```\n\n**StateOptions:**\n\n| Option    | Type                     | Description                  |\n| --------- | ------------------------ | ---------------------------- |\n| `persist` | `'session'` \\| `'local'` | Storage backend              |\n| `ttl`     | `number`                 | Time to live in milliseconds |\n\n#### `state.has(key: string): boolean`\n\nCheck if a key exists (and hasn't expired).\n\n```javascript\nif (cc.state.has('user')) {\n    // User data exists\n}\n```\n\n#### `state.remove(key: string): void`\n\nRemove a key from state. Emits `state:{key}` event.\n\n```javascript\ncc.state.remove('tempData');\n```\n\n#### `state.list(): Array\u003c{ key: string; storage: 'memory' | 'session' | 'local' }\u003e`\n\nList all state keys and their storage locations (useful for debugging).\n\n```javascript\ncc.state.set('user', { name: 'Pat' });\ncc.state.set('theme', 'dark', { persist: 'local' });\n\nconsole.log(cc.state.list());\n// [\n//   { key: 'user', storage: 'memory' },\n//   { key: 'theme', storage: 'local' }\n// ]\n```\n\n#### `state.clear(): void`\n\nClear all state (memory and persisted). Does not emit events.\n\n```javascript\ncc.state.clear();\n```\n\n---\n\n### cc.events\n\nUnified event system supporting custom events and automatic realtime subscriptions.\n\n#### `events.on(event: string, callback: EventCallback): void`\n\nSubscribe to an event.\n\n```javascript\ncc.events.on('auth:login', ({ user }) =\u003e {\n    console.log('User logged in:', user.email);\n});\n\ncc.events.on('state:theme', ({ value, oldValue }) =\u003e {\n    console.log('Theme changed from', oldValue, 'to', value);\n});\n```\n\n#### `events.off(event: string, callback: EventCallback): void`\n\nUnsubscribe from an event.\n\n```javascript\nconst handler = ({ user }) =\u003e console.log(user);\ncc.events.on('auth:login', handler);\ncc.events.off('auth:login', handler);\n```\n\n#### `events.once(event: string, callback: EventCallback): void`\n\nSubscribe to an event once (auto-unsubscribes after first call).\n\n```javascript\ncc.events.once('auth:login', ({ user }) =\u003e {\n    showWelcomeModal(user);\n});\n```\n\n#### `events.emit\u003cT\u003e(event: string, payload?: T): void`\n\nEmit a custom event.\n\n```javascript\ncc.events.emit('cart:updated', { itemCount: 5 });\ncc.events.emit('notification', { message: 'Item added!', type: 'success' });\n```\n\n#### `events.clear(event?: string): void`\n\nRemove all handlers for an event, or all events if no name provided.\n\n```javascript\ncc.events.clear('auth:login'); // Clear specific event\ncc.events.clear(); // Clear all events\n```\n\n#### `events.list(): Array\u003c{ event: string }\u003e`\n\nList all active listeners (useful for debugging).\n\n```javascript\nconsole.log(cc.events.list());\n// [\n//   { event: 'auth:login' },\n//   { event: 'db:posts:create' }\n// ]\n```\n\n---\n\n### cc.db\n\nPocketBase wrapper with authentication, CRUD operations, realtime, and file handling.\n\n#### Configuration\n\n```javascript\n// Change PocketBase URL (default: window.location.origin)\ncc.db.url = 'https://api.example.com';\n\n// Enable auto-cancellation for duplicate requests (default: false)\ncc.db.autoCancellation = true;\n\n// Get underlying PocketBase client for advanced usage\nconst pb = cc.db.client();\n```\n\n#### Authentication State\n\n```javascript\n// Check if user is authenticated\nif (cc.db.isAuthenticated()) {\n  console.log('User is logged in');\n}\n\n// Get current user\nconst user = cc.db.getUser();\nconsole.log(user.email, user.id);\n\n// With custom user type\nconst user = cc.db.getUser\u003c{ email: string; role: string }\u003e();\n```\n\n#### Sign Up\n\n```javascript\nconst user = await cc.db.signup('user@example.com', 'password123');\n\n// With additional data\nconst user = await cc.db.signup('user@example.com', 'password123', {\n    name: 'John Doe',\n    role: 'member',\n});\n```\n\n#### Login\n\n```javascript\n// Email/password\nconst user = await cc.db.login('user@example.com', 'password123');\n\n// OAuth2\nconst user = await cc.db.loginWithOAuth('google');\nconst user = await cc.db.loginWithOAuth('github');\n```\n\n#### Logout\n\n```javascript\ncc.db.logout();\n```\n\n#### Token Refresh\n\n```javascript\nconst user = await cc.db.refreshAuth();\n```\n\n#### Password Reset\n\n```javascript\n// Request reset email\nawait cc.db.resetPassword('user@example.com');\n\n// Confirm reset (from email link)\nawait cc.db.confirmResetPassword(token, 'newPassword123');\n```\n\n#### Email Verification\n\n```javascript\n// Request verification email\nawait cc.db.requestVerification('user@example.com');\n\n// Confirm verification (from email link)\nawait cc.db.confirmVerification(token);\n```\n\n#### CRUD Operations\n\n##### Get Single Record\n\n```javascript\nconst post = await cc.db.get('posts', 'RECORD_ID');\n\n// With expand\nconst post = await cc.db.get('posts', 'RECORD_ID', { expand: 'author' });\n```\n\n##### List Records (Paginated)\n\n```javascript\nconst result = await cc.db.list('posts');\n// { page: 1, perPage: 20, totalItems: 100, totalPages: 5, items: [...] }\n\n// With options\nconst result = await cc.db.list('posts', {\n    page: 2,\n    perPage: 10,\n    filter: 'status = \"published\"',\n    sort: '-created',\n    expand: 'author,comments',\n});\n```\n\n##### Get All Records\n\n```javascript\nconst allPosts = await cc.db.getAll('posts');\n\n// With filter\nconst myPosts = await cc.db.getAll('posts', {\n    filter: `author = \"${userId}\"`,\n});\n```\n\n##### Get First Matching Record\n\n```javascript\nconst post = await cc.db.getFirst('posts', 'slug = \"hello-world\"');\n// Returns null if not found (doesn't throw)\n```\n\n##### Create Record\n\n```javascript\nconst post = await cc.db.create('posts', {\n    title: 'Hello World',\n    content: 'My first post',\n    status: 'draft',\n});\n```\n\n##### Update Record\n\n```javascript\nconst updated = await cc.db.update('posts', 'RECORD_ID', {\n    status: 'published',\n});\n```\n\n##### Delete Record\n\n```javascript\nawait cc.db.delete('posts', 'RECORD_ID');\n```\n\n#### File Uploads\n\nUse `FormData` with `create` or `update`:\n\n```javascript\n// Create with file\nconst form = new FormData();\nform.append('title', 'My Image');\nform.append('image', fileInput.files[0]);\n\nconst record = await cc.db.create('gallery', form);\n\n// Update with file\nconst form = new FormData();\nform.append('avatar', fileInput.files[0]);\n\nawait cc.db.update('users', userId, form);\n```\n\n#### Get File URL\n\n```javascript\nconst url = cc.db.getFileUrl(record, record.image);\n\n// With thumbnail\nconst thumbUrl = cc.db.getFileUrl(record, record.image, { thumb: '100x100' });\n```\n\n---\n\n## Events Reference\n\n### Authentication Events\n\n| Event                 | Payload                                              | Triggered When                                    |\n| --------------------- | ---------------------------------------------------- | ------------------------------------------------- |\n| `auth:change`         | `{ user: DbUser \\| null, isAuthenticated: boolean }` | Auth state changes (login, logout, token refresh) |\n| `auth:signup`         | `{ user: DbUser }`                                   | User signs up                                     |\n| `auth:login`          | `{ user: DbUser }`                                   | User logs in                                      |\n| `auth:logout`         | `{ user: DbUser \\| null }`                           | User logs out                                     |\n| `auth:refresh`        | `{ user: DbUser }`                                   | Auth token refreshed                              |\n| `auth:reset-request`  | `{ email: string }`                                  | Password reset requested                          |\n| `auth:reset-confirm`  | `{}`                                                 | Password reset confirmed                          |\n| `auth:verify-request` | `{ email: string }`                                  | Email verification requested                      |\n| `auth:verify-confirm` | `{}`                                                 | Email verification confirmed                      |\n\n### Database Events (Realtime)\n\nSubscribe to database events to automatically enable realtime updates.\n\n**Collection-wide format:** `db:{collection}:{action}`\n**Record-specific format:** `db:{collection}:{action}:{id}` (update/delete only)\n\n| Event Pattern                 | Payload                   | Description             |\n| ----------------------------- | ------------------------- | ----------------------- |\n| `db:{collection}:create`      | `{ record: RecordModel }` | Any record created      |\n| `db:{collection}:update`      | `{ record: RecordModel }` | Any record updated      |\n| `db:{collection}:delete`      | `{ id: string }`          | Any record deleted      |\n| `db:{collection}:update:{id}` | `{ record: RecordModel }` | Specific record updated |\n| `db:{collection}:delete:{id}` | `{ id: string }`          | Specific record deleted |\n\n**Examples:**\n\n```javascript\n// Listen to all posts updates\ncc.events.on('db:posts:create', ({ record }) =\u003e {\n    console.log('New post:', record.title);\n});\n\ncc.events.on('db:posts:update', ({ record }) =\u003e {\n    console.log('Post updated:', record.id);\n});\n\n// Listen to a specific post only\ncc.events.on('db:posts:update:abc123', ({ record }) =\u003e {\n    console.log('Post abc123 was updated:', record.title);\n});\n\ncc.events.on('db:posts:delete:abc123', ({ id }) =\u003e {\n    console.log('Post abc123 was deleted');\n});\n```\n\n**Automatic Realtime Management:**\n\n-   Subscribing to any `db:*` event automatically enables PocketBase realtime\n-   Collection-wide events use `subscribe('*', ...)` for all records\n-   Record-specific events use `subscribe(recordId, ...)` for targeted updates\n-   Unsubscribing from the last listener automatically disables the subscription\n-   No manual subscription management required\n\n### State Events\n\n| Event Pattern | Payload                                 | Description                  |\n| ------------- | --------------------------------------- | ---------------------------- |\n| `state:{key}` | `{ value: unknown, oldValue: unknown }` | State key changed or deleted |\n\n**Examples:**\n\n```javascript\ncc.events.on('state:theme', ({ value, oldValue }) =\u003e {\n    document.body.className = value;\n});\n\ncc.events.on('state:cart', ({ value }) =\u003e {\n    updateCartBadge(value?.items?.length || 0);\n});\n```\n\n---\n\n## Complete Examples\n\n### User Authentication Flow\n\n```html\n\u003cform id=\"auth-form\"\u003e\n    \u003cinput type=\"email\" name=\"email\" placeholder=\"Email\" required /\u003e\n    \u003cinput type=\"password\" name=\"password\" placeholder=\"Password\" required /\u003e\n    \u003cbutton type=\"submit\"\u003eLogin\u003c/button\u003e\n    \u003cbutton type=\"button\" id=\"signup-btn\"\u003eSign Up\u003c/button\u003e\n    \u003cbutton type=\"button\" id=\"google-btn\"\u003eLogin with Google\u003c/button\u003e\n\u003c/form\u003e\n\u003cdiv id=\"user-area\" style=\"display: none;\"\u003e\n    \u003cspan id=\"user-email\"\u003e\u003c/span\u003e\n    \u003cbutton id=\"logout-btn\"\u003eLogout\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\n    // Update UI on auth changes\n    cc.events.on('auth:change', ({ user, isAuthenticated }) =\u003e {\n        document.getElementById('auth-form').style.display = isAuthenticated\n            ? 'none'\n            : 'block';\n        document.getElementById('user-area').style.display = isAuthenticated\n            ? 'block'\n            : 'none';\n        if (user) {\n            document.getElementById('user-email').textContent = user.email;\n        }\n    });\n\n    // Login form\n    document.getElementById('auth-form').addEventListener('submit', async (e) =\u003e {\n        e.preventDefault();\n        const form = new FormData(e.target);\n        try {\n            await cc.db.login(form.get('email'), form.get('password'));\n        } catch (err) {\n            alert('Login failed: ' + err.message);\n        }\n    });\n\n    // Sign up\n    document.getElementById('signup-btn').addEventListener('click', async () =\u003e {\n        const email = document.querySelector('[name=\"email\"]').value;\n        const password = document.querySelector('[name=\"password\"]').value;\n        try {\n            await cc.db.signup(email, password);\n            await cc.db.login(email, password);\n        } catch (err) {\n            alert('Signup failed: ' + err.message);\n        }\n    });\n\n    // Google OAuth\n    document.getElementById('google-btn').addEventListener('click', async () =\u003e {\n        try {\n            await cc.db.loginWithOAuth('google');\n        } catch (err) {\n            alert('OAuth failed: ' + err.message);\n        }\n    });\n\n    // Logout\n    document.getElementById('logout-btn').addEventListener('click', () =\u003e {\n        cc.db.logout();\n    });\n\u003c/script\u003e\n```\n\n### Realtime Chat Application\n\n```html\n\u003cdiv id=\"messages\"\u003e\u003c/div\u003e\n\u003cform id=\"message-form\"\u003e\n    \u003cinput\n        type=\"text\"\n        name=\"content\"\n        placeholder=\"Type a message...\"\n        required\n    /\u003e\n    \u003cbutton type=\"submit\"\u003eSend\u003c/button\u003e\n\u003c/form\u003e\n\n\u003cscript\u003e\n    const messagesDiv = document.getElementById('messages');\n    const currentUser = cc.db.getUser();\n\n    // Load existing messages\n    async function loadMessages() {\n        const messages = await cc.db.getAll('messages', {\n            sort: 'created',\n            expand: 'author',\n        });\n        messagesDiv.innerHTML = '';\n        messages.forEach(addMessageToUI);\n    }\n\n    function addMessageToUI(msg) {\n        const div = document.createElement('div');\n        div.className = 'message';\n        div.dataset.id = msg.id;\n        div.innerHTML = `\n      \u003cstrong\u003e${msg.expand?.author?.name || 'Unknown'}:\u003c/strong\u003e\n      ${msg.content}\n    `;\n        messagesDiv.appendChild(div);\n        messagesDiv.scrollTop = messagesDiv.scrollHeight;\n    }\n\n    // Send message\n    document.getElementById('message-form').addEventListener('submit', async (e) =\u003e {\n        e.preventDefault();\n        const form = new FormData(e.target);\n        await cc.db.create('messages', {\n            content: form.get('content'),\n            author: currentUser.id,\n        });\n        e.target.reset();\n    });\n\n    // Realtime: new messages\n    cc.events.on('db:messages:create', ({ record }) =\u003e {\n        addMessageToUI(record);\n    });\n\n    // Realtime: deleted messages\n    cc.events.on('db:messages:delete', ({ id }) =\u003e {\n        document.querySelector(`.message[data-id=\"${id}\"]`)?.remove();\n    });\n\n    // Initialize\n    loadMessages();\n\u003c/script\u003e\n```\n\n### Todo List with Persistence\n\n```html\n\u003cinput type=\"text\" id=\"new-todo\" placeholder=\"Add a todo...\" /\u003e\n\u003cul id=\"todo-list\"\u003e\u003c/ul\u003e\n\n\u003cscript\u003e\n    // Load todos from state or fetch from server\n    async function init() {\n        let todos = cc.state.get('todos');\n        if (!todos) {\n            todos = await cc.db.getAll('todos', {\n                filter: `user = \"${cc.db.getUser()?.id}\"`,\n            });\n            cc.state.set('todos', todos);\n        }\n        renderTodos(todos);\n    }\n\n    function renderTodos(todos) {\n        const list = document.getElementById('todo-list');\n        list.innerHTML = todos\n            .map(\n                (t) =\u003e `\n      \u003cli data-id=\"${t.id}\"\u003e\n        \u003cinput type=\"checkbox\" ${t.completed ? 'checked' : ''}\u003e\n        \u003cspan\u003e${t.title}\u003c/span\u003e\n        \u003cbutton class=\"delete-btn\"\u003eDelete\u003c/button\u003e\n      \u003c/li\u003e\n    `\n            )\n            .join('');\n    }\n\n    // React to state changes\n    cc.events.on('state:todos', ({ value }) =\u003e {\n        renderTodos(value || []);\n    });\n\n    // Add todo\n    document.getElementById('new-todo').addEventListener('keypress', async (e) =\u003e {\n        if (e.key !== 'Enter' || !e.target.value.trim()) return;\n\n        const todo = await cc.db.create('todos', {\n            title: e.target.value.trim(),\n            completed: false,\n            user: cc.db.getUser().id,\n        });\n\n        const todos = cc.state.get('todos') || [];\n        cc.state.set('todos', [...todos, todo]);\n        e.target.value = '';\n    });\n\n    // Toggle completion and delete (event delegation)\n    document.getElementById('todo-list').addEventListener('click', async (e) =\u003e {\n        const li = e.target.closest('li');\n        if (!li) return;\n        const id = li.dataset.id;\n\n        if (e.target.matches('input[type=\"checkbox\"]')) {\n            const completed = e.target.checked;\n            await cc.db.update('todos', id, { completed });\n            const todos = cc.state\n                .get('todos')\n                .map((t) =\u003e (t.id === id ? { ...t, completed } : t));\n            cc.state.set('todos', todos);\n        }\n\n        if (e.target.matches('.delete-btn')) {\n            await cc.db.delete('todos', id);\n            const todos = cc.state.get('todos').filter((t) =\u003e t.id !== id);\n            cc.state.set('todos', todos);\n        }\n    });\n\n    // Sync with realtime updates from other devices\n    cc.events.on('db:todos:create', ({ record }) =\u003e {\n        const todos = cc.state.get('todos') || [];\n        if (!todos.find((t) =\u003e t.id === record.id)) {\n            cc.state.set('todos', [...todos, record]);\n        }\n    });\n\n    cc.events.on('db:todos:update', ({ record }) =\u003e {\n        const todos = cc.state\n            .get('todos')\n            .map((t) =\u003e (t.id === record.id ? record : t));\n        cc.state.set('todos', todos);\n    });\n\n    cc.events.on('db:todos:delete', ({ id }) =\u003e {\n        const todos = cc.state.get('todos').filter((t) =\u003e t.id !== id);\n        cc.state.set('todos', todos);\n    });\n\n    init();\n\u003c/script\u003e\n```\n\n### Image Gallery with Uploads\n\n```html\n\u003cform id=\"upload-form\"\u003e\n    \u003cinput type=\"file\" name=\"image\" accept=\"image/*\" required /\u003e\n    \u003cinput type=\"text\" name=\"caption\" placeholder=\"Caption\" /\u003e\n    \u003cbutton type=\"submit\"\u003eUpload\u003c/button\u003e\n\u003c/form\u003e\n\u003cdiv id=\"gallery\"\u003e\u003c/div\u003e\n\n\u003cscript\u003e\n    async function loadGallery() {\n        const images = await cc.db.getAll('gallery', { sort: '-created' });\n        renderGallery(images);\n    }\n\n    function renderGallery(images) {\n        const gallery = document.getElementById('gallery');\n        gallery.innerHTML = images\n            .map(\n                (img) =\u003e `\n      \u003cdiv class=\"image-card\" data-id=\"${img.id}\"\u003e\n        \u003cimg src=\"${cc.db.getFileUrl(img, img.image, {\n            thumb: '300x300',\n        })}\" alt=\"${img.caption}\"\u003e\n        \u003cp\u003e${img.caption || ''}\u003c/p\u003e\n        \u003cbutton class=\"delete-btn\"\u003eDelete\u003c/button\u003e\n      \u003c/div\u003e\n    `\n            )\n            .join('');\n    }\n\n    // Upload image\n    document.getElementById('upload-form').addEventListener('submit', async (e) =\u003e {\n        e.preventDefault();\n        const form = new FormData(e.target);\n        form.append('user', cc.db.getUser().id);\n\n        await cc.db.create('gallery', form);\n        e.target.reset();\n    });\n\n    // Delete image (event delegation)\n    document.getElementById('gallery').addEventListener('click', async (e) =\u003e {\n        if (e.target.matches('.delete-btn')) {\n            const id = e.target.closest('.image-card').dataset.id;\n            await cc.db.delete('gallery', id);\n        }\n    });\n\n    // Realtime updates\n    cc.events.on('db:gallery:create', loadGallery);\n    cc.events.on('db:gallery:delete', loadGallery);\n\n    loadGallery();\n\u003c/script\u003e\n```\n\n---\n\n## Configuration\n\n### PocketBase URL\n\nBy default, Connect uses `window.location.origin`. Override for different backend:\n\n```javascript\ncc.db.url = 'https://api.myapp.com';\n```\n\n### Auto-Cancellation\n\nPocketBase auto-cancels duplicate pending requests by default. Connect disables this:\n\n```javascript\n// Default: false (requests not auto-cancelled)\ncc.db.autoCancellation = false;\n\n// Enable if you want duplicate requests cancelled\ncc.db.autoCancellation = true;\n```\n\n### Advanced PocketBase Access\n\nFor features not wrapped by Connect, access the underlying client:\n\n```javascript\nconst pb = cc.db.client();\n\n// Use any PocketBase SDK feature\npb.health.check();\npb.backups.getFullList();\n```\n\n---\n\n## TypeScript Support\n\nConnect exports TypeScript types:\n\n```typescript\nimport {\n    cc,\n    state,\n    events,\n    db,\n    StateOptions,\n    EventCallback,\n    DbUser,\n    ListOptions,\n    ListResult,\n} from 'cloud-canal-connect';\n\n// Custom user type\ninterface MyUser extends DbUser {\n    role: 'admin' | 'user';\n    plan: string;\n}\n\nconst user = cc.db.getUser\u003cMyUser\u003e();\n```\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcloudcanal%2Fconnect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcloudcanal%2Fconnect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcloudcanal%2Fconnect/lists"}