Repository: typeonce-dev/sync-engine-web
Branch: main
Commit: 956aaa9cec0f
Files: 68
Total size: 101.0 KB
Directory structure:
gitextract_usv87bki/
├── .gitignore
├── .npmrc
├── .vscode/
│ └── settings.json
├── README.md
├── apps/
│ ├── client/
│ │ ├── .gitignore
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-food.ts
│ │ │ │ │ └── use-meal.ts
│ │ │ │ ├── runtime-client.ts
│ │ │ │ └── services/
│ │ │ │ └── storage.ts
│ │ │ ├── main.tsx
│ │ │ ├── routeTree.gen.ts
│ │ │ ├── routes/
│ │ │ │ ├── $workspaceId/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── join.tsx
│ │ │ │ │ └── token.tsx
│ │ │ │ ├── __root.tsx
│ │ │ │ └── index.tsx
│ │ │ └── workers/
│ │ │ ├── bootstrap.ts
│ │ │ └── live.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── server/
│ ├── drizzle/
│ │ ├── 0000_supreme_bedlam.sql
│ │ └── meta/
│ │ ├── 0000_snapshot.json
│ │ └── _journal.json
│ ├── drizzle.config.ts
│ ├── package.json
│ ├── src/
│ │ ├── database.ts
│ │ ├── db/
│ │ │ └── schema.ts
│ │ ├── group/
│ │ │ ├── sync-auth.ts
│ │ │ └── sync-data.ts
│ │ ├── main.ts
│ │ ├── middleware/
│ │ │ ├── authorization.ts
│ │ │ ├── master-authorization.ts
│ │ │ └── version-check.ts
│ │ └── services/
│ │ ├── drizzle.ts
│ │ └── jwt.ts
│ └── tsconfig.json
├── docker-compose.yaml
├── package.json
├── packages/
│ ├── client-lib/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── main.ts
│ │ │ ├── runtime-layer.ts
│ │ │ ├── schema.ts
│ │ │ ├── services/
│ │ │ │ ├── api-client.ts
│ │ │ │ ├── dexie.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── loro-storage.ts
│ │ │ │ ├── migration.ts
│ │ │ │ ├── sync.ts
│ │ │ │ ├── temp-workspace.ts
│ │ │ │ └── workspace-manager.ts
│ │ │ ├── sync-worker.ts
│ │ │ ├── use-action-effect.ts
│ │ │ └── use-dexie-query.ts
│ │ └── tsconfig.json
│ ├── schema/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── main.ts
│ │ │ ├── migrations.ts
│ │ │ ├── schema.ts
│ │ │ └── versioning.ts
│ │ └── tsconfig.json
│ └── sync/
│ ├── package.json
│ ├── src/
│ │ └── main.ts
│ └── tsconfig.json
├── pnpm-workspace.yaml
└── turbo.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem
================================================
FILE: .npmrc
================================================
================================================
FILE: .vscode/settings.json
================================================
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}
================================================
FILE: README.md
================================================
# Sync Engine Web
A local-first, offline-capable web sync engine implementation with CRDT-based synchronization. The project provides a complete solution for data synchronization between multiple devices while maintaining data consistency and offline capabilities.
## Architecture
The project is structured as a monorepo with three main components:
1. **Client**
- Web-based UI implementation
- Local-first storage using IndexedDB ([`dexie`](https://dexie.org/))
- Background sync using Web Workers
- Client-side schema migrations
- Offline-first data management
2. **Server**
- REST API for data synchronization
- JWT-based authentication and authorization
- Byte-level data storage (client-agnostic)
- Workspace management and backup
- Token-based access control
3. **Shared Packages**
- Schema definitions
- Type definitions
- Shared utilities
- Migration utilities
## Key Features
- **Local-First Architecture**: Data is primarily stored and managed locally, with server acting as sync + backup
- **CRDT-Based Sync**: Uses [`loro-crdt`](https://loro.dev/) for conflict-free data synchronization
- **Offline Support**: Full offline capability with background sync
- **Multi-Device Access**: Share workspaces across devices using access tokens
- **Type-Safe Migrations**: Version-controlled schema migrations
- **Secure Authentication**: JWT-based auth with master/access token system
## Implementation Details
- Client stores and syncs data using CRDT (Conflict-free Replicated Data Type)
- Server is client-agnostic, storing only byte-level data
- Master client controls workspace access through token generation
- Schema migrations are performed client-side
- Background syncing handled by Web Workers
- No server-side querying - all data operations happen client-side
## Authentication Flow
1. Client creates local workspace
2. Server stores workspace and designates client as "master"
3. Master client receives master token
4. Master client generates access tokens for other devices
5. Access tokens can be shared via links
6. Server handles token verification and authorization
## Data Flow
```
Client (Local Storage) <-> Web Worker (Background Sync) <-> Server (Byte Storage)
```
- UI reads directly from local storage
- Sync operations happen in background
- Server stores encoded CRDT data as bytes
- Migrations happen during client initialization
## Technologies
- CRDT: `loro-crdt` for data synchronization
- Storage: IndexedDB (client), Database (server)
- Authentication: JWT tokens
- Background Processing: Web Workers
## Dependencies
### Client
- **UI & Routing**
- `react` - UI framework
- `@tanstack/react-router` - Type-safe routing
- **Storage & Sync**
- `loro-crdt` - CRDT implementation
- `dexie` - IndexedDB wrapper
- `dexie-react-hooks` - React hooks for Dexie
- **Core**
- `effect` - Functional programming toolkit
- `@effect/platform-browser` - Browser-specific Effect utilities
### Server
- **Database**
- `drizzle-orm` - TypeScript ORM
- `@effect/sql` - SQL integration for Effect
- `@effect/sql-pg` - PostgreSQL driver
- `pg` - PostgreSQL client
- **Authentication**
- `jsonwebtoken` - JWT implementation
- **Core**
- `effect` - Functional programming toolkit
- `@effect/platform-node` - Node.js-specific Effect utilities
### Build Tools
- `turbo` - Monorepo build system
- `vite` - Frontend build tool
- `typescript` - Type system
## Project Status
This is a functional implementation of a web sync engine, optimized for single-user scenarios (not real-time collaboration). The focus is on providing reliable data synchronization while maintaining offline capabilities.
================================================
FILE: apps/client/.gitignore
================================================
# Local
.DS_Store
*.local
*.log*
# Dist
node_modules
dist/
.vinxi
.output
.vercel
.netlify
.wrangler
# IDE
.vscode/*
!.vscode/extensions.json
.idea
================================================
FILE: apps/client/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TanStack Router</title>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
html {
color-scheme: light dark;
}
* {
@apply border-gray-200 dark:border-gray-800;
}
body {
@apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: apps/client/package.json
================================================
{
"name": "client",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "vite --port=3001",
"build": "vite build",
"serve": "vite preview",
"start": "vite"
},
"devDependencies": {
"@tanstack/router-plugin": "^1.105.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.2",
"vite": "^6.0.3",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.4.1"
},
"dependencies": {
"@effect/platform": "^0.77.2",
"@effect/platform-browser": "^0.56.2",
"@local/sync": "workspace:*",
"@local/schema": "workspace:*",
"@local/client-lib": "workspace:*",
"@tanstack/react-router": "^1.105.0",
"dexie": "^4.0.11",
"dexie-react-hooks": "^1.1.7",
"effect": "^3.13.2",
"loro-crdt": "^1.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
================================================
FILE: apps/client/src/lib/constants.ts
================================================
export const WEBSITE_URL = "http://localhost:3001";
================================================
FILE: apps/client/src/lib/hooks/use-food.ts
================================================
import { Service, useDexieQuery } from "@local/client-lib";
import { SnapshotSchema } from "@local/schema";
import { RuntimeClient } from "../runtime-client";
export const useFood = ({ workspaceId }: { workspaceId: string }) => {
return useDexieQuery(() =>
RuntimeClient.runPromise(
Service.LoroStorage.use(({ query }) =>
query((doc) => doc.getList("food"), SnapshotSchema.fields.food.value, {
workspaceId,
})
)
)
);
};
================================================
FILE: apps/client/src/lib/hooks/use-meal.ts
================================================
import { Service, useDexieQuery } from "@local/client-lib";
import { SnapshotSchema } from "@local/schema";
import { Effect } from "effect";
import { RuntimeClient } from "../runtime-client";
export const useMeal = ({ workspaceId }: { workspaceId: string }) => {
return useDexieQuery(() =>
RuntimeClient.runPromise(
Effect.gen(function* () {
const { query } = yield* Service.LoroStorage;
const meals = yield* query(
(doc) => doc.getList("meal"),
SnapshotSchema.fields.meal.value,
{ workspaceId }
);
const foods = yield* query(
(doc) => doc.getList("food"),
SnapshotSchema.fields.food.value,
{ workspaceId }
);
return meals.map(({ foodId, id, quantity }) => {
const food = foods.find((food) => food.id === foodId);
return { id, quantity, food };
});
})
)
);
};
================================================
FILE: apps/client/src/lib/runtime-client.ts
================================================
import { RuntimeLayer } from "@local/client-lib";
import { Layer, ManagedRuntime } from "effect";
import { Storage } from "./services/storage";
const MainLayer = Layer.mergeAll(RuntimeLayer, Storage.Default);
export const RuntimeClient = ManagedRuntime.make(MainLayer);
================================================
FILE: apps/client/src/lib/services/storage.ts
================================================
import { Service } from "@local/client-lib";
import { CurrentSchema, SnapshotSchema } from "@local/schema";
import { Effect, Schema } from "effect";
import { LoroMap, VersionVector } from "loro-crdt";
export class Storage extends Effect.Service<Storage>()("Storage", {
dependencies: [Service.TempWorkspace.Default, Service.LoroStorage.Default],
effect: Effect.gen(function* () {
const temp = yield* Service.TempWorkspace;
const { load } = yield* Service.LoroStorage;
const insert =
<T extends typeof SnapshotSchema.Table.Type>(table: T) =>
({
workspaceId,
value,
}: {
workspaceId: string;
value: Schema.Schema.Encoded<(typeof CurrentSchema.fields)[T]>[number];
}) =>
Effect.gen(function* () {
const { doc, workspace } = yield* load({ workspaceId });
const list = doc.getList(table);
const container = list.insertContainer(list.length, new LoroMap());
const data = yield* Schema.encode(
CurrentSchema.fields[table].value as Schema.Schema<typeof value>
)(value);
Object.entries(data).forEach(([key, val]) => {
container.set(key, val);
});
const snapshotExport =
workspace === undefined
? doc.export({ mode: "snapshot" })
: doc.export({
mode: "update",
from: new VersionVector(workspace.version),
});
return yield* temp.put({
workspaceId,
snapshot: snapshotExport,
snapshotId: crypto.randomUUID(),
});
});
return {
insertFood: insert("food"),
insertMeal: insert("meal"),
} as const;
}),
}) {}
================================================
FILE: apps/client/src/main.tsx
================================================
import { RouterProvider, createRouter } from "@tanstack/react-router";
import ReactDOM from "react-dom/client";
import { routeTree } from "./routeTree.gen";
// Set up a Router instance
const router = createRouter({
routeTree,
defaultPreload: "intent",
});
// Register things for typesafety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const rootElement = document.getElementById("app")!;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />);
}
================================================
FILE: apps/client/src/routeTree.gen.ts
================================================
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as WorkspaceIdIndexImport } from './routes/$workspaceId/index'
import { Route as WorkspaceIdTokenImport } from './routes/$workspaceId/token'
import { Route as WorkspaceIdJoinImport } from './routes/$workspaceId/join'
// Create/Update Routes
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const WorkspaceIdIndexRoute = WorkspaceIdIndexImport.update({
id: '/$workspaceId/',
path: '/$workspaceId/',
getParentRoute: () => rootRoute,
} as any)
const WorkspaceIdTokenRoute = WorkspaceIdTokenImport.update({
id: '/$workspaceId/token',
path: '/$workspaceId/token',
getParentRoute: () => rootRoute,
} as any)
const WorkspaceIdJoinRoute = WorkspaceIdJoinImport.update({
id: '/$workspaceId/join',
path: '/$workspaceId/join',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/$workspaceId/join': {
id: '/$workspaceId/join'
path: '/$workspaceId/join'
fullPath: '/$workspaceId/join'
preLoaderRoute: typeof WorkspaceIdJoinImport
parentRoute: typeof rootRoute
}
'/$workspaceId/token': {
id: '/$workspaceId/token'
path: '/$workspaceId/token'
fullPath: '/$workspaceId/token'
preLoaderRoute: typeof WorkspaceIdTokenImport
parentRoute: typeof rootRoute
}
'/$workspaceId/': {
id: '/$workspaceId/'
path: '/$workspaceId'
fullPath: '/$workspaceId'
preLoaderRoute: typeof WorkspaceIdIndexImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/$workspaceId/join': typeof WorkspaceIdJoinRoute
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
'/$workspaceId': typeof WorkspaceIdIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/$workspaceId/join': typeof WorkspaceIdJoinRoute
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
'/$workspaceId': typeof WorkspaceIdIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/$workspaceId/join': typeof WorkspaceIdJoinRoute
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
'/$workspaceId/': typeof WorkspaceIdIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/$workspaceId/join'
| '/$workspaceId/token'
| '/$workspaceId'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/$workspaceId/join' | '/$workspaceId/token' | '/$workspaceId'
id:
| '__root__'
| '/'
| '/$workspaceId/join'
| '/$workspaceId/token'
| '/$workspaceId/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
WorkspaceIdJoinRoute: typeof WorkspaceIdJoinRoute
WorkspaceIdTokenRoute: typeof WorkspaceIdTokenRoute
WorkspaceIdIndexRoute: typeof WorkspaceIdIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
WorkspaceIdJoinRoute: WorkspaceIdJoinRoute,
WorkspaceIdTokenRoute: WorkspaceIdTokenRoute,
WorkspaceIdIndexRoute: WorkspaceIdIndexRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/$workspaceId/join",
"/$workspaceId/token",
"/$workspaceId/"
]
},
"/": {
"filePath": "index.tsx"
},
"/$workspaceId/join": {
"filePath": "$workspaceId/join.tsx"
},
"/$workspaceId/token": {
"filePath": "$workspaceId/token.tsx"
},
"/$workspaceId/": {
"filePath": "$workspaceId/index.tsx"
}
}
}
ROUTE_MANIFEST_END */
================================================
FILE: apps/client/src/routes/$workspaceId/index.tsx
================================================
import { Worker } from "@effect/platform";
import { BrowserWorker } from "@effect/platform-browser";
import { Service, SyncWorker, useActionEffect } from "@local/client-lib";
import { createFileRoute, Link } from "@tanstack/react-router";
import { Effect } from "effect";
import { startTransition, useEffect } from "react";
import { useFood } from "../../lib/hooks/use-food";
import { useMeal } from "../../lib/hooks/use-meal";
import { RuntimeClient } from "../../lib/runtime-client";
import { Storage } from "../../lib/services/storage";
const bootstrap = ({ workspaceId }: { workspaceId: string }) =>
Effect.gen(function* () {
const pool = yield* Worker.makePoolSerialized({ size: 1 });
return yield* pool.broadcast(new SyncWorker.Bootstrap({ workspaceId }));
}).pipe(
Effect.scoped,
Effect.provide(
BrowserWorker.layer(
() =>
new globalThis.Worker(
new URL("./src/workers/bootstrap.ts", globalThis.origin),
{ type: "module" }
)
)
),
Effect.catchAll((error) => Effect.logError("Bootstrap error", error))
);
export const Route = createFileRoute("/$workspaceId/")({
component: RouteComponent,
loader: ({ params: { workspaceId } }) =>
RuntimeClient.runPromise(
Service.WorkspaceManager.getById({ workspaceId }).pipe(
Effect.flatMap(Effect.fromNullable),
Effect.tap(({ workspaceId }) => bootstrap({ workspaceId }))
)
),
});
function RouteComponent() {
const workspace = Route.useLoaderData();
const { data, error, loading } = useFood({
workspaceId: workspace.workspaceId,
});
const { data: meals } = useMeal({
workspaceId: workspace.workspaceId,
});
const [, onBootstrap, bootstrapping] = useActionEffect(
RuntimeClient,
bootstrap
);
const [, onAddFood] = useActionEffect(RuntimeClient, (formData: FormData) =>
Effect.gen(function* () {
const loroStorage = yield* Storage;
const name = formData.get("name") as string;
const calories = formData.get("calories") as string;
yield* loroStorage.insertFood({
workspaceId: workspace.workspaceId,
value: {
id: crypto.randomUUID(),
name,
calories: parseInt(calories, 10),
},
});
})
);
const [, onAddMeal] = useActionEffect(RuntimeClient, (formData: FormData) =>
Effect.gen(function* () {
const loroStorage = yield* Storage;
const foodId = formData.get("foodId") as string;
const quantity = formData.get("quantity") as string;
yield* loroStorage.insertMeal({
workspaceId: workspace.workspaceId,
value: {
id: crypto.randomUUID(),
foodId,
quantity: parseInt(quantity, 10),
},
});
})
);
useEffect(() => {
const url = new URL("./src/workers/live.ts", globalThis.origin);
const newWorker = new globalThis.Worker(url, { type: "module" });
void RuntimeClient.runPromise(
Effect.gen(function* () {
const pool = yield* Worker.makePoolSerialized({ size: 1 });
return yield* pool.broadcast(
new SyncWorker.LiveQuery({ workspaceId: workspace.workspaceId })
);
}).pipe(
Effect.scoped,
Effect.provide(BrowserWorker.layer(() => newWorker))
)
);
newWorker.onerror = (error) => {
console.error("Live query worker error", error);
};
return () => {
newWorker.terminate();
};
}, []);
return (
<div>
<Link
to="/$workspaceId/token"
params={{ workspaceId: workspace.workspaceId }}
>
Tokens
</Link>
<p>{workspace.workspaceId}</p>
<button
disabled={bootstrapping}
onClick={() =>
startTransition(() =>
onBootstrap({ workspaceId: workspace.workspaceId })
)
}
>
{bootstrapping ? "Bootstrapping..." : "Bootstrap"}
</button>
<form action={onAddFood}>
<input type="text" name="name" />
<input type="number" name="calories" min={1} />
<button type="submit">Add food</button>
</form>
<div>
{loading && <p>Loading...</p>}
{error && <pre>{JSON.stringify(error, null, 2)}</pre>}
{(data ?? []).map((food) => (
<div key={food.id}>
<p>Name: {food.name}</p>
<p>Calories: {food.calories}</p>
</div>
))}
</div>
<form action={onAddMeal}>
{(data ?? []).map((food) => (
<div key={food.id}>
<input type="radio" name="foodId" id={food.id} value={food.id} />
<label htmlFor={food.id}>{food.name}</label>
</div>
))}
<input type="number" name="quantity" min={1} />
<button type="submit">Add meal</button>
</form>
<div>
{(meals ?? []).map((meal) => (
<div key={meal.id}>
<p>Food: {meal.food?.name}</p>
<p>Quantity: {meal.quantity}</p>
</div>
))}
</div>
</div>
);
}
================================================
FILE: apps/client/src/routes/$workspaceId/join.tsx
================================================
import { Service } from "@local/client-lib";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { Effect } from "effect";
import { RuntimeClient } from "../../lib/runtime-client";
export const Route = createFileRoute("/$workspaceId/join")({
component: RouteComponent,
loader: ({ params }) =>
RuntimeClient.runPromise(
Effect.gen(function* () {
const { join } = yield* Service.Sync;
yield* join({ workspaceId: params.workspaceId });
return redirect({
to: `/$workspaceId`,
params: { workspaceId: params.workspaceId },
});
})
),
});
function RouteComponent() {
return null;
}
================================================
FILE: apps/client/src/routes/$workspaceId/token.tsx
================================================
import { Service, useActionEffect } from "@local/client-lib";
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
import { Duration, Effect } from "effect";
import { WEBSITE_URL } from "../../lib/constants";
import { RuntimeClient } from "../../lib/runtime-client";
export const Route = createFileRoute("/$workspaceId/token")({
component: RouteComponent,
loader: ({ params: { workspaceId } }) =>
RuntimeClient.runPromise(
Effect.gen(function* () {
const api = yield* Service.ApiClient;
const token = yield* Service.WorkspaceManager.getById({
workspaceId,
}).pipe(
Effect.flatMap((workspace) => Effect.fromNullable(workspace?.token))
);
const tokens = yield* api.client.syncAuth.listTokens({
path: { workspaceId },
headers: { "x-api-key": token },
});
return { tokens, token };
})
),
});
function RouteComponent() {
const { workspaceId } = Route.useParams();
const { tokens, token } = Route.useLoaderData();
const router = useRouter();
const [, onIssueToken, issuing] = useActionEffect(
RuntimeClient,
(formData: FormData) =>
Effect.gen(function* () {
const api = yield* Service.ApiClient;
const clientId = formData.get("clientId") as string;
yield* api.client.syncAuth.issueToken({
path: { workspaceId },
headers: { "x-api-key": token },
payload: {
clientId,
expiresIn: Duration.days(30),
scope: "read_write",
},
});
yield* Effect.promise(() => router.invalidate({ sync: true }));
})
);
const [, onRevoke, revoking] = useActionEffect(
RuntimeClient,
(formData: FormData) =>
Effect.gen(function* () {
const api = yield* Service.ApiClient;
const clientId = formData.get("clientId") as string;
yield* api.client.syncAuth.revokeToken({
path: { workspaceId, clientId },
headers: { "x-api-key": token },
});
yield* Effect.promise(() => router.invalidate({ sync: true }));
})
);
return (
<div>
<Link to={`/$workspaceId`} params={{ workspaceId }}>
Back
</Link>
<h1>Tokens</h1>
<table>
<thead>
<tr>
<th>Index</th>
<th>clientId</th>
<th>isMaster</th>
<th>scope</th>
<th>issuedAt</th>
<th>expiresAt</th>
<th>revokedAt</th>
<th>Share</th>
</tr>
</thead>
<tbody>
{tokens.map((token, index) => (
<tr key={index}>
<td>{index}</td>
<td>{token.clientId}</td>
<td>{token.isMaster ? "✔️" : "❌"}</td>
<td>{token.scope}</td>
<td>
{new Date(token.issuedAt).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
})}
</td>
<td>
{token.expiresAt
? new Date(token.expiresAt).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
})
: "N/A"}
</td>
<td>
{token.revokedAt ? (
<span>
{new Date(token.revokedAt).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
})}
</span>
) : (
<form action={onRevoke}>
<input
type="hidden"
name="clientId"
value={token.clientId}
/>
<button type="submit" disabled={revoking}>
Revoke access
</button>
</form>
)}
</td>
<td>
<button
type="button"
onClick={() =>
navigator.clipboard.writeText(
`${WEBSITE_URL}/${workspaceId}/join`
)
}
>
Share
</button>
</td>
</tr>
))}
</tbody>
</table>
<form action={onIssueToken}>
<input type="text" name="clientId" />
<button type="submit" disabled={issuing}>
Issue token
</button>
</form>
</div>
);
}
================================================
FILE: apps/client/src/routes/__root.tsx
================================================
import { Service } from "@local/client-lib";
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { Effect } from "effect";
import { RuntimeClient } from "../lib/runtime-client";
export const Route = createRootRoute({
component: RootComponent,
loader: () =>
RuntimeClient.runPromise(
Service.Migration.pipe(
Effect.flatMap((migration) => migration.migrate),
Effect.catchAll((error) => Effect.logError("Migration error", error)),
Effect.andThen(
Effect.gen(function* () {
const { initClient } = yield* Service.Dexie;
return yield* initClient;
})
)
)
),
});
function RootComponent() {
const clientId = Route.useLoaderData();
return (
<>
<nav>
<span>{clientId}</span>
</nav>
<Outlet />
</>
);
}
================================================
FILE: apps/client/src/routes/index.tsx
================================================
import { Service, useActionEffect } from "@local/client-lib";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { Effect } from "effect";
import { RuntimeClient } from "../lib/runtime-client";
export const Route = createFileRoute("/")({
component: HomeComponent,
loader: () => RuntimeClient.runPromise(Service.WorkspaceManager.getAll),
});
function HomeComponent() {
const allWorkspaces = Route.useLoaderData();
const navigate = useNavigate();
const [, joinWorkspace] = useActionEffect(RuntimeClient, () =>
Effect.gen(function* () {
const workspace = yield* Service.WorkspaceManager.create;
yield* Effect.sync(() =>
navigate({
to: `/$workspaceId`,
params: { workspaceId: workspace.workspaceId },
})
);
})
);
return (
<div>
<p>Select workspace</p>
{allWorkspaces.map((workspace) => (
<Link
key={workspace.workspaceId}
to="/$workspaceId"
params={{ workspaceId: workspace.workspaceId }}
>
{workspace.workspaceId}
</Link>
))}
<div>
<button type="button" onClick={joinWorkspace}>
Create workspace
</button>
</div>
</div>
);
}
================================================
FILE: apps/client/src/workers/bootstrap.ts
================================================
import { WorkerRunner } from "@effect/platform";
import { BrowserWorkerRunner } from "@effect/platform-browser";
import { SyncWorker } from "@local/client-lib";
import { Effect, Layer } from "effect";
import { RuntimeClient } from "../lib/runtime-client";
const WorkerLive = WorkerRunner.layerSerialized(SyncWorker.WorkerMessage, {
Bootstrap: (params) =>
Effect.gen(function* () {
const worker = yield* SyncWorker.SyncWorker;
return yield* worker.bootstrap(params);
}),
}).pipe(Layer.provide(BrowserWorkerRunner.layer));
RuntimeClient.runFork(WorkerRunner.launch(WorkerLive));
================================================
FILE: apps/client/src/workers/live.ts
================================================
import { WorkerRunner } from "@effect/platform";
import { BrowserWorkerRunner } from "@effect/platform-browser";
import { SyncWorker } from "@local/client-lib";
import { Effect, Layer } from "effect";
import { RuntimeClient } from "../lib/runtime-client";
const WorkerLive = WorkerRunner.layer((params: SyncWorker.LiveQuery) =>
Effect.scoped(
Effect.gen(function* () {
const worker = yield* SyncWorker.SyncWorker;
yield* Effect.fork(worker.liveSync({ workspaceId: params.workspaceId }));
yield* Effect.never;
})
)
).pipe(Layer.provide(BrowserWorkerRunner.layer));
RuntimeClient.runFork(WorkerRunner.launch(WorkerLive));
================================================
FILE: apps/client/tsconfig.json
================================================
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,
// "exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noEmit": true,
"lib": ["es2022", "dom", "dom.iterable"],
"types": ["vite/client"]
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: apps/client/vite.config.ts
================================================
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import topLevelAwait from "vite-plugin-top-level-await";
import wasm from "vite-plugin-wasm";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite({}), react(), wasm(), topLevelAwait()],
worker: { format: "es" },
});
================================================
FILE: apps/server/drizzle/0000_supreme_bedlam.sql
================================================
CREATE TYPE "public"."scope" AS ENUM('read', 'read_write');--> statement-breakpoint
CREATE TABLE "client" (
"clientId" uuid NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "token" (
"tokenId" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "token_tokenId_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"tokenValue" varchar NOT NULL,
"clientId" uuid NOT NULL,
"workspaceId" uuid NOT NULL,
"isMaster" boolean DEFAULT false NOT NULL,
"scope" "scope" NOT NULL,
"issuedAt" timestamp DEFAULT now() NOT NULL,
"expiresAt" timestamp,
"revokedAt" timestamp
);
--> statement-breakpoint
CREATE TABLE "workspace" (
"workspaceId" uuid NOT NULL,
"ownerClientId" uuid NOT NULL,
"clientId" uuid NOT NULL,
"snapshotId" uuid NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"snapshot" "bytea" NOT NULL,
CONSTRAINT "workspace_snapshotId_unique" UNIQUE("snapshotId")
);
================================================
FILE: apps/server/drizzle/meta/0000_snapshot.json
================================================
{
"id": "b089a447-bbae-4d52-b83e-a0ce1cc4b359",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.client": {
"name": "client",
"schema": "",
"columns": {
"clientId": {
"name": "clientId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.token": {
"name": "token",
"schema": "",
"columns": {
"tokenId": {
"name": "tokenId",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "token_tokenId_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"tokenValue": {
"name": "tokenValue",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"clientId": {
"name": "clientId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workspaceId": {
"name": "workspaceId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isMaster": {
"name": "isMaster",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"scope": {
"name": "scope",
"type": "scope",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"issuedAt": {
"name": "issuedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"revokedAt": {
"name": "revokedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"workspaceId": {
"name": "workspaceId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"ownerClientId": {
"name": "ownerClientId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"clientId": {
"name": "clientId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"snapshotId": {
"name": "snapshotId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"snapshot": {
"name": "snapshot",
"type": "bytea",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"workspace_snapshotId_unique": {
"name": "workspace_snapshotId_unique",
"nullsNotDistinct": false,
"columns": [
"snapshotId"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.scope": {
"name": "scope",
"schema": "public",
"values": [
"read",
"read_write"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/server/drizzle/meta/_journal.json
================================================
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1740889946454,
"tag": "0000_supreme_bedlam",
"breakpoints": true
}
]
}
================================================
FILE: apps/server/drizzle.config.ts
================================================
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "postgresql",
});
================================================
FILE: apps/server/package.json
================================================
{
"name": "@local/server",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"typecheck": "tsc",
"dev": "tsx watch src/main.ts",
"generate": "pnpm drizzle-kit generate"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.5.1",
"drizzle-kit": "^0.30.4",
"tsx": "^4.19.2"
},
"dependencies": {
"@effect/platform": "^0.77.2",
"@effect/platform-node": "^0.73.1",
"@effect/sql": "^0.30.2",
"@effect/sql-pg": "^0.31.2",
"@local/sync": "workspace:*",
"@local/schema": "workspace:*",
"drizzle-orm": "^0.39.3",
"effect": "^3.13.2",
"jsonwebtoken": "^9.0.2",
"pg": "^8.13.3"
}
}
================================================
FILE: apps/server/src/database.ts
================================================
import { PgClient } from "@effect/sql-pg";
import { Config, Effect, Layer, Redacted } from "effect";
const password = Config.redacted("POSTGRES_PASSWORD");
const username = Config.string("POSTGRES_USERNAME");
const database = Config.string("POSTGRES_DATABASE");
const host = Config.string("POSTGRES_HOST");
const port = Config.number("POSTGRES_PORT");
export const DatabaseUrl = Config.all({
username,
password,
host,
port,
database,
}).pipe(
Config.map(({ username, password, host, port, database }) =>
Redacted.make(
`postgresql://${username}:${Redacted.value(password)}@${host}:${port}/${database}`
)
)
);
export const DatabaseLive = DatabaseUrl.pipe(
Effect.tap((url) =>
Effect.log(`Connecting to database: ${Redacted.value(url)}`)
),
Effect.map((url) => PgClient.layer({ url })),
Layer.unwrapEffect
);
================================================
FILE: apps/server/src/db/schema.ts
================================================
import {
boolean,
customType,
integer,
pgEnum,
pgTable,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
export const scope = pgEnum("scope", ["read", "read_write"]);
export const bytea = customType<{ data: Uint8Array }>({
dataType: () => "bytea",
});
export const workspaceTable = pgTable("workspace", {
workspaceId: uuid().notNull(),
ownerClientId: uuid().notNull(),
clientId: uuid().notNull(),
snapshotId: uuid().notNull().unique(),
createdAt: timestamp().notNull().defaultNow(),
snapshot: bytea().notNull(),
});
export const clientTable = pgTable("client", {
clientId: uuid().notNull(),
createdAt: timestamp().notNull().defaultNow(),
});
export const tokenTable = pgTable("token", {
tokenId: integer().primaryKey().generatedAlwaysAsIdentity(),
tokenValue: varchar().notNull(),
clientId: uuid().notNull(),
workspaceId: uuid().notNull(),
isMaster: boolean().notNull().default(false),
scope: scope().notNull(),
issuedAt: timestamp().notNull().defaultNow(),
expiresAt: timestamp(),
revokedAt: timestamp(),
});
================================================
FILE: apps/server/src/group/sync-auth.ts
================================================
import { HttpApiBuilder } from "@effect/platform";
import { AuthWorkspace, Scope, SyncApi } from "@local/sync";
import { and, eq, not } from "drizzle-orm";
import { DateTime, Effect, Layer, Schema } from "effect";
import { tokenTable, workspaceTable } from "../db/schema";
import { AuthorizationLive } from "../middleware/authorization";
import { MasterAuthorizationLive } from "../middleware/master-authorization";
import { VersionCheckLive } from "../middleware/version-check";
import { Drizzle } from "../services/drizzle";
import { Jwt } from "../services/jwt";
export const SyncAuthGroupLive = HttpApiBuilder.group(
SyncApi,
"syncAuth",
(handlers) =>
Effect.gen(function* () {
const jwt = yield* Jwt;
const { query } = yield* Drizzle;
return handlers
.handle("generateToken", ({ payload }) =>
Effect.gen(function* () {
yield* Effect.log(
`Generating token for workspace ${payload.workspaceId}`
);
const scope: typeof Scope.Type = "read_write";
const isMaster = true;
const issuedAt = DateTime.toDate(yield* DateTime.now);
yield* query({
Request: Schema.Struct({ workspaceId: Schema.String }),
execute: (db, { workspaceId }) =>
db
.select()
.from(workspaceTable)
.where(eq(workspaceTable.workspaceId, workspaceId)),
})({ workspaceId: payload.workspaceId }).pipe(
Effect.flatMap((rows) =>
rows.length === 0
? Effect.void
: Effect.fail({ message: "Workspace already exists" })
)
);
const token = yield* jwt.sign({
clientId: payload.clientId,
workspaceId: payload.workspaceId,
});
yield* Effect.all([
query({
Request: Schema.Struct({
clientId: Schema.String,
workspaceId: Schema.String,
snapshot: Schema.Uint8ArrayFromSelf,
}),
execute: (db, { clientId, snapshot, workspaceId }) =>
db.insert(workspaceTable).values({
snapshot,
clientId,
workspaceId,
ownerClientId: clientId,
snapshotId: payload.snapshotId,
}),
})({
clientId: payload.clientId,
snapshot: payload.snapshot,
workspaceId: payload.workspaceId,
}),
query({
Request: Schema.Struct({
clientId: Schema.String,
workspaceId: Schema.String,
tokenValue: Schema.String,
}),
execute: (db, { clientId, tokenValue, workspaceId }) =>
db.insert(tokenTable).values({
clientId,
scope,
tokenValue,
workspaceId,
isMaster,
issuedAt,
expiresAt: null,
revokedAt: null,
}),
})({
clientId: payload.clientId,
tokenValue: token,
workspaceId: payload.workspaceId,
}),
]);
return {
token,
workspaceId: payload.workspaceId,
snapshot: payload.snapshot,
createdAt: issuedAt,
};
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
)
.handle("listTokens", ({ path: { workspaceId } }) =>
Effect.gen(function* () {
const workspace = yield* AuthWorkspace;
const tokens = yield* query({
Request: Schema.Struct({
workspaceId: Schema.String,
clientId: Schema.String,
}),
execute: (db, { workspaceId, clientId }) =>
db
.select()
.from(tokenTable)
.where(
and(
eq(tokenTable.workspaceId, workspaceId),
not(eq(tokenTable.clientId, clientId))
)
),
})({ workspaceId, clientId: workspace.clientId });
return tokens.map((token) => ({
clientId: token.clientId,
tokenValue: token.tokenValue,
scope: token.scope,
isMaster: token.isMaster,
issuedAt: token.issuedAt,
expiresAt: token.expiresAt,
revokedAt: token.revokedAt,
}));
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
)
.handle("issueToken", ({ path: { workspaceId }, payload }) =>
Effect.gen(function* () {
yield* Effect.log(`Issuing token for workspace ${workspaceId}`);
const issuedAt = yield* DateTime.now;
const expiresAt = issuedAt.pipe(
DateTime.addDuration(payload.expiresIn),
DateTime.toDate
);
const token = yield* jwt.sign({
clientId: payload.clientId,
workspaceId,
});
yield* query({
Request: Schema.Struct({
clientId: Schema.String,
workspaceId: Schema.String,
tokenValue: Schema.String,
scope: Scope,
expiresAt: Schema.DateFromSelf,
issuedAt: Schema.DateFromSelf,
}),
execute: (
db,
{
clientId,
tokenValue,
workspaceId,
scope,
expiresAt,
issuedAt,
}
) =>
db.insert(tokenTable).values({
clientId,
scope,
tokenValue,
workspaceId,
issuedAt,
expiresAt,
isMaster: false,
revokedAt: null,
}),
})({
expiresAt,
workspaceId,
tokenValue: token,
scope: payload.scope,
clientId: payload.clientId,
issuedAt: DateTime.toDate(issuedAt),
});
return {
token,
expiresAt,
scope: payload.scope,
};
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
)
.handle("revokeToken", ({ path: { workspaceId, clientId } }) =>
Effect.gen(function* () {
yield* Effect.log(`Revoking token for workspace ${workspaceId}`);
const revokedAt = yield* DateTime.now;
yield* query({
Request: Schema.Struct({
workspaceId: Schema.String,
clientId: Schema.String,
}),
execute: (db, { workspaceId, clientId }) =>
db
.update(tokenTable)
.set({ revokedAt: DateTime.toDate(revokedAt) })
.where(
and(
eq(tokenTable.workspaceId, workspaceId),
eq(tokenTable.clientId, clientId)
)
),
})({ workspaceId, clientId });
return true;
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
);
})
).pipe(
Layer.provide([
Drizzle.Default,
AuthorizationLive,
MasterAuthorizationLive,
VersionCheckLive,
Jwt.Default,
])
);
================================================
FILE: apps/server/src/group/sync-data.ts
================================================
import { HttpApiBuilder } from "@effect/platform";
import { SnapshotToLoroDoc } from "@local/schema";
import { AuthWorkspace, SyncApi } from "@local/sync";
import { and, desc, eq, gt, isNull, or } from "drizzle-orm";
import { Array, Data, Effect, Layer, Schema } from "effect";
import { tokenTable, workspaceTable } from "../db/schema";
import { AuthorizationLive } from "../middleware/authorization";
import { VersionCheckLive } from "../middleware/version-check";
import { Drizzle } from "../services/drizzle";
class InvalidVersionError extends Data.TaggedError("InvalidVersionError")<{
reason: "missing" | "outdated";
}> {}
export const SyncDataGroupLive = HttpApiBuilder.group(
SyncApi,
"syncData",
(handlers) =>
Effect.gen(function* () {
const { query } = yield* Drizzle;
return handlers
.handle(
"push",
({ payload: { snapshot, snapshotId }, path: { workspaceId } }) =>
Effect.gen(function* () {
const workspace = yield* AuthWorkspace;
const doc = yield* Schema.decode(SnapshotToLoroDoc)(snapshot);
yield* Effect.log(`Pushing workspace ${workspaceId}`);
doc.import(workspace.snapshot);
const newSnapshot = yield* Schema.encode(SnapshotToLoroDoc)(doc);
const newWorkspace = yield* query({
Request: Schema.Struct({
newSnapshot: Schema.Uint8ArrayFromSelf,
}),
execute: (db, { newSnapshot: snapshot }) =>
db
.insert(workspaceTable)
.values({
snapshot,
snapshotId,
clientId: workspace.clientId,
workspaceId: workspace.workspaceId,
ownerClientId: workspace.ownerClientId,
})
.returning(),
})({ newSnapshot }).pipe(Effect.flatMap(Array.head));
return {
workspaceId: newWorkspace.workspaceId,
createdAt: newWorkspace.createdAt,
snapshot: newWorkspace.snapshot,
};
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
)
.handle("pull", ({ path: { workspaceId } }) =>
Effect.gen(function* () {
const workspace = yield* query({
Request: Schema.Struct({ workspaceId: Schema.String }),
execute: (db, { workspaceId }) =>
db
.select()
.from(workspaceTable)
.where(eq(workspaceTable.workspaceId, workspaceId))
.orderBy(desc(workspaceTable.createdAt))
.limit(1),
})({ workspaceId }).pipe(
Effect.flatMap(Array.head),
Effect.mapError(() => ({ message: "Missing workspace" }))
);
return { snapshot: workspace.snapshot };
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
)
.handle("join", ({ path: { workspaceId, clientId } }) =>
Effect.gen(function* () {
const { tokenValue } = yield* query({
Request: Schema.Struct({
clientId: Schema.String,
workspaceId: Schema.String,
}),
execute: (db, { clientId, workspaceId }) =>
db
.select()
.from(tokenTable)
.where(
and(
eq(tokenTable.workspaceId, workspaceId),
eq(tokenTable.clientId, clientId),
or(
isNull(tokenTable.revokedAt),
gt(tokenTable.revokedAt, new Date())
),
or(
isNull(tokenTable.expiresAt),
gt(tokenTable.expiresAt, new Date())
)
)
)
.orderBy(desc(tokenTable.issuedAt))
.limit(1),
})({ clientId, workspaceId }).pipe(
Effect.flatMap(Array.head),
Effect.mapError(() => ({ message: "Missing token" }))
);
const workspace = yield* query({
Request: Schema.Struct({ workspaceId: Schema.String }),
execute: (db, { workspaceId }) =>
db
.select()
.from(workspaceTable)
.where(eq(workspaceTable.workspaceId, workspaceId))
.orderBy(desc(workspaceTable.createdAt))
.limit(1),
})({ workspaceId }).pipe(
Effect.flatMap(Array.head),
Effect.mapError(() => ({ message: "Missing workspace" }))
);
return {
token: tokenValue,
snapshot: workspace.snapshot,
};
}).pipe(
Effect.tapErrorCause(Effect.logError),
Effect.mapError((error) => error.message)
)
);
})
).pipe(Layer.provide([Drizzle.Default, AuthorizationLive, VersionCheckLive]));
================================================
FILE: apps/server/src/main.ts
================================================
import {
HttpApiBuilder,
HttpMiddleware,
HttpServer,
PlatformConfigProvider,
} from "@effect/platform";
import {
NodeFileSystem,
NodeHttpServer,
NodeRuntime,
} from "@effect/platform-node";
import { SyncApi } from "@local/sync";
import { Effect, Layer } from "effect";
import { createServer } from "node:http";
import { SyncAuthGroupLive } from "./group/sync-auth";
import { SyncDataGroupLive } from "./group/sync-data";
import { Drizzle } from "./services/drizzle";
const EnvProviderLayer = Layer.unwrapEffect(
PlatformConfigProvider.fromDotEnv(".env").pipe(
Effect.map(Layer.setConfigProvider),
Effect.provide(NodeFileSystem.layer)
)
);
const MainApiLive = HttpApiBuilder.api(SyncApi).pipe(
Layer.provide([Drizzle.Default, SyncDataGroupLive, SyncAuthGroupLive]),
Layer.provide(EnvProviderLayer)
);
const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Layer.provide(HttpApiBuilder.middlewareCors()),
Layer.provide(MainApiLive),
HttpServer.withLogAddress,
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
);
NodeRuntime.runMain(Layer.launch(HttpLive));
================================================
FILE: apps/server/src/middleware/authorization.ts
================================================
import {
Authorization,
ClientId,
DatabaseError,
MissingWorkspace,
Unauthorized,
WorkspaceId,
} from "@local/sync";
import { and, desc, eq, gt, isNull, or } from "drizzle-orm";
import { Array, Effect, Layer, Match, Redacted, Schema } from "effect";
import { tokenTable, workspaceTable } from "../db/schema";
import { Drizzle } from "../services/drizzle";
import { Jwt } from "../services/jwt";
export const AuthorizationLive = Layer.effect(
Authorization,
Effect.gen(function* () {
const jwt = yield* Jwt;
const { query } = yield* Drizzle;
yield* Effect.log("Creating Authorization middleware");
return {
apiKey: (apiKey) =>
Effect.gen(function* () {
yield* Effect.log("Api key", Redacted.value(apiKey));
const tokenPayload = yield* jwt.decode({ apiKey });
yield* Effect.log(`Valid auth ${tokenPayload.workspaceId}`);
yield* query({
Request: Schema.Struct({
workspaceId: WorkspaceId,
clientId: ClientId,
}),
execute: (db, { workspaceId, clientId }) =>
db
.select()
.from(tokenTable)
.where(
and(
eq(tokenTable.workspaceId, workspaceId),
eq(tokenTable.clientId, clientId),
or(
isNull(tokenTable.revokedAt),
gt(tokenTable.revokedAt, new Date())
),
or(
isNull(tokenTable.expiresAt),
gt(tokenTable.expiresAt, new Date())
)
)
)
.orderBy(desc(tokenTable.issuedAt))
.limit(1),
})({
workspaceId: tokenPayload.workspaceId,
clientId: tokenPayload.sub,
}).pipe(
Effect.flatMap(Array.head),
Effect.tapError(Effect.logError),
Effect.mapError(
() =>
new Unauthorized({
message: "Missing, expired, or revoked token",
})
)
);
return yield* query({
Request: Schema.Struct({
workspaceId: Schema.UUID,
}),
execute: (db, { workspaceId }) =>
db
.select()
.from(workspaceTable)
.where(eq(workspaceTable.workspaceId, workspaceId))
.orderBy(desc(workspaceTable.createdAt))
.limit(1),
})({ workspaceId: tokenPayload.workspaceId }).pipe(
Effect.flatMap(Array.head)
);
}).pipe(
Effect.mapError((error) =>
Match.value(error).pipe(
Match.tagsExhaustive({
NoSuchElementException: () => new MissingWorkspace(),
Unauthorized: (error) => error,
JwtError: () => new Unauthorized({ message: "Invalid token" }),
ParseError: () =>
new Unauthorized({ message: "Invalid parameters" }),
QueryError: () => new DatabaseError(),
})
)
)
),
};
})
).pipe(Layer.provide([Drizzle.Default, Jwt.Default]));
================================================
FILE: apps/server/src/middleware/master-authorization.ts
================================================
import {
DatabaseError,
MasterAuthorization,
MissingWorkspace,
Unauthorized,
} from "@local/sync";
import { and, desc, eq } from "drizzle-orm";
import { Array, Effect, Layer, Match, Redacted, Schema } from "effect";
import { workspaceTable } from "../db/schema";
import { Drizzle } from "../services/drizzle";
import { Jwt } from "../services/jwt";
export const MasterAuthorizationLive = Layer.effect(
MasterAuthorization,
Effect.gen(function* () {
const jwt = yield* Jwt;
const { query } = yield* Drizzle;
yield* Effect.log("Creating Master Authorization middleware");
return {
apiKey: (apiKey) =>
Effect.gen(function* () {
yield* Effect.log("Api key", Redacted.value(apiKey));
const tokenPayload = yield* jwt.decode({ apiKey });
if (tokenPayload.isMaster) {
return yield* query({
Request: Schema.Struct({
workspaceId: Schema.UUID,
clientId: Schema.UUID,
}),
execute: (db, { workspaceId, clientId }) =>
db
.select()
.from(workspaceTable)
.where(
and(
eq(workspaceTable.workspaceId, workspaceId),
eq(workspaceTable.clientId, clientId)
)
)
.orderBy(desc(workspaceTable.createdAt))
.limit(1),
})({
clientId: tokenPayload.sub,
workspaceId: tokenPayload.workspaceId,
}).pipe(Effect.flatMap(Array.head));
}
return yield* new Unauthorized({ message: "Not master access" });
}).pipe(
Effect.mapError((error) =>
Match.value(error).pipe(
Match.tagsExhaustive({
NoSuchElementException: () => new MissingWorkspace(),
Unauthorized: (error) => error,
JwtError: () => new Unauthorized({ message: "Invalid token" }),
ParseError: () =>
new Unauthorized({ message: "Invalid parameters" }),
QueryError: () => new DatabaseError(),
})
)
)
),
};
})
).pipe(Layer.provide([Drizzle.Default, Jwt.Default]));
================================================
FILE: apps/server/src/middleware/version-check.ts
================================================
import { HttpServerRequest } from "@effect/platform";
import { SnapshotToLoroDoc, VERSION } from "@local/schema";
import { Snapshot, VersionCheck, VersionError } from "@local/sync";
import { Effect, Layer, Schema } from "effect";
export const VersionCheckLive = Layer.effect(
VersionCheck,
Effect.gen(function* () {
yield* Effect.log("Creating version check middleware");
return Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
yield* Effect.log("Checking version");
const { snapshot } = yield* request.json.pipe(
Effect.flatMap(
Schema.decodeUnknown(
Schema.Struct({
snapshot: Snapshot,
})
)
),
Effect.mapError(() => new VersionError({ reason: "missing-snapshot" }))
);
const doc = yield* Schema.decode(SnapshotToLoroDoc)(snapshot).pipe(
Effect.mapError(() => new VersionError({ reason: "invalid-doc" }))
);
yield* Effect.log("Checking doc", doc.toJSON());
const currentVersion = doc.getMap("metadata").get("version");
yield* Effect.log("Current version", currentVersion);
if (typeof currentVersion !== "number") {
return yield* new VersionError({ reason: "missing-version" });
} else if (currentVersion !== VERSION) {
return yield* new VersionError({ reason: "outdated-version" });
}
return snapshot;
});
})
);
================================================
FILE: apps/server/src/services/drizzle.ts
================================================
import { Data, Effect, Redacted, Schema } from "effect";
import { DatabaseUrl } from "../database";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { fileURLToPath } from "node:url";
class MigrationError extends Data.TaggedError("MigrationError")<{
cause: unknown;
}> {}
class QueryError extends Data.TaggedError("QueryError")<{
cause: unknown;
}> {}
export class Drizzle extends Effect.Service<Drizzle>()("Drizzle", {
effect: Effect.gen(function* () {
const databaseUrl = yield* DatabaseUrl;
const db = drizzle(Redacted.value(databaseUrl));
yield* Effect.log("Applying migrations").pipe(
Effect.andThen(
Effect.tryPromise({
try: () =>
migrate(db, {
migrationsFolder: fileURLToPath(
new URL("../../drizzle", import.meta.url)
),
}),
catch: (error) => new MigrationError({ cause: error }),
})
),
Effect.tap(() => Effect.log("Migrations applied"))
);
const query =
<RQI, RQA, RSA>({
Request,
// Result,
execute,
}: {
Request: Schema.Schema<RQA, RQI>;
// Result: Schema.Schema<RSA, RSI>;
execute: (_: typeof db, __: RQA) => Promise<RSA>;
}) =>
(params: RQI) =>
Schema.decode(Request)(params).pipe(
Effect.flatMap((_) =>
Effect.tryPromise({
try: () => execute(db, _),
catch: (error) => new QueryError({ cause: error }),
})
)
// Effect.flatMap(Schema.decode(Result))
);
return { db, query };
}),
}) {}
================================================
FILE: apps/server/src/services/jwt.ts
================================================
import { Scope } from "@local/sync";
import { Config, Data, Effect, Redacted, Schema } from "effect";
import * as jwt from "jsonwebtoken";
class TokenPayload extends Schema.Class<TokenPayload>("TokenPayload")({
iat: Schema.Number,
exp: Schema.optional(Schema.Number),
sub: Schema.String,
workspaceId: Schema.String,
scope: Scope,
isMaster: Schema.Boolean,
}) {}
class JwtError extends Data.TaggedError("JwtError")<{
reason: "missing" | "invalid";
}> {}
export class Jwt extends Effect.Service<Jwt>()("Jwt", {
effect: Effect.gen(function* () {
const secretKey = yield* Config.redacted("JWT_SECRET");
return {
sign: ({
clientId,
workspaceId,
}: {
clientId: string;
workspaceId: string;
}) =>
Schema.encode(TokenPayload)(
new TokenPayload({
iat: Math.floor(Date.now() / 1000),
sub: clientId,
workspaceId,
scope: "read_write",
isMaster: true,
})
).pipe(
Effect.flatMap((payload) =>
Effect.try({
try: () =>
jwt.sign(payload, Redacted.value(secretKey), {
algorithm: "HS256",
}),
catch: () => new JwtError({ reason: "invalid" }),
})
)
),
decode: ({ apiKey }: { apiKey: Redacted.Redacted<string> }) =>
Effect.gen(function* () {
const decoded = jwt.decode(Redacted.value(apiKey));
if (decoded === null) {
return yield* new JwtError({ reason: "missing" });
}
return yield* Schema.decodeUnknown(TokenPayload)(decoded).pipe(
Effect.mapError(() => new JwtError({ reason: "invalid" }))
);
}),
};
}),
}) {}
================================================
FILE: apps/server/tsconfig.json
================================================
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": ["es2022"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: docker-compose.yaml
================================================
version: "0.2"
name: app_docker
services:
postgres:
env_file: .env
container_name: postgres
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DATABASE}
ports:
- 5435:5432
pgadmin:
env_file: .env
container_name: pgadmin
image: dpage/pgadmin4:latest
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_MAIL}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PW}
ports:
- 5050:80
================================================
FILE: package.json
================================================
{
"name": "sql-database-local-config",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"typecheck": "turbo run typecheck",
"generate": "turbo run generate",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"prettier": "^3.5.0",
"turbo": "^2.4.2",
"typescript": "5.7.3"
},
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=18"
}
}
================================================
FILE: packages/client-lib/package.json
================================================
{
"name": "@local/client-lib",
"type": "module",
"scripts": {
"typecheck": "tsc"
},
"exports": {
".": "./src/main.ts"
},
"devDependencies": {
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3"
},
"peerDependencies": {
"@effect/platform": "^0.77.2",
"effect": "^3.13.2",
"@local/sync": "workspace:*",
"@local/schema": "workspace:*",
"dexie": "^4.0.11",
"dexie-react-hooks": "^1.1.7",
"loro-crdt": "^1.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"dependencies": {}
}
================================================
FILE: packages/client-lib/src/main.ts
================================================
import { RuntimeLayer } from "./runtime-layer";
import * as Service from "./services";
import * as SyncWorker from "./sync-worker";
import { useActionEffect } from "./use-action-effect";
import { useDexieQuery } from "./use-dexie-query";
export {
RuntimeLayer,
Service,
SyncWorker,
useActionEffect as useActionEffect,
useDexieQuery,
};
================================================
FILE: packages/client-lib/src/runtime-layer.ts
================================================
import { Layer } from "effect";
import { ApiClient } from "./services/api-client";
import { Dexie } from "./services/dexie";
import { LoroStorage } from "./services/loro-storage";
import { Migration } from "./services/migration";
import { Sync } from "./services/sync";
import { TempWorkspace } from "./services/temp-workspace";
import { WorkspaceManager } from "./services/workspace-manager";
import { SyncWorker } from "./sync-worker";
export const RuntimeLayer = Layer.mergeAll(
Dexie.Default,
ApiClient.Default,
WorkspaceManager.Default,
TempWorkspace.Default,
LoroStorage.Default,
Sync.Default,
Migration.Default,
SyncWorker.Default
);
================================================
FILE: packages/client-lib/src/schema.ts
================================================
import { ClientId, Snapshot, SnapshotId, WorkspaceId } from "@local/sync";
import { Schema } from "effect";
export class ClientTable extends Schema.Class<ClientTable>("ClientTable")({
clientId: ClientId,
}) {}
export class WorkspaceTable extends Schema.Class<WorkspaceTable>(
"WorkspaceTable"
)({
workspaceId: WorkspaceId,
snapshot: Snapshot,
token: Schema.NullOr(Schema.String),
version: Schema.NullOr(Schema.Uint8Array),
}) {}
export class TempWorkspaceTable extends Schema.Class<TempWorkspaceTable>(
"TempWorkspaceTable"
)({
workspaceId: WorkspaceId,
snapshot: Snapshot,
snapshotId: SnapshotId,
}) {}
================================================
FILE: packages/client-lib/src/services/api-client.ts
================================================
import { FetchHttpClient, HttpApiClient } from "@effect/platform";
import { SyncApi } from "@local/sync";
import { Effect } from "effect";
export class ApiClient extends Effect.Service<ApiClient>()("ApiClient", {
dependencies: [FetchHttpClient.layer],
effect: Effect.gen(function* () {
const client = yield* HttpApiClient.make(SyncApi, {
baseUrl: "http://localhost:3000",
});
return { client };
}),
}) {}
================================================
FILE: packages/client-lib/src/services/dexie.ts
================================================
import * as _Dexie from "dexie";
import { Data, Effect, Schema } from "effect";
import {
type ClientTable,
type TempWorkspaceTable,
type WorkspaceTable,
} from "../schema";
class QueryApiError extends Data.TaggedError("QueryApiError")<{
cause: unknown;
}> {}
class WriteApiError extends Data.TaggedError("WriteApiError")<{
cause: unknown;
}> {}
const formDataToRecord = (formData: FormData): Record<string, string> => {
const record: Record<string, string> = {};
for (const [key, value] of formData.entries()) {
if (typeof value === "string") {
record[key] = value;
}
}
return record;
};
export class Dexie extends Effect.Service<Dexie>()("Dexie", {
accessors: true,
effect: Effect.gen(function* () {
const db = new _Dexie.Dexie("_db") as _Dexie.Dexie & {
client: _Dexie.EntityTable<typeof ClientTable.Encoded, "clientId">;
workspace: _Dexie.EntityTable<
typeof WorkspaceTable.Encoded,
"workspaceId"
>;
temp_workspace: _Dexie.EntityTable<
typeof TempWorkspaceTable.Encoded,
"workspaceId"
>;
};
db.version(1).stores({
client: "clientId",
workspace: "workspaceId",
temp_workspace: "workspaceId",
});
const query = <T>(execute: (_: typeof db) => Promise<T>) =>
Effect.tryPromise({
try: () => execute(db),
catch: (error) => new QueryApiError({ cause: error }),
}).pipe(Effect.tapErrorCause(Effect.logError));
const initClient = query((_) => _.client.toCollection().last()).pipe(
Effect.map((client) => client?.clientId),
Effect.flatMap(Effect.fromNullable),
Effect.orElse(() =>
query((_) => _.client.add({ clientId: crypto.randomUUID() }))
)
);
const formAction =
<const R extends string, I, T>(
source: Schema.Schema<I, Record<R, string>>,
exec: (values: Readonly<I>) => Promise<T>
) =>
(formData: FormData) =>
Schema.decodeUnknown(source)(formDataToRecord(formData)).pipe(
Effect.mapError((error) => new WriteApiError({ cause: error })),
Effect.flatMap((values) =>
Effect.tryPromise({
try: () => exec(values),
catch: (error) => new WriteApiError({ cause: error }),
})
)
);
const changeAction =
<A, I, T>(
source: Schema.Schema<A, I>,
exec: (values: Readonly<A>) => Promise<T>
) =>
(params: I) =>
Schema.decode(source)(params).pipe(
Effect.tap(Effect.log),
Effect.mapError((error) => new WriteApiError({ cause: error })),
Effect.flatMap((values) =>
Effect.tryPromise({
try: () => exec(values),
catch: (error) => new WriteApiError({ cause: error }),
})
)
);
return { db, initClient, query };
}),
}) {}
================================================
FILE: packages/client-lib/src/services/index.ts
================================================
import { ApiClient } from "./api-client";
import { Dexie } from "./dexie";
import { LoroStorage } from "./loro-storage";
import { Migration } from "./migration";
import { Sync } from "./sync";
import { TempWorkspace } from "./temp-workspace";
import { WorkspaceManager } from "./workspace-manager";
export {
ApiClient,
Dexie,
LoroStorage,
Migration,
Sync,
TempWorkspace,
WorkspaceManager,
};
================================================
FILE: packages/client-lib/src/services/loro-storage.ts
================================================
import { SnapshotSchema, type LoroSchema } from "@local/schema";
import { Effect, Schema } from "effect";
import { LoroDoc, type LoroList, type LoroMap } from "loro-crdt";
import { TempWorkspace } from "./temp-workspace";
import { WorkspaceManager } from "./workspace-manager";
export class LoroStorage extends Effect.Service<LoroStorage>()("LoroStorage", {
accessors: true,
dependencies: [TempWorkspace.Default, WorkspaceManager.Default],
effect: Effect.gen(function* () {
const manager = yield* WorkspaceManager;
const temp = yield* TempWorkspace;
const load = ({ workspaceId }: { workspaceId: string }) =>
Effect.all(
{
workspace: manager.getById({ workspaceId }),
tempWorkspace: temp.getById({ workspaceId }),
},
{ concurrency: "unbounded" }
).pipe(
Effect.map(({ workspace, tempWorkspace }) => {
const doc = SnapshotSchema.EmptyDoc();
if (workspace !== undefined) {
doc.import(workspace.snapshot);
}
if (tempWorkspace !== undefined) {
doc.import(tempWorkspace.snapshot);
}
return { doc, workspace };
})
);
const query = <A extends Record<string, unknown>>(
extract: (doc: LoroDoc<LoroSchema>) => LoroList<LoroMap<A>>,
schema: Schema.Schema<A>,
{ workspaceId }: { workspaceId: string }
) =>
Effect.gen(function* () {
const { doc } = yield* load({ workspaceId });
const data = extract(doc);
const list = data.toArray();
return yield* Effect.all(
list.map((item) => Schema.decode(schema)(item.toJSON() as A)),
{ concurrency: 10 }
);
});
return { load, query } as const;
}),
}) {}
================================================
FILE: packages/client-lib/src/services/migration.ts
================================================
import { SnapshotToLoroDoc } from "@local/schema";
import { LoroDocMigration } from "@local/schema/migrations";
import { Data, Effect, Schema, type ParseResult } from "effect";
import { TempWorkspace } from "./temp-workspace";
class MigrationError extends Data.TaggedError("MigrationError")<{
parseError: ParseResult.ParseError;
}> {}
export class Migration extends Effect.Service<Migration>()("Migration", {
dependencies: [TempWorkspace.Default],
effect: Effect.gen(function* () {
const temp = yield* TempWorkspace;
return {
migrate: temp.getAll.pipe(
Effect.tap((workspaces) =>
Effect.log(`Migrating ${workspaces.length} workspaces`)
),
Effect.flatMap((workspaces) =>
Effect.all(
workspaces.map((workspace) =>
Effect.gen(function* () {
const doc = yield* Schema.decode(SnapshotToLoroDoc)(
workspace.snapshot
);
yield* Effect.log(doc.toJSON());
const newDoc = yield* Schema.decode(LoroDocMigration)(doc).pipe(
Effect.catchTag(
"ParseError",
(parseError) => new MigrationError({ parseError })
)
);
const newSnapshot =
yield* Schema.encode(SnapshotToLoroDoc)(newDoc);
yield* temp.put({
workspaceId: workspace.workspaceId,
snapshot: newSnapshot,
snapshotId: workspace.snapshotId,
});
})
)
)
),
Effect.tap(() =>
Effect.log("Migration completed successfully for all workspaces")
)
),
};
}),
}) {}
================================================
FILE: packages/client-lib/src/services/sync.ts
================================================
import type { LoroSchema } from "@local/schema";
import { Effect, flow, Option } from "effect";
import { LoroDoc } from "loro-crdt";
import { ApiClient } from "./api-client";
import { Dexie } from "./dexie";
import { TempWorkspace } from "./temp-workspace";
import { WorkspaceManager } from "./workspace-manager";
export class Sync extends Effect.Service<Sync>()("Sync", {
dependencies: [
TempWorkspace.Default,
WorkspaceManager.Default,
ApiClient.Default,
Dexie.Default,
],
effect: Effect.gen(function* () {
const { client } = yield* ApiClient;
const { initClient } = yield* Dexie;
const manager = yield* WorkspaceManager;
const temp = yield* TempWorkspace;
return {
push: ({
snapshot,
workspaceId,
snapshotId,
}: {
workspaceId: string;
snapshotId: string;
snapshot: globalThis.Uint8Array;
}) =>
manager.getById({ workspaceId }).pipe(
Effect.flatMap(
flow(
Option.fromNullable,
Option.match({
onNone: () => Effect.log("No workspace found"),
onSome: (workspace) =>
Effect.gen(function* () {
const clientId = yield* initClient;
yield* Effect.log(`Pushing snapshot ${snapshotId}`);
const response = yield* Effect.fromNullable(
workspace.token
).pipe(
Effect.flatMap((token) =>
client.syncData
.push({
headers: { "x-api-key": token },
path: { workspaceId: workspace.workspaceId },
payload: { snapshot, snapshotId },
})
.pipe(
Effect.map((response) => ({
...response,
token,
}))
)
),
Effect.catchTag("NoSuchElementException", () =>
client.syncAuth
.generateToken({
payload: {
clientId,
snapshot,
snapshotId,
workspaceId: workspace.workspaceId,
},
})
.pipe(
Effect.tap(({ token }) =>
manager.setToken({
workspaceId: workspace.workspaceId,
token,
})
)
)
)
);
const doc = new LoroDoc<LoroSchema>();
doc.import(response.snapshot);
yield* Effect.all([
manager.put({
workspaceId: response.workspaceId,
snapshot: response.snapshot,
token: response.token,
version: doc.version().encode(),
}),
temp.clean({
workspaceId: workspace.workspaceId,
}),
]);
}),
})
)
)
),
pull: ({ workspaceId }: { workspaceId: string }) =>
manager.getById({ workspaceId }).pipe(
Effect.flatMap(
flow(
Option.fromNullable,
Option.flatMap((workspace) =>
Option.fromNullable(workspace.token)
),
Option.match({
onNone: () =>
Effect.log("No token found").pipe(Effect.map(() => null)),
onSome: (token) =>
Effect.gen(function* () {
yield* Effect.log(`Pulling from ${workspaceId}`);
const response = yield* client.syncData.pull({
headers: { "x-api-key": token },
path: { workspaceId },
});
const doc = new LoroDoc();
doc.import(response.snapshot);
yield* manager.put({
token,
workspaceId,
snapshot: response.snapshot,
version: doc.version().encode(),
});
return response;
}),
})
)
)
),
join: ({ workspaceId }: { workspaceId: string }) =>
Effect.gen(function* () {
const clientId = yield* initClient;
const response = yield* client.syncData.join({
path: { clientId, workspaceId },
});
const doc = new LoroDoc();
doc.import(response.snapshot);
yield* manager.put({
workspaceId,
token: response.token,
snapshot: response.snapshot,
version: doc.version().encode(),
});
return response;
}),
};
}),
}) {}
================================================
FILE: packages/client-lib/src/services/temp-workspace.ts
================================================
import { Effect, Schema } from "effect";
import { TempWorkspaceTable } from "../schema";
import { Dexie } from "./dexie";
export class TempWorkspace extends Effect.Service<TempWorkspace>()(
"TempWorkspace",
{
dependencies: [Dexie.Default],
effect: Effect.gen(function* () {
const { query } = yield* Dexie;
return {
getAll: query((_) => _.temp_workspace.toArray()).pipe(
Effect.flatMap(Schema.decode(Schema.Array(TempWorkspaceTable)))
),
put: (params: typeof TempWorkspaceTable.Type) =>
Schema.encode(TempWorkspaceTable)(params).pipe(
Effect.flatMap((data) => query((_) => _.temp_workspace.put(data)))
),
getById: ({ workspaceId }: { workspaceId: string }) =>
query((_) =>
_.temp_workspace
.where("workspaceId")
.equals(workspaceId)
.limit(1)
.first()
).pipe(
Effect.flatMap((workspace) =>
workspace === undefined
? Effect.succeed(undefined)
: Schema.decode(TempWorkspaceTable)(workspace)
)
),
clean: ({ workspaceId }: { workspaceId: string }) =>
query((_) =>
_.temp_workspace.where("workspaceId").equals(workspaceId).delete()
),
};
}),
}
) {}
================================================
FILE: packages/client-lib/src/services/workspace-manager.ts
================================================
import { SnapshotSchema } from "@local/schema";
import { Snapshot } from "@local/sync";
import { Effect, Schema } from "effect";
import { WorkspaceTable } from "../schema";
import { Dexie } from "./dexie";
export class WorkspaceManager extends Effect.Service<WorkspaceManager>()(
"WorkspaceManager",
{
accessors: true,
dependencies: [Dexie.Default],
effect: Effect.gen(function* () {
const { query } = yield* Dexie;
return {
setToken: ({
token,
workspaceId,
}: {
workspaceId: typeof WorkspaceTable.Type.workspaceId;
token: NonNullable<typeof WorkspaceTable.Type.token>;
}) => query((_) => _.workspace.update(workspaceId, { token })),
put: (update: typeof WorkspaceTable.Type) =>
Schema.encode(WorkspaceTable)(update).pipe(
Effect.flatMap((data) => query((_) => _.workspace.put(data)))
),
getAll: query((_) => _.workspace.toArray()),
getById: ({ workspaceId }: { workspaceId: string }) =>
query((_) =>
_.workspace
.where("workspaceId")
.equals(workspaceId)
.limit(1)
.first()
).pipe(
Effect.flatMap((workspace) =>
workspace === undefined
? Effect.succeed(undefined)
: Schema.decode(WorkspaceTable)(workspace)
)
),
create: query((_) =>
_.workspace.toCollection().modify({ current: false })
).pipe(
Effect.andThen(
Schema.encode(Snapshot)(
SnapshotSchema.EmptyDoc().export({
mode: "snapshot",
})
)
),
Effect.flatMap((snapshot) =>
query((_) =>
_.workspace.put({
snapshot,
token: null,
version: null,
workspaceId: crypto.randomUUID(),
})
)
),
Effect.map((workspaceId) => ({ workspaceId }))
),
};
}),
}
) {}
================================================
FILE: packages/client-lib/src/sync-worker.ts
================================================
import { Snapshot } from "@local/sync";
import { liveQuery } from "dexie";
import { Array, Effect, Number, Schema, Stream, SynchronizedRef } from "effect";
import { Dexie, Sync, TempWorkspace, WorkspaceManager } from "./services";
export class LiveQuery extends Schema.TaggedRequest<LiveQuery>()("LiveQuery", {
failure: Schema.String,
payload: { workspaceId: Schema.String },
success: Schema.Boolean,
}) {}
export class Bootstrap extends Schema.TaggedRequest<Bootstrap>()("Bootstrap", {
failure: Schema.String,
payload: { workspaceId: Schema.String },
success: Schema.Boolean,
}) {}
export const WorkerMessage = Schema.Union(Bootstrap);
export class SyncWorker extends Effect.Service<SyncWorker>()("SyncWorker", {
effect: Effect.gen(function* () {
return {
liveSync: (params: { workspaceId: string }) =>
Effect.gen(function* () {
const manager = yield* WorkspaceManager;
const { db } = yield* Dexie;
const { push } = yield* Sync;
const snapshotEq = Array.getEquivalence(Number.Equivalence);
yield* Effect.log(`Live query workspace '${params.workspaceId}'`);
const workspace = yield* manager
.getById({ workspaceId: params.workspaceId })
.pipe(Effect.flatMap(Effect.fromNullable));
const live = liveQuery(() =>
db.temp_workspace
.where("workspaceId")
.equals(params.workspaceId)
.toArray()
);
yield* Effect.forkScoped(
Effect.acquireRelease(
Effect.gen(function* () {
yield* Effect.log("Subscribing");
const ref = yield* SynchronizedRef.make(0);
return live.subscribe((payload) =>
Effect.runPromise(
Effect.gen(function* () {
yield* Effect.log(`Change detected`);
const id = yield* ref.pipe(
SynchronizedRef.updateAndGet((n) => n + 1)
);
yield* Stream.runDrain(
Stream.make(...payload).pipe(
Stream.changesWith((a, b) =>
snapshotEq(a.snapshot, b.snapshot)
),
Stream.debounce("3 seconds"),
Stream.tap((message) =>
Effect.gen(function* () {
const streamId = yield* ref.get;
if (streamId === id) {
yield* Effect.log(
`Syncing ${payload.length} changes`
);
const snapshot = yield* Schema.decode(Snapshot)(
message.snapshot
);
yield* push({
snapshot,
snapshotId: message.snapshotId,
workspaceId: workspace.workspaceId,
});
}
})
)
)
);
})
)
);
}),
(subscription) =>
Effect.gen(function* () {
yield* Effect.log("Live query unsubscribing");
return subscription.unsubscribe();
})
)
);
return true;
}),
bootstrap: (params: Bootstrap) =>
Effect.gen(function* () {
const { push, pull } = yield* Sync;
const manager = yield* WorkspaceManager;
const temp = yield* TempWorkspace;
yield* Effect.log(`Running workspace '${params.workspaceId}'`);
const workspace = yield* manager
.getById({ workspaceId: params.workspaceId })
.pipe(Effect.flatMap(Effect.fromNullable));
const tempUpdates = yield* temp.getById({
workspaceId: workspace.workspaceId,
});
if (tempUpdates !== undefined) {
yield* push({
workspaceId: workspace.workspaceId,
snapshot: tempUpdates.snapshot,
snapshotId: tempUpdates.snapshotId,
});
yield* Effect.log("Push sync completed");
} else {
yield* pull({ workspaceId: workspace.workspaceId });
yield* Effect.log("Pull sync completed");
}
return true;
}).pipe(
Effect.mapError(
(error) => `Bootstrap error: ${JSON.stringify(error)}`
)
),
};
}),
}) {}
================================================
FILE: packages/client-lib/src/use-action-effect.ts
================================================
import { Effect, type ManagedRuntime } from "effect";
import { useActionState } from "react";
export const useActionEffect = <Payload, A, E, R>(
runtime: ManagedRuntime.ManagedRuntime<R, never>,
effect: (payload: Payload) => Effect.Effect<A, E, R>
) => {
return useActionState<
| { error: E; data: null }
| { error: null; data: A }
| { error: null; data: null },
Payload
>(
(_, payload) =>
runtime.runPromise(
effect(payload).pipe(
Effect.match({
onFailure: (error) => ({ error, data: null }),
onSuccess: (data) => ({ error: null, data }),
})
)
),
{ error: null, data: null }
);
};
================================================
FILE: packages/client-lib/src/use-dexie-query.ts
================================================
import { useLiveQuery } from "dexie-react-hooks";
import { Data, Effect, Either, Function, Match, pipe } from "effect";
import { Dexie } from "./services/dexie";
class MissingData extends Data.TaggedError("MissingData")<{}> {}
class DexieError extends Data.TaggedError("DexieError")<{
reason: "invalid-data" | "query-error";
cause: unknown;
}> {}
export const useDexieQuery = <A, I>(
query: (db: (typeof Dexie.Service)["db"]) => Promise<I>,
deps: unknown[] = []
) => {
const results = useLiveQuery(
() =>
Effect.runPromise(
Effect.gen(function* () {
const { db } = yield* Dexie;
return yield* Effect.tryPromise({
try: () => query(db),
catch: (cause) => new DexieError({ reason: "query-error", cause }),
});
}).pipe(Effect.either, Effect.provide(Dexie.Default))
),
deps
);
return pipe(
results,
Either.fromNullable(() => new MissingData()),
Either.flatMap(Function.identity),
Either.match({
onLeft: (_) =>
Match.value(_).pipe(
Match.tagsExhaustive({
DexieError: (error) => ({
error,
loading: false as const,
data: undefined,
}),
MissingData: (_) => ({
loading: true as const,
data: undefined,
error: undefined,
}),
})
),
onRight: (rows) => ({
data: rows,
loading: false as const,
error: undefined,
}),
})
);
};
================================================
FILE: packages/client-lib/tsconfig.json
================================================
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": ["es2022", "DOM", "DOM.Iterable"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: packages/schema/package.json
================================================
{
"name": "@local/schema",
"type": "module",
"scripts": {
"typecheck": "tsc"
},
"exports": {
".": "./src/main.ts",
"./migrations": "./src/migrations.ts"
},
"devDependencies": {},
"peerDependencies": {
"loro-crdt": "^1.4.2",
"effect": "^3.13.2"
},
"dependencies": {}
}
================================================
FILE: packages/schema/src/main.ts
================================================
import { Schema } from "effect";
import { LoroDoc, LoroList, LoroMap } from "loro-crdt";
import { AnyLoroDocSchema, Table, VersioningSchema } from "./schema";
import type { Version } from "./versioning";
export const VERSION = 1 satisfies Version;
export const CurrentSchema = VersioningSchema[VERSION];
const Metadata = Schema.Struct({ version: Schema.Number });
export type LoroSchema = {
metadata: LoroMap<Partial<typeof Metadata.Encoded>>;
food: LoroList<LoroMap<typeof CurrentSchema.fields.food.value.Encoded>>;
meal: LoroList<LoroMap<typeof CurrentSchema.fields.meal.value.Encoded>>;
};
export class SnapshotSchema extends Schema.Class<SnapshotSchema>(
"SnapshotSchema"
)({
metadata: Metadata,
...CurrentSchema.fields,
}) {
static readonly Table = Table;
static readonly EmptyDoc = () => {
const doc = new LoroDoc<LoroSchema>();
doc.getMap("metadata").set("version", VERSION);
Table.literals.map((key) => {
doc.getList(key);
});
return doc;
};
}
export const SnapshotToLoroDoc = Schema.Uint8ArrayFromSelf.pipe(
Schema.transform(AnyLoroDocSchema, {
decode: (from) => {
const doc = new LoroDoc();
doc.import(from);
return doc;
},
encode: (to) => to.export({ mode: "snapshot" }),
})
);
================================================
FILE: packages/schema/src/migrations.ts
================================================
import { ParseResult, Schema } from "effect";
import { LoroDoc } from "loro-crdt";
import { SnapshotSchema } from "./main";
import { AnyLoroDocSchema } from "./schema";
import { Version } from "./versioning";
const migrations = {
1: (_) => SnapshotSchema.EmptyDoc(),
} satisfies Record<Version, (doc: LoroDoc) => LoroDoc>;
export const LoroDocMigration = AnyLoroDocSchema.pipe(
Schema.transformOrFail(AnyLoroDocSchema, {
decode: (from, _, ast) => {
const doc = new LoroDoc();
doc.import(from.export({ mode: "snapshot" }));
const currentVersion = doc.getMap("metadata").get("version");
if (typeof currentVersion === "number") {
Version.forEach((version) => {
doc.import(migrations[version](doc).export({ mode: "snapshot" }));
});
} else {
return ParseResult.fail(
new ParseResult.Type(ast, from, "Invalid version number in metadata")
);
}
return ParseResult.succeed(doc);
},
encode: (to, _, ast) =>
ParseResult.fail(
new ParseResult.Forbidden(
ast,
to,
"Encoding LoroDoc migration is not allowed (should not happen...)"
)
),
})
);
================================================
FILE: packages/schema/src/schema.ts
================================================
import { Schema } from "effect";
import { LoroDoc } from "loro-crdt";
import { type Version } from "./versioning";
export const AnyLoroDocSchema = Schema.instanceOf(LoroDoc);
export class FoodV1 extends Schema.Class<FoodV1>("FoodV1")({
id: Schema.UUID,
name: Schema.String,
calories: Schema.Number.pipe(Schema.positive()),
}) {}
export class MealV1 extends Schema.Class<MealV1>("MealV1")({
id: Schema.UUID,
foodId: FoodV1.fields.id,
quantity: Schema.Number.pipe(Schema.positive()),
}) {}
export const Table = Schema.Literal("food", "meal");
export const VersioningSchema = {
1: Schema.Struct({
[Table.literals[0]]: Schema.Array(FoodV1),
[Table.literals[1]]: Schema.Array(MealV1),
}),
} as const satisfies Record<Version, Schema.Schema.AnyNoContext>;
================================================
FILE: packages/schema/src/versioning.ts
================================================
export const Version = [1] as const;
export type Version = (typeof Version)[number];
================================================
FILE: packages/schema/tsconfig.json
================================================
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": ["es2022"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: packages/sync/package.json
================================================
{
"name": "@local/sync",
"type": "module",
"scripts": {
"typecheck": "tsc"
},
"exports": {
".": "./src/main.ts"
},
"devDependencies": {},
"peerDependencies": {
"@effect/platform": "^0.77.2",
"effect": "^3.13.2"
},
"dependencies": {}
}
================================================
FILE: packages/sync/src/main.ts
================================================
import {
HttpApi,
HttpApiEndpoint,
HttpApiGroup,
HttpApiMiddleware,
HttpApiSchema,
HttpApiSecurity,
} from "@effect/platform";
import { Context, Schema } from "effect";
export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
"Unauthorized",
{ message: Schema.String },
HttpApiSchema.annotations({ status: 401 })
) {}
export class MissingWorkspace extends Schema.TaggedError<MissingWorkspace>()(
"MissingWorkspace",
{},
HttpApiSchema.annotations({ status: 404 })
) {}
export class DatabaseError extends Schema.TaggedError<DatabaseError>()(
"DatabaseError",
{},
HttpApiSchema.annotations({ status: 500 })
) {}
export class VersionError extends Schema.TaggedError<VersionError>()(
"VersionError",
{
reason: Schema.Literal(
"missing-snapshot",
"invalid-doc",
"missing-version",
"outdated-version"
),
},
HttpApiSchema.annotations({ status: 400 })
) {}
export const ClientId = Schema.UUID;
export const WorkspaceId = Schema.UUID;
export const Snapshot = Schema.Uint8Array;
export const SnapshotId = Schema.UUID;
export const Scope = Schema.Literal("read", "read_write");
export class ClientTable extends Schema.Class<ClientTable>("ClientTable")({
clientId: ClientId,
createdAt: Schema.DateFromString,
}) {}
export class WorkspaceTable extends Schema.Class<WorkspaceTable>(
"WorkspaceTable"
)({
workspaceId: WorkspaceId,
ownerClientId: ClientId,
createdAt: Schema.DateFromString,
clientId: ClientId,
snapshotId: SnapshotId,
snapshot: Snapshot,
}) {}
export class TokenTable extends Schema.Class<TokenTable>("TokenTable")({
tokenId: Schema.Number,
tokenValue: Schema.String,
clientId: ClientId,
workspaceId: WorkspaceId,
isMaster: Schema.Boolean,
scope: Scope,
issuedAt: Schema.DateFromString,
expiresAt: Schema.NullOr(Schema.DateFromString),
revokedAt: Schema.NullOr(Schema.DateFromString),
}) {}
export class AuthWorkspace extends Context.Tag("AuthWorkspace")<
AuthWorkspace,
WorkspaceTable
>() {}
export class ValidDoc extends Context.Tag("ValidDoc")<
ValidDoc,
typeof Snapshot.Type
>() {}
const authKey = "x-api-key";
export const ApiKey = HttpApiSecurity.apiKey({
in: "header",
key: authKey,
});
const ApiKeyHeader = Schema.Struct({
[authKey]: Schema.String,
});
export class VersionCheck extends HttpApiMiddleware.Tag<VersionCheck>()(
"VersionCheck",
{
failure: VersionError,
provides: ValidDoc,
}
) {}
export class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
"Authorization",
{
failure: Schema.Union(Unauthorized, MissingWorkspace, DatabaseError),
provides: AuthWorkspace,
security: { apiKey: ApiKey },
}
) {}
export class MasterAuthorization extends HttpApiMiddleware.Tag<MasterAuthorization>()(
"MasterAuthorization",
{
failure: Schema.Union(Unauthorized, MissingWorkspace, DatabaseError),
provides: AuthWorkspace,
security: { apiKey: ApiKey },
}
) {}
export class SyncAuthGroup extends HttpApiGroup.make("syncAuth")
.add(
/**
Allows a client to create a new workshop and upload its initial data to the server. The server marks the client as the owner and issues a master token for full control.
*/
HttpApiEndpoint.post("generateToken")`/`
.setPayload(
Schema.Struct({
clientId: ClientId,
workspaceId: WorkspaceId,
snapshotId: SnapshotId,
snapshot: WorkspaceTable.fields.snapshot,
})
)
.addError(Schema.String)
.addSuccess(
Schema.Struct({
token: Schema.String,
workspaceId: WorkspaceTable.fields.workspaceId,
createdAt: WorkspaceTable.fields.createdAt,
snapshot: WorkspaceTable.fields.snapshot,
})
)
.middleware(VersionCheck)
)
.add(
/**
Allows the owner (via master token) to generate an access token for another client, specifying permissions and expiration. The owner shares this token with the client securely.
*/
HttpApiEndpoint.post(
"issueToken"
)`/${HttpApiSchema.param("workspaceId", Schema.UUID)}/token`
.setPayload(
Schema.Struct({
clientId: ClientId,
scope: Scope,
expiresIn: Schema.Duration,
})
)
.addError(Schema.String)
.addSuccess(
Schema.Struct({
token: Schema.String,
scope: Scope,
expiresAt: Schema.DateFromString,
})
)
.setHeaders(ApiKeyHeader)
.middleware(MasterAuthorization)
)
.add(
/**
Lets the owner revoke access for a specific client by invalidating their access token. Requires the master token and targets the `clientId` tied to the token.
*/
HttpApiEndpoint.del(
"revokeToken"
)`/${HttpApiSchema.param("workspaceId", Schema.UUID)}/token/${HttpApiSchema.param("clientId", Schema.UUID)}`
.addError(Schema.String)
.addSuccess(Schema.Boolean)
.setHeaders(ApiKeyHeader)
.middleware(MasterAuthorization)
)
.add(
/**
Provides the owner with a list of all active tokens (master and access) for a workshop, showing their status. Useful for managing access.
*/
HttpApiEndpoint.get(
"listTokens"
)`/${HttpApiSchema.param("workspaceId", Schema.UUID)}/tokens`
.addError(Schema.String)
.addSuccess(
Schema.Array(
TokenTable.pipe(
Schema.pick(
"clientId",
"tokenValue",
"scope",
"isMaster",
"issuedAt",
"expiresAt",
"revokedAt"
)
)
)
)
.setHeaders(ApiKeyHeader)
.middleware(MasterAuthorization)
)
.prefix("/workspaces") {}
export class SyncDataGroup extends HttpApiGroup.make("syncData")
.add(
/**
Updates the workshop data on the server with changes from a client. Requires a valid token with `read_write` scope.
*/
HttpApiEndpoint.put(
"push"
)`/${HttpApiSchema.param("workspaceId", Schema.UUID)}/push`
.setPayload(WorkspaceTable.pipe(Schema.pick("snapshot", "snapshotId")))
.addError(Schema.String)
.addSuccess(
WorkspaceTable.pipe(Schema.pick("workspaceId", "createdAt", "snapshot"))
)
.setHeaders(ApiKeyHeader)
.middleware(Authorization)
.middleware(VersionCheck)
)
.add(
/**
Retrieves the current workshop data for a client (owner or authorized user). Requires a valid token (master or access) with at least `read` scope. Used for initial download or sync verification.
*/
HttpApiEndpoint.get(
"pull"
)`/${HttpApiSchema.param("workspaceId", Schema.UUID)}/pull`
.addError(Schema.String)
.addSuccess(Schema.Struct({ snapshot: WorkspaceTable.fields.snapshot }))
.setHeaders(ApiKeyHeader)
.middleware(Authorization)
)
.add(
/**
Client opens link to join another workspace. The server issues a token for the workspace and returns the workspace data.
*/
HttpApiEndpoint.get(
"join"
)`/${HttpApiSchema.param("workspaceId", Schema.UUID)}/join/${HttpApiSchema.param("clientId", Schema.UUID)}`
.addError(Schema.String)
.addSuccess(
Schema.Struct({
snapshot: WorkspaceTable.fields.snapshot,
token: TokenTable.fields.tokenValue,
})
)
)
.prefix("/workspaces") {}
export class SyncApi extends HttpApi.make("SyncApi")
.add(SyncAuthGroup)
.add(SyncDataGroup) {}
================================================
FILE: packages/sync/tsconfig.json
================================================
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": ["es2022"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- "apps/*"
- "packages/*"
================================================
FILE: turbo.json
================================================
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
},
"generate": {
"dependsOn": ["^generate"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
gitextract_usv87bki/ ├── .gitignore ├── .npmrc ├── .vscode/ │ └── settings.json ├── README.md ├── apps/ │ ├── client/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── lib/ │ │ │ │ ├── constants.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-food.ts │ │ │ │ │ └── use-meal.ts │ │ │ │ ├── runtime-client.ts │ │ │ │ └── services/ │ │ │ │ └── storage.ts │ │ │ ├── main.tsx │ │ │ ├── routeTree.gen.ts │ │ │ ├── routes/ │ │ │ │ ├── $workspaceId/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── join.tsx │ │ │ │ │ └── token.tsx │ │ │ │ ├── __root.tsx │ │ │ │ └── index.tsx │ │ │ └── workers/ │ │ │ ├── bootstrap.ts │ │ │ └── live.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── server/ │ ├── drizzle/ │ │ ├── 0000_supreme_bedlam.sql │ │ └── meta/ │ │ ├── 0000_snapshot.json │ │ └── _journal.json │ ├── drizzle.config.ts │ ├── package.json │ ├── src/ │ │ ├── database.ts │ │ ├── db/ │ │ │ └── schema.ts │ │ ├── group/ │ │ │ ├── sync-auth.ts │ │ │ └── sync-data.ts │ │ ├── main.ts │ │ ├── middleware/ │ │ │ ├── authorization.ts │ │ │ ├── master-authorization.ts │ │ │ └── version-check.ts │ │ └── services/ │ │ ├── drizzle.ts │ │ └── jwt.ts │ └── tsconfig.json ├── docker-compose.yaml ├── package.json ├── packages/ │ ├── client-lib/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── main.ts │ │ │ ├── runtime-layer.ts │ │ │ ├── schema.ts │ │ │ ├── services/ │ │ │ │ ├── api-client.ts │ │ │ │ ├── dexie.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loro-storage.ts │ │ │ │ ├── migration.ts │ │ │ │ ├── sync.ts │ │ │ │ ├── temp-workspace.ts │ │ │ │ └── workspace-manager.ts │ │ │ ├── sync-worker.ts │ │ │ ├── use-action-effect.ts │ │ │ └── use-dexie-query.ts │ │ └── tsconfig.json │ ├── schema/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── main.ts │ │ │ ├── migrations.ts │ │ │ ├── schema.ts │ │ │ └── versioning.ts │ │ └── tsconfig.json │ └── sync/ │ ├── package.json │ ├── src/ │ │ └── main.ts │ └── tsconfig.json ├── pnpm-workspace.yaml └── turbo.json
SYMBOL INDEX (63 symbols across 27 files)
FILE: apps/client/src/lib/constants.ts
constant WEBSITE_URL (line 1) | const WEBSITE_URL = "http://localhost:3001";
FILE: apps/client/src/lib/services/storage.ts
class Storage (line 6) | class Storage extends Effect.Service<Storage>()("Storage", {
FILE: apps/client/src/main.tsx
type Register (line 13) | interface Register {
FILE: apps/client/src/routeTree.gen.ts
type FileRoutesByPath (line 48) | interface FileRoutesByPath {
type FileRoutesByFullPath (line 82) | interface FileRoutesByFullPath {
type FileRoutesByTo (line 89) | interface FileRoutesByTo {
type FileRoutesById (line 96) | interface FileRoutesById {
type FileRouteTypes (line 104) | interface FileRouteTypes {
type RootRouteChildren (line 122) | interface RootRouteChildren {
FILE: apps/client/src/routes/$workspaceId/index.tsx
function RouteComponent (line 41) | function RouteComponent() {
FILE: apps/client/src/routes/$workspaceId/join.tsx
function RouteComponent (line 21) | function RouteComponent() {
FILE: apps/client/src/routes/$workspaceId/token.tsx
function RouteComponent (line 29) | function RouteComponent() {
FILE: apps/client/src/routes/__root.tsx
function RootComponent (line 23) | function RootComponent() {
FILE: apps/client/src/routes/index.tsx
function HomeComponent (line 11) | function HomeComponent() {
FILE: apps/server/drizzle/0000_supreme_bedlam.sql
type "client" (line 2) | CREATE TABLE "client" (
type "token" (line 7) | CREATE TABLE "token" (
type "workspace" (line 19) | CREATE TABLE "workspace" (
FILE: apps/server/src/group/sync-data.ts
class InvalidVersionError (line 11) | class InvalidVersionError extends Data.TaggedError("InvalidVersionError")<{
FILE: apps/server/src/services/drizzle.ts
class MigrationError (line 8) | class MigrationError extends Data.TaggedError("MigrationError")<{
class QueryError (line 12) | class QueryError extends Data.TaggedError("QueryError")<{
class Drizzle (line 16) | class Drizzle extends Effect.Service<Drizzle>()("Drizzle", {
FILE: apps/server/src/services/jwt.ts
class TokenPayload (line 5) | class TokenPayload extends Schema.Class<TokenPayload>("TokenPayload")({
class JwtError (line 14) | class JwtError extends Data.TaggedError("JwtError")<{
class Jwt (line 18) | class Jwt extends Effect.Service<Jwt>()("Jwt", {
FILE: packages/client-lib/src/schema.ts
class ClientTable (line 4) | class ClientTable extends Schema.Class<ClientTable>("ClientTable")({
class WorkspaceTable (line 8) | class WorkspaceTable extends Schema.Class<WorkspaceTable>(
class TempWorkspaceTable (line 18) | class TempWorkspaceTable extends Schema.Class<TempWorkspaceTable>(
FILE: packages/client-lib/src/services/api-client.ts
class ApiClient (line 5) | class ApiClient extends Effect.Service<ApiClient>()("ApiClient", {
FILE: packages/client-lib/src/services/dexie.ts
class QueryApiError (line 9) | class QueryApiError extends Data.TaggedError("QueryApiError")<{
class WriteApiError (line 13) | class WriteApiError extends Data.TaggedError("WriteApiError")<{
class Dexie (line 27) | class Dexie extends Effect.Service<Dexie>()("Dexie", {
FILE: packages/client-lib/src/services/loro-storage.ts
class LoroStorage (line 7) | class LoroStorage extends Effect.Service<LoroStorage>()("LoroStorage", {
FILE: packages/client-lib/src/services/migration.ts
class MigrationError (line 6) | class MigrationError extends Data.TaggedError("MigrationError")<{
class Migration (line 10) | class Migration extends Effect.Service<Migration>()("Migration", {
FILE: packages/client-lib/src/services/sync.ts
class Sync (line 9) | class Sync extends Effect.Service<Sync>()("Sync", {
FILE: packages/client-lib/src/services/temp-workspace.ts
class TempWorkspace (line 5) | class TempWorkspace extends Effect.Service<TempWorkspace>()(
FILE: packages/client-lib/src/services/workspace-manager.ts
class WorkspaceManager (line 7) | class WorkspaceManager extends Effect.Service<WorkspaceManager>()(
FILE: packages/client-lib/src/sync-worker.ts
class LiveQuery (line 6) | class LiveQuery extends Schema.TaggedRequest<LiveQuery>()("LiveQuery", {
class Bootstrap (line 12) | class Bootstrap extends Schema.TaggedRequest<Bootstrap>()("Bootstrap", {
class SyncWorker (line 20) | class SyncWorker extends Effect.Service<SyncWorker>()("SyncWorker", {
FILE: packages/client-lib/src/use-dexie-query.ts
class MissingData (line 5) | class MissingData extends Data.TaggedError("MissingData")<{}> {}
class DexieError (line 6) | class DexieError extends Data.TaggedError("DexieError")<{
FILE: packages/schema/src/main.ts
constant VERSION (line 6) | const VERSION = 1 satisfies Version;
type LoroSchema (line 11) | type LoroSchema = {
class SnapshotSchema (line 17) | class SnapshotSchema extends Schema.Class<SnapshotSchema>(
FILE: packages/schema/src/schema.ts
class FoodV1 (line 7) | class FoodV1 extends Schema.Class<FoodV1>("FoodV1")({
class MealV1 (line 13) | class MealV1 extends Schema.Class<MealV1>("MealV1")({
FILE: packages/schema/src/versioning.ts
type Version (line 2) | type Version = (typeof Version)[number];
FILE: packages/sync/src/main.ts
class Unauthorized (line 11) | class Unauthorized extends Schema.TaggedError<Unauthorized>()(
class MissingWorkspace (line 17) | class MissingWorkspace extends Schema.TaggedError<MissingWorkspace>()(
class DatabaseError (line 23) | class DatabaseError extends Schema.TaggedError<DatabaseError>()(
class VersionError (line 29) | class VersionError extends Schema.TaggedError<VersionError>()(
class ClientTable (line 49) | class ClientTable extends Schema.Class<ClientTable>("ClientTable")({
class WorkspaceTable (line 54) | class WorkspaceTable extends Schema.Class<WorkspaceTable>(
class TokenTable (line 65) | class TokenTable extends Schema.Class<TokenTable>("TokenTable")({
class AuthWorkspace (line 77) | class AuthWorkspace extends Context.Tag("AuthWorkspace")<
class ValidDoc (line 82) | class ValidDoc extends Context.Tag("ValidDoc")<
class VersionCheck (line 97) | class VersionCheck extends HttpApiMiddleware.Tag<VersionCheck>()(
class Authorization (line 105) | class Authorization extends HttpApiMiddleware.Tag<Authorization>()(
class MasterAuthorization (line 114) | class MasterAuthorization extends HttpApiMiddleware.Tag<MasterAuthorizat...
class SyncAuthGroup (line 123) | class SyncAuthGroup extends HttpApiGroup.make("syncAuth")
class SyncDataGroup (line 213) | class SyncDataGroup extends HttpApiGroup.make("syncData")
class SyncApi (line 259) | class SyncApi extends HttpApi.make("SyncApi")
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (113K chars).
[
{
"path": ".gitignore",
"chars": 399,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependencies\nnode_modules\n.pnp\n"
},
{
"path": ".npmrc",
"chars": 0,
"preview": ""
},
{
"path": ".vscode/settings.json",
"chars": 193,
"preview": "{\n \"files.watcherExclude\": {\n \"**/routeTree.gen.ts\": true\n },\n \"search.exclude\": {\n \"**/routeTree.gen.ts\": true"
},
{
"path": "README.md",
"chars": 3694,
"preview": "# Sync Engine Web\n\nA local-first, offline-capable web sync engine implementation with CRDT-based synchronization. The pr"
},
{
"path": "apps/client/.gitignore",
"chars": 150,
"preview": "# Local\n.DS_Store\n*.local\n*.log*\n\n# Dist\nnode_modules\ndist/\n.vinxi\n.output\n.vercel\n.netlify\n.wrangler\n\n# IDE\n.vscode/*\n!"
},
{
"path": "apps/client/index.html",
"chars": 644,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "apps/client/package.json",
"chars": 936,
"preview": "{\n \"name\": \"client\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"tsc "
},
{
"path": "apps/client/src/lib/constants.ts",
"chars": 52,
"preview": "export const WEBSITE_URL = \"http://localhost:3001\";\n"
},
{
"path": "apps/client/src/lib/hooks/use-food.ts",
"chars": 471,
"preview": "import { Service, useDexieQuery } from \"@local/client-lib\";\nimport { SnapshotSchema } from \"@local/schema\";\nimport { Run"
},
{
"path": "apps/client/src/lib/hooks/use-meal.ts",
"chars": 924,
"preview": "import { Service, useDexieQuery } from \"@local/client-lib\";\nimport { SnapshotSchema } from \"@local/schema\";\nimport { Eff"
},
{
"path": "apps/client/src/lib/runtime-client.ts",
"chars": 272,
"preview": "import { RuntimeLayer } from \"@local/client-lib\";\nimport { Layer, ManagedRuntime } from \"effect\";\nimport { Storage } fro"
},
{
"path": "apps/client/src/lib/services/storage.ts",
"chars": 1761,
"preview": "import { Service } from \"@local/client-lib\";\nimport { CurrentSchema, SnapshotSchema } from \"@local/schema\";\nimport { Eff"
},
{
"path": "apps/client/src/main.tsx",
"chars": 581,
"preview": "import { RouterProvider, createRouter } from \"@tanstack/react-router\";\nimport ReactDOM from \"react-dom/client\";\nimport {"
},
{
"path": "apps/client/src/routeTree.gen.ts",
"chars": 4494,
"preview": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by"
},
{
"path": "apps/client/src/routes/$workspaceId/index.tsx",
"chars": 5073,
"preview": "import { Worker } from \"@effect/platform\";\nimport { BrowserWorker } from \"@effect/platform-browser\";\nimport { Service, S"
},
{
"path": "apps/client/src/routes/$workspaceId/join.tsx",
"chars": 677,
"preview": "import { Service } from \"@local/client-lib\";\nimport { createFileRoute, redirect } from \"@tanstack/react-router\";\nimport "
},
{
"path": "apps/client/src/routes/$workspaceId/token.tsx",
"chars": 4763,
"preview": "import { Service, useActionEffect } from \"@local/client-lib\";\nimport { createFileRoute, Link, useRouter } from \"@tanstac"
},
{
"path": "apps/client/src/routes/__root.tsx",
"chars": 854,
"preview": "import { Service } from \"@local/client-lib\";\nimport { Outlet, createRootRoute } from \"@tanstack/react-router\";\nimport { "
},
{
"path": "apps/client/src/routes/index.tsx",
"chars": 1266,
"preview": "import { Service, useActionEffect } from \"@local/client-lib\";\nimport { createFileRoute, Link, useNavigate } from \"@tanst"
},
{
"path": "apps/client/src/workers/bootstrap.ts",
"chars": 601,
"preview": "import { WorkerRunner } from \"@effect/platform\";\nimport { BrowserWorkerRunner } from \"@effect/platform-browser\";\nimport "
},
{
"path": "apps/client/src/workers/live.ts",
"chars": 652,
"preview": "import { WorkerRunner } from \"@effect/platform\";\nimport { BrowserWorkerRunner } from \"@effect/platform-browser\";\nimport "
},
{
"path": "apps/client/tsconfig.json",
"chars": 637,
"preview": "{\n \"compilerOptions\": {\n \"strict\": true,\n \"esModuleInterop\": true,\n \"jsx\": \"react-jsx\",\n \"target\": \"ESNext\""
},
{
"path": "apps/client/vite.config.ts",
"chars": 404,
"preview": "import { TanStackRouterVite } from \"@tanstack/router-plugin/vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { de"
},
{
"path": "apps/server/drizzle/0000_supreme_bedlam.sql",
"chars": 965,
"preview": "CREATE TYPE \"public\".\"scope\" AS ENUM('read', 'read_write');--> statement-breakpoint\nCREATE TABLE \"client\" (\n\t\"clientId\" "
},
{
"path": "apps/server/drizzle/meta/0000_snapshot.json",
"chars": 4585,
"preview": "{\n \"id\": \"b089a447-bbae-4d52-b83e-a0ce1cc4b359\",\n \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n \"version\": \"7\",\n"
},
{
"path": "apps/server/drizzle/meta/_journal.json",
"chars": 208,
"preview": "{\n \"version\": \"7\",\n \"dialect\": \"postgresql\",\n \"entries\": [\n {\n \"idx\": 0,\n \"version\": \"7\",\n \"when\": "
},
{
"path": "apps/server/drizzle.config.ts",
"chars": 156,
"preview": "import { defineConfig } from \"drizzle-kit\";\n\nexport default defineConfig({\n out: \"./drizzle\",\n schema: \"./src/db/schem"
},
{
"path": "apps/server/package.json",
"chars": 756,
"preview": "{\n \"name\": \"@local/server\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"main.js\",\n \"scripts\": {\n \"typeche"
},
{
"path": "apps/server/src/database.ts",
"chars": 850,
"preview": "import { PgClient } from \"@effect/sql-pg\";\nimport { Config, Effect, Layer, Redacted } from \"effect\";\n\nconst password = C"
},
{
"path": "apps/server/src/db/schema.ts",
"chars": 1074,
"preview": "import {\n boolean,\n customType,\n integer,\n pgEnum,\n pgTable,\n timestamp,\n uuid,\n varchar,\n} from \"drizzle-orm/pg"
},
{
"path": "apps/server/src/group/sync-auth.ts",
"chars": 8123,
"preview": "import { HttpApiBuilder } from \"@effect/platform\";\nimport { AuthWorkspace, Scope, SyncApi } from \"@local/sync\";\nimport {"
},
{
"path": "apps/server/src/group/sync-data.ts",
"chars": 5358,
"preview": "import { HttpApiBuilder } from \"@effect/platform\";\nimport { SnapshotToLoroDoc } from \"@local/schema\";\nimport { AuthWorks"
},
{
"path": "apps/server/src/main.ts",
"chars": 1126,
"preview": "import {\n HttpApiBuilder,\n HttpMiddleware,\n HttpServer,\n PlatformConfigProvider,\n} from \"@effect/platform\";\nimport {"
},
{
"path": "apps/server/src/middleware/authorization.ts",
"chars": 3310,
"preview": "import {\n Authorization,\n ClientId,\n DatabaseError,\n MissingWorkspace,\n Unauthorized,\n WorkspaceId,\n} from \"@local"
},
{
"path": "apps/server/src/middleware/master-authorization.ts",
"chars": 2317,
"preview": "import {\n DatabaseError,\n MasterAuthorization,\n MissingWorkspace,\n Unauthorized,\n} from \"@local/sync\";\nimport { and,"
},
{
"path": "apps/server/src/middleware/version-check.ts",
"chars": 1461,
"preview": "import { HttpServerRequest } from \"@effect/platform\";\nimport { SnapshotToLoroDoc, VERSION } from \"@local/schema\";\nimport"
},
{
"path": "apps/server/src/services/drizzle.ts",
"chars": 1701,
"preview": "import { Data, Effect, Redacted, Schema } from \"effect\";\nimport { DatabaseUrl } from \"../database\";\n\nimport { drizzle } "
},
{
"path": "apps/server/src/services/jwt.ts",
"chars": 1800,
"preview": "import { Scope } from \"@local/sync\";\nimport { Config, Data, Effect, Redacted, Schema } from \"effect\";\nimport * as jwt fr"
},
{
"path": "apps/server/tsconfig.json",
"chars": 472,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"target\": \"es2022\",\n \"allowJs\": t"
},
{
"path": "docker-compose.yaml",
"chars": 535,
"preview": "version: \"0.2\"\nname: app_docker\n\nservices:\n postgres:\n env_file: .env\n container_name: postgres\n image: postgr"
},
{
"path": "package.json",
"chars": 449,
"preview": "{\n \"name\": \"sql-database-local-config\",\n \"private\": true,\n \"scripts\": {\n \"build\": \"turbo run build\",\n \"dev\": \"t"
},
{
"path": "packages/client-lib/package.json",
"chars": 553,
"preview": "{\n \"name\": \"@local/client-lib\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"tsc\"\n },\n \"exports\": {\n \".\": "
},
{
"path": "packages/client-lib/src/main.ts",
"chars": 347,
"preview": "import { RuntimeLayer } from \"./runtime-layer\";\nimport * as Service from \"./services\";\nimport * as SyncWorker from \"./sy"
},
{
"path": "packages/client-lib/src/runtime-layer.ts",
"chars": 658,
"preview": "import { Layer } from \"effect\";\nimport { ApiClient } from \"./services/api-client\";\nimport { Dexie } from \"./services/dex"
},
{
"path": "packages/client-lib/src/schema.ts",
"chars": 628,
"preview": "import { ClientId, Snapshot, SnapshotId, WorkspaceId } from \"@local/sync\";\nimport { Schema } from \"effect\";\n\nexport clas"
},
{
"path": "packages/client-lib/src/services/api-client.ts",
"chars": 431,
"preview": "import { FetchHttpClient, HttpApiClient } from \"@effect/platform\";\nimport { SyncApi } from \"@local/sync\";\nimport { Effec"
},
{
"path": "packages/client-lib/src/services/dexie.ts",
"chars": 2900,
"preview": "import * as _Dexie from \"dexie\";\nimport { Data, Effect, Schema } from \"effect\";\nimport {\n type ClientTable,\n type Temp"
},
{
"path": "packages/client-lib/src/services/index.ts",
"chars": 407,
"preview": "import { ApiClient } from \"./api-client\";\nimport { Dexie } from \"./dexie\";\nimport { LoroStorage } from \"./loro-storage\";"
},
{
"path": "packages/client-lib/src/services/loro-storage.ts",
"chars": 1773,
"preview": "import { SnapshotSchema, type LoroSchema } from \"@local/schema\";\nimport { Effect, Schema } from \"effect\";\nimport { LoroD"
},
{
"path": "packages/client-lib/src/services/migration.ts",
"chars": 1765,
"preview": "import { SnapshotToLoroDoc } from \"@local/schema\";\nimport { LoroDocMigration } from \"@local/schema/migrations\";\nimport {"
},
{
"path": "packages/client-lib/src/services/sync.ts",
"chars": 5352,
"preview": "import type { LoroSchema } from \"@local/schema\";\nimport { Effect, flow, Option } from \"effect\";\nimport { LoroDoc } from "
},
{
"path": "packages/client-lib/src/services/temp-workspace.ts",
"chars": 1365,
"preview": "import { Effect, Schema } from \"effect\";\nimport { TempWorkspaceTable } from \"../schema\";\nimport { Dexie } from \"./dexie\""
},
{
"path": "packages/client-lib/src/services/workspace-manager.ts",
"chars": 2104,
"preview": "import { SnapshotSchema } from \"@local/schema\";\nimport { Snapshot } from \"@local/sync\";\nimport { Effect, Schema } from \""
},
{
"path": "packages/client-lib/src/sync-worker.ts",
"chars": 4836,
"preview": "import { Snapshot } from \"@local/sync\";\nimport { liveQuery } from \"dexie\";\nimport { Array, Effect, Number, Schema, Strea"
},
{
"path": "packages/client-lib/src/use-action-effect.ts",
"chars": 689,
"preview": "import { Effect, type ManagedRuntime } from \"effect\";\nimport { useActionState } from \"react\";\n\nexport const useActionEff"
},
{
"path": "packages/client-lib/src/use-dexie-query.ts",
"chars": 1549,
"preview": "import { useLiveQuery } from \"dexie-react-hooks\";\nimport { Data, Effect, Either, Function, Match, pipe } from \"effect\";\n"
},
{
"path": "packages/client-lib/tsconfig.json",
"chars": 495,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"target\": \"es2022\",\n \"allowJs\": t"
},
{
"path": "packages/schema/package.json",
"chars": 308,
"preview": "{\n \"name\": \"@local/schema\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"tsc\"\n },\n \"exports\": {\n \".\": \"./s"
},
{
"path": "packages/schema/src/main.ts",
"chars": 1273,
"preview": "import { Schema } from \"effect\";\nimport { LoroDoc, LoroList, LoroMap } from \"loro-crdt\";\nimport { AnyLoroDocSchema, Tabl"
},
{
"path": "packages/schema/src/migrations.ts",
"chars": 1205,
"preview": "import { ParseResult, Schema } from \"effect\";\nimport { LoroDoc } from \"loro-crdt\";\nimport { SnapshotSchema } from \"./mai"
},
{
"path": "packages/schema/src/schema.ts",
"chars": 778,
"preview": "import { Schema } from \"effect\";\nimport { LoroDoc } from \"loro-crdt\";\nimport { type Version } from \"./versioning\";\n\nexpo"
},
{
"path": "packages/schema/src/versioning.ts",
"chars": 85,
"preview": "export const Version = [1] as const;\nexport type Version = (typeof Version)[number];\n"
},
{
"path": "packages/schema/tsconfig.json",
"chars": 472,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"target\": \"es2022\",\n \"allowJs\": t"
},
{
"path": "packages/sync/package.json",
"chars": 271,
"preview": "{\n \"name\": \"@local/sync\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"tsc\"\n },\n \"exports\": {\n \".\": \"./src"
},
{
"path": "packages/sync/src/main.ts",
"chars": 7534,
"preview": "import {\n HttpApi,\n HttpApiEndpoint,\n HttpApiGroup,\n HttpApiMiddleware,\n HttpApiSchema,\n HttpApiSecurity,\n} from \""
},
{
"path": "packages/sync/tsconfig.json",
"chars": 472,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"target\": \"es2022\",\n \"allowJs\": t"
},
{
"path": "pnpm-workspace.yaml",
"chars": 40,
"preview": "packages:\n - \"apps/*\"\n - \"packages/*\"\n"
},
{
"path": "turbo.json",
"chars": 416,
"preview": "{\n \"$schema\": \"https://turbo.build/schema.json\",\n \"ui\": \"tui\",\n \"tasks\": {\n \"build\": {\n \"dependsOn\": [\"^build"
}
]
About this extraction
This page contains the full source code of the typeonce-dev/sync-engine-web GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (101.0 KB), approximately 25.4k tokens, and a symbol index with 63 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.