Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Run tests
run: pnpm test
run: pnpm run build && pnpm test
14 changes: 3 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ A TypeScript state management library that syncs application state with [loro-cr

- [`loro-mirror`](./packages/core): Core state management functionality
- [`loro-mirror-react`](./packages/react): React integration with hooks and context
- [`loro-mirror-jotai`](./packages/jotai): Jotai integration

## Installation

### Core Package

```bash
npm install loro-mirror loro-crdt
# or
Expand All @@ -28,15 +27,7 @@ yarn add loro-mirror loro-crdt
pnpm add loro-mirror loro-crdt
```

### React Package

```bash
npm install loro-mirror-react loro-mirror loro-crdt
# or
yarn add loro-mirror-react loro-mirror loro-crdt
# or
pnpm add loro-mirror-react loro-mirror loro-crdt
```
`loro-mirror-react` and `loro-mirror-jotai` are optional.

## Quick Start

Expand Down Expand Up @@ -316,6 +307,7 @@ For detailed documentation, see the README files in each package:

- [Core Documentation](./packages/core/README.md)
- [React Documentation](./packages/react/README.md)
- [Jotai Documentation](./packages/jotai/README.md)

## API Reference (Core Mirror)

Expand Down
83 changes: 83 additions & 0 deletions packages/jotai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Loro Mirror for Jotai

Jotai integration for Loro Mirror, providing atomic state management with Loro CRDT synchronization.

## Installation

```bash
# Using pnpm
pnpm add loro-mirror-jotai jotai loro-crdt

# Using npm
npm install loro-mirror-jotai jotai loro-crdt

# Using yarn
yarn add loro-mirror-jotai jotai loro-crdt
```

## Usage

Create a `loroMirrorAtom` to represent your shared state. It syncs automatically with the provided Loro document.

```tsx
import { useAtom } from 'jotai';
import { LoroDoc } from 'loro-crdt';
import { schema } from 'loro-mirror';
import { loroMirrorAtom } from 'loro-mirror-jotai';

type TodoStatus = "todo" | "inProgress" | "done";

// 1. Define your schema
const todoSchema = schema({
todos: schema.LoroList(
schema.LoroMap({
text: schema.String(),
status: schema.String<TodoStatus>()
}),
),
});

// 2. Create a Loro document instance
const doc = new LoroDoc();

// 3. Create the Jotai atom with Loro Mirror config
const todoAtom = loroMirrorAtom({
doc,
schema: todoSchema,
initialState: { todos: [] },
});

// 4. Use it in your React component
function TodoApp() {
const [state, setState] = useAtom(todoAtom);

const addTodo = () => {
setState((prevState) => ({
todos: [
...prevState.todos,
{
text: 'New Todo',
status: "todo",
},
],
}));
};

return (
<div>
<button onClick={addTodo}>Add Todo</button>
<ul>
{state.todos.map((todo) => (
<li key={todo.text}>
{todo.text}: {todo.status}
</li>
))}
</ul>
</div>
);
}
```

## License

[MIT](./LICENSE)
50 changes: 50 additions & 0 deletions packages/jotai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "loro-mirror-jotai",
"version": "0.1.0",
"description": "Jotai integration for Loro Mirror - a state management library with Loro CRDT synchronization",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc -p .",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
},
"keywords": [
"loro",
"crdt",
"state",
"management",
"sync",
"mirror",
"jotai"
],
"author": "",
"license": "MIT",
"dependencies": {
"loro-mirror": "workspace:*"
},
"peerDependencies": {
"jotai": "^2.0.0",
"loro-crdt": "^1.5.12"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^16.3.0",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"jotai": "^2.0.0",
"jsdom": "^23.0.1",
"loro-crdt": "^1.5.12",
"loro-mirror": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.3.3",
"vitest": "^1.0.4"
}
}
40 changes: 40 additions & 0 deletions packages/jotai/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const typescript = require('rollup-plugin-typescript2');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const pkg = require('./package.json');

module.exports = {
input: 'src/index.ts',
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
},
{
file: pkg.module,
format: 'esm',
sourcemap: true,
},
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
'jotai',
'jotai/utils',
],
plugins: [
nodeResolve(),
commonjs(),
typescript({
useTsconfigDeclarationDir: true,
tsconfigOverride: {
compilerOptions: {
declaration: true,
declarationDir: 'dist',
},
exclude: ['**/*.test.ts', '**/*.test.tsx', 'tests'],
},
}),
],
};
123 changes: 123 additions & 0 deletions packages/jotai/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Jotai integration for Loro Mirror - Atomic state management with Loro CRDT synchronization
*
* This package provides atom-based state management following Jotai's bottom-up approach.
* Each piece of state is represented as an atom, enabling fine-grained reactivity and composition.
*/

import { atom, WritableAtom } from 'jotai';

// Import types only to avoid module resolution issues
import type { LoroDoc } from "loro-crdt";
import { createStore, SchemaType, Store } from "loro-mirror";

/**
* Configuration for creating a Loro Mirror atom
*/
export interface LoroMirrorAtomConfig<T = any> {
/**
* The Loro document to sync with
*/
doc: LoroDoc;

/**
* The schema definition for the state
*/
schema: any;


/**
* Initial state (optional)
*/
initialState?: Partial<T>;

/**
* Whether to validate state updates against the schema
* @default true
*/
validateUpdates?: boolean;

/**
* Whether to throw errors on validation failures
* @default false
*/
throwOnValidationError?: boolean;

/**
* Debug mode - logs operations
* @default false
*/
debug?: boolean;
}


/**
* Creates a primary state atom that syncs with Loro
*
* This is the main atom that holds the synchronized state.
* It automatically syncs with the Loro document and notifies subscribers.
*
* @example
* ```tsx
* const todoSchema = schema({
* todos: schema.LoroList(schema.LoroMap({
* id: schema.String(),
* text: schema.String(),
* completed: schema.Boolean({ defaultValue: false }),
* })),
* });
*
* const todoAtom = loroAtom({
* doc: new LoroDoc(),
* schema: todoSchema,
* initialState: { todos: [] },
* key: 'todos'
* });
*
* function TodoApp() {
* const [state, setState] = useAtom(todoAtom);
* // Use state and setState...
* }
* ```
*/
export function loroMirrorAtom<T = any>(
config: LoroMirrorAtomConfig<T>
): WritableAtom<T, [T | ((prev: T) => T)], void> {
const store = createStore(config);
const stateAtom = atom(store.getState());
let sub: () => void | undefined;
const initAtom = atom(null, async (_get, set, action: "init" | "destroy") => {
if (action === "init") {
sub = store.subscribe((state) => {
set(stateAtom, state);
});
} else {
sub?.()
}
})

initAtom.onMount = (action) => {
action("init");
return () => {
action("destroy");
}
}

const base = atom(
(get) => {
get(initAtom)
return get(stateAtom);
},
(get, _set, update) => {
const currentState = get(stateAtom);
if (typeof update === 'function') {
const newState = (update as (prev: T) => T)(currentState);
store.setState(newState as Partial<T>);
} else {
store.setState(update as Partial<T>);
}
}
);

return base;
}
Loading