JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 88
  • Score
    100M100P100Q63045F
  • License MIT

wsfs: local-first virtual filesystem with IndexedDB cache and pluggable backend persistence.

Package Exports

  • @mfukala/wsfs
  • @mfukala/wsfs/client
  • @mfukala/wsfs/server

Readme

wsfs

wsfs (Web Sync File System) is local-first virtual filesystem with IndexedDB caching, optimistic concurrency, and pluggable backend persistence.

The package ships:

  • A browser client (@mfukala/wsfs/client) that caches reads/writes locally, syncs in batches, and surfaces conflicts
  • A server toolkit (@mfukala/wsfs/server) that wires the sync protocol into any persistence adapter (an in-memory adapter is included)
  • No runtime dependencies

Browser client (@mfukala/wsfs/client)

import { Wsfs } from "@mfukala/wsfs/client";

const wsfs = await Wsfs.init({
  namespace: "vault", // IndexedDB namespace
  backendUrl: "http://localhost:8787", // your API base URL
});

// Mutations run inside a write task (exclusive lock + rollback on task failure).
await wsfs.runWriteTask(async (fs) => {
  await fs.write("/primary/item.json", JSON.stringify({ hello: "world" }));
});

// Reads run in read tasks (can run concurrently).
const item = await wsfs.runReadTask((fs) => fs.read("/primary/item.json"));

// Batch push local changes and pull remote updates.
await wsfs.sync();
  • runWriteTaskAndSync keeps the write lock through the sync; local edits rollback only if the task throws, not if sync fails.
  • list(prefix?) hides local tombstones; info(path) returns { etag, encoding, updatedBy? }.
  • Conflicts surface via a conflict event:
wsfs.addEventListener("conflict", (event) => {
  console.warn("Conflict detected", event.detail);
});
  • Pass a codec (encode/decode) to encrypt/compress payloads before hitting storage or the network.

Server toolkit (@mfukala/wsfs/server)

Hook the protocol into any Node.js framework. createWsfsApi exposes the core sync/read/write/delete/list methods and works with any PersistenceAdapter. An in-memory adapter ships with the package for testing.

Express server example

import express from "express";
import { createWsfsApi, MemoryPersistence } from "@mfukala/wsfs/server";

const persistence = new MemoryPersistence(); // bring your own adapter in production
const api = createWsfsApi(persistence);
const app = express();
app.use(express.json({ limit: "5mb" }));

app.post("/sync", async (req, res) => {
  try {
    res.status(200).json(await api.sync(req.body));
  } catch (err: any) {
    res.status(err?.status ?? 500).json({ error: err?.message ?? "sync failed" });
  }
});

app.get("/file", async (req, res) => {
  const file = await api.getFile(String(req.query.path ?? ""));
  if (!file) return res.status(404).end();
  res.json(file);
});

app.get("/file/info", async (req, res) => {
  const info = await api.getFileInfo(String(req.query.path ?? ""));
  if (!info) return res.status(404).end();
  res.json(info);
});

app.get("/list", async (req, res) => {
  res.json(await api.list(String(req.query.prefix ?? "/")));
});

app.put("/file", async (req, res) => {
  try {
    const result = await api.putFile({
      path: req.body.path,
      content: req.body.content,
      contentBase64: req.body.contentBase64,
      encoding: req.body.encoding,
      ifMatch: req.headers["if-match"] as string | undefined,
      updatedBy: req.body.updatedBy,
    });
    res.status(200).json(result);
  } catch (err: any) {
    res.status(err?.status ?? 500).json({ error: err?.message ?? "put failed" });
  }
});

app.delete("/file", async (req, res) => {
  try {
    await api.deleteFile({
      path: String(req.query.path ?? ""),
      ifMatch: req.headers["if-match"] as string | undefined,
    });
    res.status(204).end();
  } catch (err: any) {
    res.status(err?.status ?? 500).json({ error: err?.message ?? "delete failed" });
  }
});

app.listen(8787);

Next.js API Route example

// pages/api/wsfs/sync.ts (or app/api/wsfs/sync/route.ts)
import { createWsfsApi, MemoryPersistence } from "@mfukala/wsfs/server";

const api = createWsfsApi(new MemoryPersistence()); // swap in your adapter

export default async function handler(req, res) {
  try {
    const result = await api.sync(req.body);
    res.status(200).json(result);
  } catch (err: any) {
    res.status(err?.status ?? 500).json({ error: err?.message ?? "sync failed" });
  }
}

Reuse the same pattern for /file, /file/info, and /list, passing the request payloads into api.putFile, api.getFile, api.getFileInfo, and api.list while preserving the If-Match header for writes/deletes.

Server endpoints the client expects

  • POST /sync with { prefix, writes, deletes, known, watermark }
  • GET /file?path=/foo.txt{ etag, encoding, updatedBy?, content|contentBase64 }
  • GET /file/info?path=/foo.txt{ etag, encoding, updatedBy? }
  • GET /list?prefix=/dir/[{ path, etag, encoding }]
  • PUT /file + If-Match: <etag|*>{ etag }
  • DELETE /file?path=... + If-Match: <etag|*>

The bundled MemoryPersistence enforces If-Match (use "*" to create new files), tracks updatedBy, and exposes incremental sync via watermarks.