Created
January 7, 2026 10:20
-
-
Save maxnowack/f2f9a5d23e44610fdbb3ad57ef0744a3 to your computer and use it in GitHub Desktop.
SignalDB Capacitor Filesystem Persistence Adapter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }, | |
| }) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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