A pocketbase collection wrapper for Svelte
- rune based reactivity
- real time sync
- optimistic updates via diff
Installation
pnpm i -D runic-pocketbase-collectionPrerequisites
This library uses PocketBase's Batch API. Enable it in your PocketBase Admin UI:
Settings → Application → Enable Batch requests
Collection Usage
import PocketBase, { type RecordModel } from "pocketbase"
import { Collection, pbid } from "runic-pocketbase-collection"
// minimal pocketbase setup
const pb = new PocketBase("http://127.0.0.1:8090")
pb.autoCancellation(false)
const tasks = new Collection<RecordModel & { text?: string; done?: boolean }>(
pb.collection("tasks")
)
// creates 15 character alphanumeric id
const id = pbid()
// create
tasks.update({ [id]: { text: "New task" } })
// read
console.log(tasks.records[id]?.text) // logs: "New task"
// update
tasks.update({ [id]: { done: true } })
// delete
tasks.update({ [id]: null })
// reactivity
const task = $derived(tasks.records[id])
$effect(() => {
console.log(`${task?.done ? "done" : "todo"} : ${task?.text}`)
})
Task App Example
<script lang="ts">
import { type RecordModel } from "pocketbase"
import { Collection, pbid } from "runic-pocketbase-collection"
let { tasks }: { tasks: Collection<RecordModel & { text?: string; done?: boolean }> } = $props()
let taskList = $derived(Object.values(tasks.records))
let newTaskText = $state("")
</script>
<table>
<thead>
<tr>
<th>
<input
type="checkbox"
indeterminate={taskList.some(task => task.done) && taskList.some(task => !task.done)}
checked={!!taskList.length && taskList.every(task => task.done)}
onclick={() =>
tasks.update(
Object.fromEntries(
taskList.map(task => [task.id, { done: !taskList.every(task => task.done) }])
)
)}
/>
</th>
<th>
{taskList.filter(task => task.done).length}/{taskList.length} done
</th>
<th>
<button
onclick={() => tasks.update(Object.fromEntries(taskList.map(task => [task.id, null])))}
>
❌
</button>
</th>
</tr>
</thead>
<tbody>
{#each taskList as task (task.id)}
<tr>
<td>
<input
type="checkbox"
checked={task.done}
onclick={() => tasks.update({ [task.id]: { done: !task.done } })}
/>
</td>
<td>
<input
type="text"
value={task.text}
oninput={event => tasks.update({ [task.id]: { text: event.currentTarget.value } })}
/>
</td>
<td><button onclick={() => tasks.update({ [task.id]: null })}>❌</button></td>
</tr>
{/each}
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<input type="text" bind:value={newTaskText} placeholder="New task" />
</td>
<td>
<button
disabled={!newTaskText}
onclick={() => {
tasks.update({ [pbid()]: { text: newTaskText } })
newTaskText = ""
}}
>
✚
</button>
</td>
</tr>
</tfoot>
</table>
<style>
table,
input[type="text"] {
width: 100%;
}
</style>
Advanced Collection Usage
import PocketBase from "pocketbase"
import { Collection, pbid } from "runic-pocketbase-collection"
// explicit model type
interface TaskModel {
id: string
collectionId: string
collectionName: string
created: string
updated: string
text?: string
done?: boolean
subtasks: string[]
expand: { subtasks: TaskModel[] }
}
// minimal pocketbase setup
const pb = new PocketBase("http://127.0.0.1:8090")
pb.autoCancellation(false)
// wrap task collection and expand subtasks
const tasks = new Collection<TaskModel>(pb.collection("tasks"), {
options: { expand: "subtasks" }
})
const parentTaskId = pbid()
const childTaskId = pbid()
// create a parent and child task
await tasks.update({
[parentTaskId]: { text: "Parent task", subtasks: [childTaskId] },
[childTaskId]: { text: "Child task" }
})
// read expanded task
console.log(
tasks.records[parentTaskId]?.expand.subtasks.some(subtask => subtask.id === childTaskId)
) // logs: true
// update filter for only done tasks
tasks.updateSubscriptionOptions({ expand: "subtasks", filter: "done = true" })
Item Usage
import PocketBase, { type RecordModel } from "pocketbase"
import { Item } from "runic-pocketbase-collection"
// minimal pocketbase setup
const pb = new PocketBase("http://127.0.0.1:8090")
pb.autoCancellation(false)
// wrap a single task by id
const taskId = "abc123def456ghi" // existing task id
const task = new Item<RecordModel & { text?: string; done?: boolean }>(
pb.collection("tasks"),
taskId
)
// read
console.log(task.record?.text) // logs: "Existing task"
// update
task.update({ done: true })
// reactivity
const taskText = $derived(task.record?.text)
$effect(() => {
console.log(`Task: ${taskText}`)
})
Advanced Item Usage
import { getAbortSignal } from "svelte"
import PocketBase from "pocketbase"
import { Item } from "runic-pocketbase-collection"
// explicit model type
interface TaskModel {
id: string
collectionId: string
collectionName: string
created: string
updated: string
text?: string
done?: boolean
priority?: number
}
// minimal pocketbase setup
const pb = new PocketBase("http://127.0.0.1:8090")
pb.autoCancellation(false)
// wrap a single task with subscription options
const taskId = "abc123def456ghi" // existing task id
const task = new Item<TaskModel>(pb.collection("tasks"), taskId, {
// real-time subscription options
options: { expand: "subtasks" }
})
// refetch on mount with cancellation
$effect(() => {
task.refetch({
options: { signal: getAbortSignal() },
onError: error => console.error("Failed to refetch:", error.message)
})
})
// optimistic update with override
await task.update(
{ priority: 1 },
{ onError: error => console.error("Failed to update:", error.message) }
)
// read updated record
console.log(task.record?.priority) // logs: 1
// update subscription options to truncate the text to 200 characters
task.updateSubscriptionOptions({ expand: "subtasks", fields: "*,text:excerpt(200,true)" })
Development
You'll need to add a pocketbase executable to the database directory.
The following start script assumes you have pnpm / tmux / fish installed.
pnpm ipnpm run start# pocketbase now running at http://127.0.0.1:8090/_# sveltekit app now running at http://localhost:5173