Skip to content

Commit c5a2d9b

Browse files
authored
H-5261: Add Petrinaut x Automerge POC (#72)
* petrinaut-automerge * delete sim-core pre-push hook * update readme * clean up logging a bit * redirect engine-web main * revert package.json change
1 parent ba6f4b5 commit c5a2d9b

File tree

16 files changed

+3754
-66
lines changed

16 files changed

+3754
-66
lines changed

.config/husky/pre-push

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,43 @@
11
{
2-
"name": "@hashintel/engine-web",
3-
"version": "0.1.1",
4-
"description": "HASH Core JavaScript bindings",
5-
"repository": "https://github.com/hashintel/labs",
6-
"license": "AGPL-3.0-only",
7-
"scripts": {
8-
"clean": "rimraf dist dist-node wasm/bundler wasm/node",
9-
"prebuild": "wasm-pack build --target bundler --out-dir wasm/bundler --out-name hash",
10-
"build": "npx tsc",
11-
"prebuild:node": "wasm-pack build --target nodejs --out-dir wasm/node --out-name hash",
12-
"build:node": "tsc --module commonjs --outdir dist-node",
13-
"fmt": "prettier --write --cache \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\" && eslint --quiet --fix \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\"",
14-
"lint": "prettier --check --cache \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\" && eslint --quiet \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\" && tsc --noEmit",
15-
"pretest": "yarn prebuild:node",
16-
"test": "jest",
17-
"prepare": "npx npm-run-all clean build build:node",
18-
"prod-env": "cross-env-shell NODE_OPTIONS=\"--max_old_space_size=4096 --openssl-legacy-provider\" NODE_ENV=production",
19-
"dev-env": "cross-env-shell NODE_OPTIONS=\"--max_old_space_size=4096 --openssl-legacy-provider\" NODE_ENV=development",
20-
"build-stdlib": "yarn prod-env webpack --mode production",
21-
"build-stdlib-dev": "yarn dev-env webpack --mode development"
22-
},
23-
"files": [
24-
"dist/**/*",
25-
"dist-node/**/*",
26-
"wasm/**/*"
27-
],
28-
"jest": {
29-
"preset": "ts-jest/presets/js-with-babel",
30-
"testEnvironment": "node",
31-
"globals": {
32-
"ts-jest": {
33-
"babelConfig": true
34-
}
35-
}
36-
},
37-
"dependencies": {
38-
"jstat": "1.9.4",
39-
"node-fetch": "2.6.1",
40-
"promise-worker-transferable": "github:hashdeps/promise-worker-transferable",
41-
"rxjs": "6.6.6"
42-
},
43-
"main": "dist-node/index.js",
44-
"module": "dist/index.js",
45-
"types": "dist/index.d.ts",
46-
"devDependencies": {}
2+
"name": "@hashintel/engine-web",
3+
"version": "0.1.1",
4+
"description": "HASH Core JavaScript bindings",
5+
"repository": "https://github.com/hashintel/labs",
6+
"license": "AGPL-3.0-only",
7+
"scripts": {
8+
"clean": "rimraf dist dist-node wasm/bundler wasm/node",
9+
"prebuild": "wasm-pack build --target bundler --out-dir wasm/bundler --out-name hash",
10+
"build": "npx tsc",
11+
"prebuild:node": "wasm-pack build --target nodejs --out-dir wasm/node --out-name hash",
12+
"build:node": "tsc --module commonjs --outdir dist-node",
13+
"fmt": "prettier --write --cache \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\" && eslint --quiet --fix \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\"",
14+
"lint": "prettier --check --cache \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\" && eslint --quiet \"*.{ts,js,json}\" \"{scripts,src}/**/*.{ts,js,json}\" && tsc --noEmit",
15+
"pretest": "yarn prebuild:node",
16+
"test": "jest",
17+
"prepare": "npx npm-run-all clean build build:node",
18+
"prod-env": "cross-env-shell NODE_OPTIONS=\"--max_old_space_size=4096 --openssl-legacy-provider\" NODE_ENV=production",
19+
"dev-env": "cross-env-shell NODE_OPTIONS=\"--max_old_space_size=4096 --openssl-legacy-provider\" NODE_ENV=development",
20+
"build-stdlib": "yarn prod-env webpack --mode production",
21+
"build-stdlib-dev": "yarn dev-env webpack --mode development"
22+
},
23+
"files": ["dist/**/*", "dist-node/**/*", "wasm/**/*"],
24+
"jest": {
25+
"preset": "ts-jest/presets/js-with-babel",
26+
"testEnvironment": "node",
27+
"globals": {
28+
"ts-jest": {
29+
"babelConfig": true
30+
}
31+
}
32+
},
33+
"dependencies": {
34+
"jstat": "1.9.4",
35+
"node-fetch": "2.6.1",
36+
"promise-worker-transferable": "github:hashdeps/promise-worker-transferable",
37+
"rxjs": "6.6.6"
38+
},
39+
"main": "dist-node/index.js",
40+
"module": "dist/index.js",
41+
"types": "dist/index.d.ts",
42+
"devDependencies": {}
4743
}

pocs/petrinaut-automerge/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Petrinaut x Automerge
2+
3+
This is a POC of [Petrinaut](https://github.com/hashintel/hash/tree/main/libs/%40hashintel/petrinaut) (a Petri net editor) with an [Automerge](https://automerge.org/) backend.
4+
5+
## Setup
6+
7+
```bash
8+
cd pocs/petrinaut-automerge
9+
yarn
10+
```
11+
12+
Run `yarn dev` and navigate to http://localhost:5173 to see the app running.
13+
14+
## Known issues
15+
16+
1. Some changes in the UI don't create a granular update and are therefore are not good candidates for exploring visual diffing.
17+
18+
- **Granular updates**: add node; add connection; move node; edit net title; auto-layout (click 'layout' button, only nodes that actually move will have their x and y position updated)
19+
- **Coarse updates**: edit token types (overwrites entire object); edit node title (overwrites entire title, doesn't use `changeText`); edit node token markings (overwrites entire object).
20+
2. Sometimes the app will hang at different points when trying to retrieve data from the Automerge sync server. This can typically be temporarily resolved by removing the root Automerge URL from local storage, and visiting http://localhost:5173 to start a new one (remove any Automerge URL from the address). I don't know why this happens. **This is why there are a lot of logs relating to `useDocument`** – so I can see where it hangs. These can be deleted.
21+
3. When creating a new net, MUI Autocomplete thinks the value is undefined. Unclear why this is (it should be defined), not going to invest time in it right now.
22+
23+
## Questions re. Automerge
24+
1. Why known issue (2) happens
25+
2. What the best way is of subscribing to patches as they change (There's currently a hacky version in `petrinaut-wrapper.tsx` which logs out patches on every render if the head has changed)
26+
3. When/why will there be more than one `head`?
27+
28+
## TODOs
29+
30+
1. Put POC into Patchwork
31+
2. Discuss approach to visual diffing
32+
- have Petrinaut be aware of change history
33+
- ability to explore change history, with appropriate visual indicators for diffs (deletions, additions)
34+
3. Add more granular updates
35+
4. Allow deleting nodes / edges
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/png" href="/hash.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Petrinaut & Automerge</title>
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="/src/main.tsx"></script>
14+
</body>
15+
16+
</html>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "petrinaut-automerge",
3+
"private": true,
4+
"version": "0.0.1",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@automerge/react": "^2.0.0",
14+
"@emotion/react": "11.14.0",
15+
"@emotion/styled": "11.14.1",
16+
"@hashintel/design-system": "0.0.9-canary.2",
17+
"@hashintel/petrinaut": "0.0.4",
18+
"@mui/material": "5.18.0",
19+
"react": "18.3.1",
20+
"react-dom": "18.3.1",
21+
"react-use": "17.6.0",
22+
"vite-plugin-wasm": "3.3.0"
23+
},
24+
"devDependencies": {
25+
"@types/react": "18.3.12",
26+
"@types/react-dom": "18.3.1",
27+
"@vitejs/plugin-react": "4.3.3",
28+
"eslint": "8.45.0",
29+
"eslint-plugin-react-hooks": "4.6.0",
30+
"eslint-plugin-react-refresh": "0.4.3",
31+
"typescript": "5.2",
32+
"typescript-eslint": "7.3.1",
33+
"vite": "5"
34+
},
35+
"packageManager": "[email protected]+sha256.c17d3797fb9a9115bf375e31bfd30058cac6bc9c3b8807a3d8cb2094794b51ca"
36+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
html,
2+
body,
3+
#root {
4+
height: 100%;
5+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { Suspense } from "react";
2+
import ReactDOM from "react-dom/client";
3+
import {
4+
Repo,
5+
BroadcastChannelNetworkAdapter,
6+
WebSocketClientAdapter,
7+
IndexedDBStorageAdapter,
8+
RepoContext,
9+
type DocHandle,
10+
} from "@automerge/react";
11+
import { CssBaseline, ThemeProvider } from "@mui/material";
12+
import { CacheProvider } from "@emotion/react";
13+
import { createEmotionCache, theme } from "@hashintel/design-system/theme";
14+
15+
import App from "./main/app.tsx";
16+
import "./index.css";
17+
18+
import { getOrCreateRoot, type RootDocument } from "./rootDoc.ts";
19+
20+
const repo = new Repo({
21+
network: [
22+
new BroadcastChannelNetworkAdapter(),
23+
new WebSocketClientAdapter("wss://sync.automerge.org"),
24+
],
25+
storage: new IndexedDBStorageAdapter(),
26+
});
27+
28+
// Add the repo to the global window object so it can be accessed in the browser console
29+
// This is useful for debugging and testing purposes.
30+
declare global {
31+
interface Window {
32+
repo: Repo;
33+
// We also add the handle to the global window object for debugging
34+
handle: DocHandle<RootDocument>;
35+
}
36+
}
37+
window.repo = repo;
38+
39+
const emotionCache = createEmotionCache();
40+
41+
// Depending if we have an AutomergeUrl, either find or create the document
42+
const rootDocUrl = getOrCreateRoot(repo);
43+
window.handle = await repo.find(rootDocUrl);
44+
45+
// biome-ignore lint/style/noNonNullAssertion: we know it exists
46+
ReactDOM.createRoot(document.getElementById("root")!).render(
47+
<React.StrictMode>
48+
<Suspense fallback={<div>Suspense fallback...</div>}>
49+
<RepoContext.Provider value={repo}>
50+
<CacheProvider value={emotionCache}>
51+
<ThemeProvider theme={theme}>
52+
<CssBaseline />
53+
<App rootDocUrl={window.handle.url} />
54+
</ThemeProvider>
55+
</CacheProvider>
56+
</RepoContext.Provider>
57+
</Suspense>
58+
</React.StrictMode>,
59+
);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
isValidAutomergeUrl,
3+
useDocument,
4+
useRepo,
5+
type AutomergeUrl,
6+
} from "@automerge/react";
7+
import { useHash } from "react-use";
8+
import type { RootDocument } from "../rootDoc";
9+
import { useCallback, useMemo, useState } from "react";
10+
import { type PetriNet, PetrinautWrapper } from "./app/petrinaut-wrapper";
11+
import { defaultTokenTypes } from "@hashintel/petrinaut";
12+
13+
const createDefaultPetriNet = (): PetriNet => ({
14+
petriNetDefinition: {
15+
arcs: [],
16+
nodes: [],
17+
tokenTypes: JSON.parse(JSON.stringify(defaultTokenTypes)),
18+
},
19+
title: "New Petri Net",
20+
});
21+
22+
function App({ rootDocUrl }: { rootDocUrl: AutomergeUrl }) {
23+
const [hash, setHash] = useHash();
24+
const cleanHash = hash.slice(1); // Remove the leading '#'
25+
const selectedDocUrl =
26+
cleanHash && isValidAutomergeUrl(cleanHash)
27+
? (cleanHash as AutomergeUrl)
28+
: undefined;
29+
30+
const [hasAddedFirstNet, setHasAddedFirstNet] = useState(false);
31+
32+
const repo = useRepo();
33+
34+
console.log("Before useDocument in app.tsx, rootDocUrl", rootDocUrl);
35+
36+
const [rootDoc, changeRootDoc] = useDocument<RootDocument>(rootDocUrl, {
37+
suspense: true,
38+
});
39+
40+
console.log("loaded rootDoc in app.tsx", rootDoc);
41+
42+
const petriNetUrls = useMemo(() => {
43+
return rootDoc?.petriNetUrls ?? [];
44+
}, [rootDoc]);
45+
46+
const createAndSelectPetriNet = useCallback(
47+
(petriNet: PetriNet | null) => {
48+
const newPetriNet = repo.create<PetriNet>(
49+
petriNet ?? createDefaultPetriNet(),
50+
);
51+
52+
changeRootDoc((rootDoc) => {
53+
rootDoc.petriNetUrls.push(newPetriNet.url);
54+
});
55+
56+
setHash(newPetriNet.url);
57+
},
58+
[repo, changeRootDoc, setHash],
59+
);
60+
61+
if (!selectedDocUrl) {
62+
console.log("No selected doc url");
63+
const firstOption = petriNetUrls[0];
64+
65+
if (firstOption) {
66+
console.log("Setting hash to first option", firstOption);
67+
setHash(firstOption);
68+
} else if (!hasAddedFirstNet) {
69+
console.log("Creating new petri net");
70+
createAndSelectPetriNet(null);
71+
setHasAddedFirstNet(true);
72+
}
73+
return null;
74+
}
75+
76+
console.log("Before useDocument in app.tsx, selectedDocUrl", selectedDocUrl);
77+
78+
const [selectedPetriNetDoc, changeSelectedPetriNetDoc] =
79+
useDocument<PetriNet>(selectedDocUrl, { suspense: true });
80+
81+
console.log("loaded selectedPetriNetDoc in app.tsx", selectedPetriNetDoc);
82+
83+
return (
84+
<PetrinautWrapper
85+
changePetriNetDoc={changeSelectedPetriNetDoc}
86+
createAndSelectPetriNet={createAndSelectPetriNet}
87+
loadPetriNetFromUrl={(url) => {
88+
setHash(url);
89+
}}
90+
rootDoc={rootDoc}
91+
selectedPetriNetUrl={selectedDocUrl}
92+
selectedPetriNetDoc={selectedPetriNetDoc}
93+
/>
94+
);
95+
}
96+
97+
export default App;

0 commit comments

Comments
 (0)