Skip to content

Commit 5a381ab

Browse files
authored
feat: jotai binding (#13)
* feat: jotai binding * fix: test deps * chore: test * Revert "chore: test" This reverts commit 039a1a1. * test: build first * fix: rm jotai key * doc: refine * chore: rm a test
1 parent 0e06194 commit 5a381ab

File tree

11 files changed

+1853
-17
lines changed

11 files changed

+1853
-17
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ jobs:
2727
run: pnpm install --frozen-lockfile
2828

2929
- name: Run tests
30-
run: pnpm test
30+
run: pnpm run build && pnpm test

README.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ A TypeScript state management library that syncs application state with [loro-cr
1515

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

1920
## Installation
2021

21-
### Core Package
22-
2322
```bash
2423
npm install loro-mirror loro-crdt
2524
# or
@@ -28,15 +27,7 @@ yarn add loro-mirror loro-crdt
2827
pnpm add loro-mirror loro-crdt
2928
```
3029

31-
### React Package
32-
33-
```bash
34-
npm install loro-mirror-react loro-mirror loro-crdt
35-
# or
36-
yarn add loro-mirror-react loro-mirror loro-crdt
37-
# or
38-
pnpm add loro-mirror-react loro-mirror loro-crdt
39-
```
30+
`loro-mirror-react` and `loro-mirror-jotai` are optional.
4031

4132
## Quick Start
4233

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

317308
- [Core Documentation](./packages/core/README.md)
318309
- [React Documentation](./packages/react/README.md)
310+
- [Jotai Documentation](./packages/jotai/README.md)
319311

320312
## API Reference (Core Mirror)
321313

packages/jotai/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Loro Mirror for Jotai
2+
3+
Jotai integration for Loro Mirror, providing atomic state management with Loro CRDT synchronization.
4+
5+
## Installation
6+
7+
```bash
8+
# Using pnpm
9+
pnpm add loro-mirror-jotai jotai loro-crdt
10+
11+
# Using npm
12+
npm install loro-mirror-jotai jotai loro-crdt
13+
14+
# Using yarn
15+
yarn add loro-mirror-jotai jotai loro-crdt
16+
```
17+
18+
## Usage
19+
20+
Create a `loroMirrorAtom` to represent your shared state. It syncs automatically with the provided Loro document.
21+
22+
```tsx
23+
import { useAtom } from 'jotai';
24+
import { LoroDoc } from 'loro-crdt';
25+
import { schema } from 'loro-mirror';
26+
import { loroMirrorAtom } from 'loro-mirror-jotai';
27+
28+
type TodoStatus = "todo" | "inProgress" | "done";
29+
30+
// 1. Define your schema
31+
const todoSchema = schema({
32+
todos: schema.LoroList(
33+
schema.LoroMap({
34+
text: schema.String(),
35+
status: schema.String<TodoStatus>()
36+
}),
37+
),
38+
});
39+
40+
// 2. Create a Loro document instance
41+
const doc = new LoroDoc();
42+
43+
// 3. Create the Jotai atom with Loro Mirror config
44+
const todoAtom = loroMirrorAtom({
45+
doc,
46+
schema: todoSchema,
47+
initialState: { todos: [] },
48+
});
49+
50+
// 4. Use it in your React component
51+
function TodoApp() {
52+
const [state, setState] = useAtom(todoAtom);
53+
54+
const addTodo = () => {
55+
setState((prevState) => ({
56+
todos: [
57+
...prevState.todos,
58+
{
59+
text: 'New Todo',
60+
status: "todo",
61+
},
62+
],
63+
}));
64+
};
65+
66+
return (
67+
<div>
68+
<button onClick={addTodo}>Add Todo</button>
69+
<ul>
70+
{state.todos.map((todo) => (
71+
<li key={todo.text}>
72+
{todo.text}: {todo.status}
73+
</li>
74+
))}
75+
</ul>
76+
</div>
77+
);
78+
}
79+
```
80+
81+
## License
82+
83+
[MIT](./LICENSE)

packages/jotai/package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "loro-mirror-jotai",
3+
"version": "0.1.0",
4+
"description": "Jotai integration for Loro Mirror - a state management library with Loro CRDT synchronization",
5+
"main": "dist/index.js",
6+
"module": "dist/index.esm.js",
7+
"types": "dist/index.d.ts",
8+
"files": [
9+
"dist"
10+
],
11+
"scripts": {
12+
"build": "tsc -p .",
13+
"test": "vitest run",
14+
"test:watch": "vitest",
15+
"lint": "eslint src --ext .ts,.tsx",
16+
"typecheck": "tsc --noEmit"
17+
},
18+
"keywords": [
19+
"loro",
20+
"crdt",
21+
"state",
22+
"management",
23+
"sync",
24+
"mirror",
25+
"jotai"
26+
],
27+
"author": "",
28+
"license": "MIT",
29+
"dependencies": {
30+
"loro-mirror": "workspace:*"
31+
},
32+
"peerDependencies": {
33+
"jotai": "^2.0.0",
34+
"loro-crdt": "^1.5.12"
35+
},
36+
"devDependencies": {
37+
"@testing-library/jest-dom": "^6.1.5",
38+
"@testing-library/react": "^16.3.0",
39+
"@types/node": "^20.10.5",
40+
"@types/react": "^18.2.45",
41+
"jotai": "^2.0.0",
42+
"jsdom": "^23.0.1",
43+
"loro-crdt": "^1.5.12",
44+
"loro-mirror": "workspace:*",
45+
"react": "^18.2.0",
46+
"react-dom": "^18.2.0",
47+
"typescript": "^5.3.3",
48+
"vitest": "^1.0.4"
49+
}
50+
}

packages/jotai/rollup.config.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const typescript = require('rollup-plugin-typescript2');
2+
const { nodeResolve } = require('@rollup/plugin-node-resolve');
3+
const commonjs = require('@rollup/plugin-commonjs');
4+
const pkg = require('./package.json');
5+
6+
module.exports = {
7+
input: 'src/index.ts',
8+
output: [
9+
{
10+
file: pkg.main,
11+
format: 'cjs',
12+
sourcemap: true,
13+
},
14+
{
15+
file: pkg.module,
16+
format: 'esm',
17+
sourcemap: true,
18+
},
19+
],
20+
external: [
21+
...Object.keys(pkg.dependencies || {}),
22+
...Object.keys(pkg.peerDependencies || {}),
23+
'jotai',
24+
'jotai/utils',
25+
],
26+
plugins: [
27+
nodeResolve(),
28+
commonjs(),
29+
typescript({
30+
useTsconfigDeclarationDir: true,
31+
tsconfigOverride: {
32+
compilerOptions: {
33+
declaration: true,
34+
declarationDir: 'dist',
35+
},
36+
exclude: ['**/*.test.ts', '**/*.test.tsx', 'tests'],
37+
},
38+
}),
39+
],
40+
};

packages/jotai/src/index.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Jotai integration for Loro Mirror - Atomic state management with Loro CRDT synchronization
3+
*
4+
* This package provides atom-based state management following Jotai's bottom-up approach.
5+
* Each piece of state is represented as an atom, enabling fine-grained reactivity and composition.
6+
*/
7+
8+
import { atom, WritableAtom } from 'jotai';
9+
10+
// Import types only to avoid module resolution issues
11+
import type { LoroDoc } from "loro-crdt";
12+
import { createStore, SchemaType, Store } from "loro-mirror";
13+
14+
/**
15+
* Configuration for creating a Loro Mirror atom
16+
*/
17+
export interface LoroMirrorAtomConfig<T = any> {
18+
/**
19+
* The Loro document to sync with
20+
*/
21+
doc: LoroDoc;
22+
23+
/**
24+
* The schema definition for the state
25+
*/
26+
schema: any;
27+
28+
29+
/**
30+
* Initial state (optional)
31+
*/
32+
initialState?: Partial<T>;
33+
34+
/**
35+
* Whether to validate state updates against the schema
36+
* @default true
37+
*/
38+
validateUpdates?: boolean;
39+
40+
/**
41+
* Whether to throw errors on validation failures
42+
* @default false
43+
*/
44+
throwOnValidationError?: boolean;
45+
46+
/**
47+
* Debug mode - logs operations
48+
* @default false
49+
*/
50+
debug?: boolean;
51+
}
52+
53+
54+
/**
55+
* Creates a primary state atom that syncs with Loro
56+
*
57+
* This is the main atom that holds the synchronized state.
58+
* It automatically syncs with the Loro document and notifies subscribers.
59+
*
60+
* @example
61+
* ```tsx
62+
* const todoSchema = schema({
63+
* todos: schema.LoroList(schema.LoroMap({
64+
* id: schema.String(),
65+
* text: schema.String(),
66+
* completed: schema.Boolean({ defaultValue: false }),
67+
* })),
68+
* });
69+
*
70+
* const todoAtom = loroAtom({
71+
* doc: new LoroDoc(),
72+
* schema: todoSchema,
73+
* initialState: { todos: [] },
74+
* key: 'todos'
75+
* });
76+
*
77+
* function TodoApp() {
78+
* const [state, setState] = useAtom(todoAtom);
79+
* // Use state and setState...
80+
* }
81+
* ```
82+
*/
83+
export function loroMirrorAtom<T = any>(
84+
config: LoroMirrorAtomConfig<T>
85+
): WritableAtom<T, [T | ((prev: T) => T)], void> {
86+
const store = createStore(config);
87+
const stateAtom = atom(store.getState());
88+
let sub: () => void | undefined;
89+
const initAtom = atom(null, async (_get, set, action: "init" | "destroy") => {
90+
if (action === "init") {
91+
sub = store.subscribe((state) => {
92+
set(stateAtom, state);
93+
});
94+
} else {
95+
sub?.()
96+
}
97+
})
98+
99+
initAtom.onMount = (action) => {
100+
action("init");
101+
return () => {
102+
action("destroy");
103+
}
104+
}
105+
106+
const base = atom(
107+
(get) => {
108+
get(initAtom)
109+
return get(stateAtom);
110+
},
111+
(get, _set, update) => {
112+
const currentState = get(stateAtom);
113+
if (typeof update === 'function') {
114+
const newState = (update as (prev: T) => T)(currentState);
115+
store.setState(newState as Partial<T>);
116+
} else {
117+
store.setState(update as Partial<T>);
118+
}
119+
}
120+
);
121+
122+
return base;
123+
}

0 commit comments

Comments
 (0)