Skip to content

Instantly share code, notes, and snippets.

@maxnowack
Created January 7, 2026 10:20
Show Gist options
  • Select an option

  • Save maxnowack/f2f9a5d23e44610fdbb3ad57ef0744a3 to your computer and use it in GitHub Desktop.

Select an option

Save maxnowack/f2f9a5d23e44610fdbb3ad57ef0744a3 to your computer and use it in GitHub Desktop.
SignalDB Capacitor Filesystem Persistence Adapter
import { createPersistenceAdapter } from '@signaldb/core'
import { parse as deserialize, stringify as serialize } from 'devalue'
export default function createFilePersistenceAdapter<
T extends { id: I } & Record<string, any>,
I,
>(name: string) {
const filePath = `signaldb-collection-${name}.json`
async function getItems(): Promise<T[]> {
const { ensureFolder, fileExists, readFile, writeFile } = await import('filestorage.ts')
await ensureFolder(filePath)
if (!(await fileExists(filePath))) {
await writeFile(filePath, '[[]]')
}
const content = await readFile(filePath)
return deserialize(content)
}
return createPersistenceAdapter<T, I>({
async load() {
if (typeof window === 'undefined' || process.env.NODE_ENV === 'test') return { items: [] } // no-op in test environment or on server
const items = await getItems()
return { items }
},
async save(_items, { added, modified, removed }) {
if (typeof window === 'undefined' || process.env.NODE_ENV === 'test') return // no-op in test environment or on server
const currentItems = await getItems()
added.forEach((item) => {
const index = currentItems.findIndex(({ id }) => id === item.id)
if (index === -1) {
currentItems.push(item)
} else {
currentItems[index] = item
}
})
modified.forEach((item) => {
const index = currentItems.findIndex(({ id }) => id === item.id)
if (index === -1) {
currentItems.push(item)
} else {
currentItems[index] = item
}
})
removed.forEach((item) => {
const index = currentItems.findIndex(({ id }) => id === item.id)
if (index === -1) return
currentItems.splice(index, 1)
})
const { writeFile } = await import('system/filestorage')
await writeFile(filePath, serialize(currentItems))
},
async register() {
// no-op
},
})
}
import path from 'path'
import { Directory, Encoding, Filesystem } from '@capacitor/filesystem'
const directory: Directory = Directory.Library
// Helper function that checks if an entry with a given name exists in the specified directory path.
async function entryExists(directoryPath: string, entryName: string) {
const readdirResult = await Filesystem.readdir({
directory,
// Map '/', '.', and './' to an empty string because these values represent the root or current directory.
// This ensures compatibility with the Filesystem API, which expects an empty string for the base directory.
path: ['/', '.', './'].includes(directoryPath) ? '' : directoryPath,
})
return readdirResult.files.some((file: { name: string }) => file.name === entryName)
}
export async function ensureFolder(filePath: string) {
const folderPath: string = path.dirname(filePath)
// If there's no directory portion, nothing to do.
if (folderPath === '.' || folderPath === '') return
// Split the folder path into its components.
const folderParts: string[] = folderPath.split(path.sep).filter(Boolean)
let currentPath: string = ''
// Walk through each level of the path.
for (const part of folderParts) {
currentPath = currentPath ? path.join(currentPath, part) : part
// For top-level folders, the parent's path is empty (base directory).
const parentPath: string = currentPath.includes(path.sep) ? path.dirname(currentPath) : ''
// Use the helper to check if the current folder exists in its parent directory.
const exists: boolean = await entryExists(parentPath, part)
if (!exists) {
await Filesystem.mkdir({
directory,
path: currentPath,
})
}
}
}
export async function fileExists(filePath: string) {
const folderPath: string = path.dirname(filePath)
const fileName: string = path.basename(filePath)
return entryExists(folderPath === '.' ? '' : folderPath, fileName)
}
export async function readFile(filePath: string) {
return Filesystem.readFile({
directory,
path: filePath,
encoding: Encoding.UTF8,
}).then(result => result.data as string)
}
export async function writeFile(filePath: string, text: string) {
await ensureFolder(filePath)
await Filesystem.writeFile({
directory,
path: filePath,
data: text,
encoding: Encoding.UTF8,
})
}
export async function listFiles(filePath: string = '') {
const readdirResult = await Filesystem.readdir({
directory,
path: filePath,
})
return readdirResult.files.map((i: { name: string }) => `${filePath}/${i.name}`)
}
export async function removeFile(filePath: string) {
if (!await fileExists(filePath)) return
await Filesystem.deleteFile({
directory,
path: filePath,
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment