[
  {
    "path": ".eslintrc",
    "content": "// next is loading eslintrc from root directory, adding this to avoid eslint rules been overridden\n{}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n.vscode"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\n.next\nbuild\ndist\n*.tsbuildinfo\n*.gitignore\n*.svg\n*.lock\n*.npmignore\n*.sql\n*.png\n*.jpg\n*.jpeg\n*.gif\n*.ico\n*.sh\nDockerfile\nDockerfile.*\n.env\n.env.*\nLICENSE\n*.log\n.DS_Store\n.dockerignore\n*.patch\n*.toml\n*.prisma"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"useTabs\": false\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Motif\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# YFS\n\nSynchronize text files between the browser and the file system using the\n[File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)\nand [Yjs](https://yjs.dev/).\n\n\nBlog post explaining the underlying concepts: [Syncing text files between browser and disk using Yjs and the File System Access API](https://motif.land/blog/syncing-text-files-using-yjs-and-the-file-system-access-api).\n\n<br />\n<p align=\"center\">\n  <a aria-label=\"NPM version\" href=\"https://www.npmjs.com/package/@yfs/react\">\n    <img alt=\"\" src=\"https://badgen.net/npm/v/@yfs/react\">\n  </a>\n  <a aria-label=\"License\" href=\"https://github.com/motifland/yfs/blob/main/LICENSE\">\n    <img alt=\"\" src=\"https://badgen.net/npm/license/@yfs/react\">\n  </a>\n</p>\n\n## Installation\n\nTo get started, install the `@yfs/react` package via npm or yarn:\n\n```shell\n# npm\nnpm install @yfs/react\n\n# Yarn\nyarn add @yfs/react\n```\n\n## Usage\n\nExample:\n\n<!-- prettier-ignore -->\n```jsx\nimport React, { useState } from 'react'\nimport * as Y from 'yjs'\nimport useYFS from '@yfs/react'\n\nfunction Editor () {\n  const { setRootDirectory, directoryName, syncDoc } = useYFS()\n  const [doc] = useState<Y.Doc>(new Y.Doc())\n\n  return (\n    <div>\n      <button\n        onClick={() => {\n          if (!directoryName) {\n            setRootDirectory(true)\n          } else {\n            syncDoc('my-file.md', doc)\n          }\n        }}\n      >\n        Sync\n      </button>\n      {/* Editor code... */}\n    </div>\n  )\n}\n```\n\n## Authors\n\nThis library is created by the team behind [Motif](https://motif.land)\n([@motifland](https://twitter.com/motifland)).\n\n- Michael Fester ([@michaelfester](https://twitter.com/michaelfester))\n\nIt is based on the great work by [Kevin Jahns](https://twitter.com/kevin_jahns)\non [Yjs](https://yjs.dev/).\n\n## License\n\nMIT\n"
  },
  {
    "path": "examples/single-file-sync/.eslintrc.js",
    "content": "/* eslint-env node */\nmodule.exports = {\n  env: {\n    browser: true,\n    es2021: true,\n    node: true,\n  },\n  extends: ['eslint:recommended', 'plugin:react/recommended', 'next'],\n  ignorePatterns: ['public/**/*'],\n  parserOptions: {\n    ecmaVersion: 12,\n    sourceType: 'module',\n  },\n  plugins: ['react', 'react-hooks'],\n  rules: {\n    'react-hooks/rules-of-hooks': 'error',\n    'react-hooks/exhaustive-deps': [\n      'error',\n      { additionalHooks: '(useDeepCompareEffect)' },\n    ],\n    '@next/next/no-img-element': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n  overrides: [\n    {\n      files: ['*.ts', '*.tsx'],\n      parser: '@typescript-eslint/parser',\n      plugins: ['@typescript-eslint'],\n      extends: ['plugin:@typescript-eslint/recommended'],\n      rules: {\n        '@typescript-eslint/no-unused-vars': 'error',\n        '@typescript-eslint/no-explicit-any': 'off',\n        '@typescript-eslint/explicit-module-boundary-types': 'off',\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": "examples/single-file-sync/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n"
  },
  {
    "path": "examples/single-file-sync/README.md",
    "content": "This is a sample app showcasing a browser-based editor\n([Monaco](https://microsoft.github.io/monaco-editor/)) syncing its content with\nthe file system. It is using the\n[File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API),\nin conjunction with the [Yjs](https://yjs.dev/) CRDT implementation.\n\n## Getting Started\n\nFirst, install the dependencies:\n\n```bash\nyarn\n```\n\nThen, run the development server:\n\n```bash\nyarn dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the\nresult.\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/Editor.module.css",
    "content": ".editor {\n  width: 100%;\n  height: 100%;\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/GitHub.tsx",
    "content": "import * as React from 'react'\n\nconst GitHub = (props: any) => {\n  return (\n    <svg\n      width={24}\n      height={24}\n      fill=\"none\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585C20.565 22.347 24 17.733 24 12.303 24 5.506 18.63 0 12 0z\"\n        fill=\"#1C1917\"\n      />\n    </svg>\n  )\n}\n\nexport default GitHub\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/index.tsx",
    "content": "import React, { useCallback } from 'react'\nimport MonacoEditor from '@monaco-editor/react'\nimport theme from './theme'\nimport options from './options'\n\nexport default function Editor ({\n  onDidMount,\n}: {\n  onDidMount: (monaco: any, editor: any) => void\n}) {\n  const onEditorDidMount = useCallback(\n    (editor: any, monaco: any) => {\n      monaco.editor.defineTheme('custom', theme)\n      monaco.editor.setTheme('custom')\n      onDidMount(monaco, editor)\n    },\n    [onDidMount],\n  )\n\n  return (\n    <MonacoEditor\n      height=\"100%\"\n      defaultLanguage=\"markdown\"\n      defaultValue=\"# Welcome to Yfs\"\n      onMount={onEditorDidMount}\n      loading={<></>}\n      options={options}\n    />\n  )\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/options.ts",
    "content": "const options = {\n  codeLens: false,\n  fontSize: 14,\n  lineHeight: 24,\n  lineNumbers: 'off',\n  padding: {\n    top: 30,\n    bottom: 30\n  },\n  selectionHighlight: false,\n  wordWrap: 'on',\n  folding: false,\n  fontFamily:\n    'Menlo, ui-monospace, SFMono-Regular, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace',\n  hideCursorInOverviewRuler: true,\n  glyphMargin: false,\n  lightbulb: { enabled: false },\n  lineDecorationsWidth: 20,\n  minimap: { enabled: false },\n  renderLineHighlight: 'none',\n  roundedSelection: true,\n  scrollbar: {\n    alwaysConsumeMouseWheel: false,\n    horizontal: 'hidden',\n    vertical: 'hidden',\n    useShadows: false,\n    verticalHasArrows: false,\n    verticalScrollbarSize: 0\n  },\n  scrollBeyondLastLine: false,\n  smoothScrolling: true,\n  snippetSuggestions: 'none',\n  suggest: {\n    showIcons: false\n  },\n  tabCompletion: 'off',\n  tabSize: 2,\n  wordBasedSuggestions: false,\n  wrappingStrategy: 'advanced'\n}\n\nexport default options\n"
  },
  {
    "path": "examples/single-file-sync/components/Editor/theme.ts",
    "content": "const theme = {\n  base: 'vs',\n  inherit: true,\n  rules: [\n    { token: '', foreground: '0F172A' },\n    { token: 'invalid', foreground: '0F172A' },\n    { token: 'emphasis', fontStyle: 'italic' },\n    { token: 'strong', fontStyle: 'bold' },\n    { token: 'variable', foreground: '5c6773' },\n    { token: 'variable.predefined', foreground: '5c6773' },\n    { token: 'constant', foreground: '854D0E' },\n    { token: 'comment', foreground: 'abb0b6', fontStyle: 'italic' },\n    { token: 'number', foreground: '854D0E' },\n    { token: 'number.hex', foreground: '854D0E' },\n    { token: 'regexp', foreground: '4dbf99' },\n    { token: 'annotation', foreground: '701A75' },\n    { token: 'type', foreground: '701A75' },\n    { token: 'delimiter', foreground: '5c6773' },\n    { token: 'delimiter.html', foreground: '5c6773' },\n    { token: 'delimiter.xml', foreground: '5c6773' },\n    { token: 'tag', foreground: '701A75' },\n    { token: 'tag.id.jade', foreground: 'C026D3' },\n    { token: 'tag.class.jade', foreground: 'C026D3' },\n    { token: 'meta.scss', foreground: 'C026D3' },\n    { token: 'metatag', foreground: 'C026D3' },\n    { token: 'metatag.content.html', foreground: '0E7490' },\n    { token: 'metatag.html', foreground: 'C026D3' },\n    { token: 'metatag.xml', foreground: 'C026D3' },\n    { token: 'metatag.php', fontStyle: 'bold' },\n    { token: 'key', foreground: '701A75' },\n    { token: 'string.key.json', foreground: '701A75' },\n    { token: 'string.value.json', foreground: '0E7490' },\n    { token: 'attribute.name', foreground: '854D0E' },\n    { token: 'attribute.value', foreground: '0451A5' },\n    { token: 'attribute.value.number', foreground: 'abb0b6' },\n    { token: 'attribute.value.unit', foreground: '0E7490' },\n    { token: 'attribute.value.html', foreground: '0E7490' },\n    { token: 'attribute.value.xml', foreground: '0E7490' },\n    { token: 'string', foreground: '0F172A' },\n    { token: 'string.html', foreground: '0E7490' },\n    { token: 'string.sql', foreground: '0E7490' },\n    { token: 'string.yaml', foreground: '0E7490' },\n    { token: 'string.js', foreground: '0E7490' },\n    { token: 'string.mdx', foreground: '0E7490' },\n    { token: 'string.link', foreground: '0369A1' },\n    { token: 'string.heading', fontStyle: 'bold' },\n    { token: 'string.target', foreground: 'A1A1AA' },\n    { token: 'string.codedelimiter.mdx', foreground: 'A1A1AA' },\n    { token: 'keyword', foreground: '854d0e' },\n    { token: 'keyword.json', foreground: '854d0e' },\n    { token: 'keyword.flow', foreground: '854d0e' },\n    { token: 'keyword.flow.scss', foreground: '854d0e' },\n    { token: 'operator.scss', foreground: '666666' },\n    { token: 'operator.sql', foreground: '778899' },\n    { token: 'operator.swift', foreground: '666666' },\n    { token: 'predefined.sql', foreground: 'FF00FF' }\n  ],\n  // Cf. https://github.com/Microsoft/monaco-editor/blob/main/test/playground.generated/customizing-the-appearence-exposed-colors.html\n  colors: {\n    'editor.background': '#FFFFFF',\n    'editor.foreground': '#1c1917',\n    'editorIndentGuide.background': '#E4E4E7',\n    'editorIndentGuide.activeBackground': '#A1A1AA',\n    'editorCursor.foreground': '#51A7F8',\n    'editor.selectionBackground': '#CFDFEC',\n    'editor.inactiveSelectionBackground': '#F5F5F5',\n    'editorLineNumber.foreground': '#D1D5DB'\n  }\n}\n\nexport default theme\n"
  },
  {
    "path": "examples/single-file-sync/components/Footer/Footer.module.css",
    "content": ".syncFooter {\n  display: flex;\n  flex-direction: row;\n  gap: 10px;\n  align-items: center;\n  align-self: flex-start;\n  padding: 0 6px;\n  font-size: var(--font-size-sm);\n  color: var(--text-light);\n}\n\n.syncFooter p {\n  margin: 0;\n  padding: 0;\n}\n\n.dot {\n  width: 8px;\n  height: 8px;\n  border-radius: 9999px;\n}\n\n.dotBlue {\n  background-color: #15e3ff;\n}\n\n.dotOrange {\n  background-color: #f7c444;\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Footer/index.tsx",
    "content": "import React from 'react'\nimport styles from './Footer.module.css'\nimport sharedStyles from '../../styles/Shared.module.css'\n\nexport default function Footer ({\n  directoryName,\n  isWritePermissionGranted,\n  unsetRootDirectory,\n  grantWritePermission,\n}: {\n  directoryName: string | undefined\n  isWritePermissionGranted: boolean\n  unsetRootDirectory: () => void\n  grantWritePermission: () => void\n}) {\n  if (!directoryName) {\n    return <></>\n  }\n\n  return (\n    <div className={styles.syncFooter}>\n      <div\n        className={`${styles.dot} ${\n          isWritePermissionGranted ? styles.dotBlue : styles.dotOrange\n        }`}\n      />\n      {isWritePermissionGranted && (\n        <>\n          <p>\n            Syncing with folder{' '}\n            <span style={{ fontStyle: 'italic' }}>{directoryName}</span>.\n          </p>\n          <a className={sharedStyles.subtleLink} onClick={unsetRootDirectory}>\n            Disconnect\n          </a>\n        </>\n      )}\n      {!isWritePermissionGranted && (\n        <>\n          <p>\n            Syncing with folder{' '}\n            <span style={{ fontStyle: 'italic' }}>{directoryName}</span> is\n            paused.\n          </p>\n          <a className={sharedStyles.subtleLink} onClick={grantWritePermission}>\n            Grant permissions\n          </a>\n        </>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Head/index.tsx",
    "content": "import React from 'react'\nimport NextHead from 'next/head'\n\nexport default function Head () {\n  return (\n    <NextHead>\n      <title>File Sync Demo</title>\n      <meta\n        name='description'\n        content='Synchronize text files between the browser and the file system'\n      />\n      <link rel='icon' href='/favicon.ico' />\n    </NextHead>\n  )\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Header/Header.module.css",
    "content": ".cta {\n  display: inline-block;\n  color: white;\n  background-color: var(--button-dark);\n  outline: 0 !important;\n  outline-offset: 2px;\n  box-shadow: none;\n  font-weight: 500;\n  font-size: var(--font-size-base);\n  padding: 10px 20px;\n  border-radius: 5px;\n  cursor: pointer;\n  -webkit-appearance: button;\n  border-style: solid;\n  border-width: 0;\n  transition-duration: 150ms;\n}\n\n.cta:hover {\n  opacity: 0.9;\n  box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n}\n\n.warning {\n  color: var(--text-warning);\n  text-align: center;\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Header/index.tsx",
    "content": "import React from 'react'\nimport styles from './Header.module.css'\n\nexport default function Header ({\n  isFSAPISupported,\n  directoryName,\n  setRootDirectory,\n}: {\n  isFSAPISupported: boolean\n  directoryName: string | undefined\n  setRootDirectory: (writable: boolean) => void\n}) {\n  return (\n    <div style={{ position: 'absolute', top: -60 }}>\n      {isFSAPISupported && !directoryName && (\n        <button\n          className={styles.cta}\n          onClick={() => {\n            setRootDirectory(true)\n          }}\n        >\n          Select folder\n        </button>\n      )}\n      {!isFSAPISupported && (\n        <p className={styles.warning}>\n          The File System Access API is currently not supported in this browser.\n        </p>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Navbar/Navbar.module.css",
    "content": ".nav {\n  display: flex;\n  flex-direction: row;\n  gap: 10px;\n  align-items: center;\n  width: 100%;\n  padding: 1.5rem;\n}\n\n.nav div:first-child {\n  flex: 1;\n  display: flex;\n  gap: 5px;\n  flex-direction: column;\n}\n\n.nav div:first-child h1,\n.nav div:first-child p {\n  margin: 0;\n}\n\n.nav div:first-child h1 {\n  font-size: 18px;\n  color: var(--text-dark);\n}\n\n.nav div:first-child p {\n  font-size: var(--font-size-sm);\n  color: var(--text-light);\n}\n\n.nav div:last-child {\n  display: flex;\n  gap: 25px;\n  flex-direction: row;\n  align-items: center;\n}\n\n.nav div:last-child a {\n  font-size: var(--font-size-sm);\n  color: var(--text-dark);\n}\n"
  },
  {
    "path": "examples/single-file-sync/components/Navbar/index.tsx",
    "content": "import React from 'react'\nimport GitHub from 'components/Editor/GitHub'\nimport styles from './Navbar.module.css'\nimport sharedStyles from '../../styles/Shared.module.css'\n\nexport default function Navbar () {\n  return (\n    <nav className={styles.nav}>\n      <div>\n        <h1>File Sync Demo</h1>\n        <p>\n          Using the{' '}\n          <a\n            className={sharedStyles.subtleLink}\n            href='https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API'\n            target='_blank'\n            rel='noreferrer'\n          >\n            File System Access API\n          </a>{' '}\n          +{' '}\n          <a\n            className={sharedStyles.subtleLink}\n            href='https://yjs.dev/'\n            target='_blank'\n            rel='noreferrer'\n          >\n            Yjs\n          </a>\n        </p>\n      </div>\n      <div>\n        <a\n          href='https://motif.land/blog/syncing-text-files-using-yjs-and-the-file-system-access-api'\n          target='_blank'\n          rel='noreferrer'\n        >\n          Read blog post ↗\n        </a>\n        <a\n          href='https://github.com/motifland/yfs'\n          target='_blank'\n          rel='noreferrer'\n        >\n          <GitHub />\n        </a>\n      </div>\n    </nav>\n  )\n}\n"
  },
  {
    "path": "examples/single-file-sync/lib/constants.ts",
    "content": "export const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =\n  'YDOC_UPDATE_ORIGIN_CURRENT_EDITOR'\n\nexport const ROOM_ID = 'yfs-sample-room'\nexport const TEST_FILE_NAME = 'yfs-test.md'\n\nexport const WEBRTC_SIGNALING_SERVERS = [\n  'wss://motif-signaling.fly.dev/',\n]\n"
  },
  {
    "path": "examples/single-file-sync/lib/use-interval.ts",
    "content": "import { useEffect, useRef } from 'react'\n\nexport const useInterval = (callback: () => void, delay: number | null) => {\n  const savedCallback = useRef<any>()\n\n  useEffect(() => {\n    savedCallback.current = callback\n  }, [callback])\n\n  useEffect(() => {\n    const tick = () => {\n      savedCallback.current()\n    }\n    if (delay !== null) {\n      const id = setInterval(tick, delay)\n      return () => clearInterval(id)\n    }\n  }, [delay])\n}\n"
  },
  {
    "path": "examples/single-file-sync/lib/y-config.ts",
    "content": "import * as Y from 'yjs'\nimport * as awarenessProtocol from 'y-protocols/awareness.js'\nimport { WebrtcProvider } from 'y-webrtc'\nimport { IndexeddbPersistence } from 'y-indexeddb'\nimport * as mutex from 'lib0/mutex.js'\nimport { MonacoBinding } from './y-monaco'\nimport { ROOM_ID, WEBRTC_SIGNALING_SERVERS } from './constants'\n\nexport const YDOC_UPDATE_ORIGIN_CURRENT_EDITOR =\n  'YDOC_UPDATE_ORIGIN_CURRENT_EDITOR'\nexport const YDOC_UPDATE_ORIGIN_PROGRAMMATIC = 'YDOC_UPDATE_ORIGIN_PROGRAMMATIC'\n\nexport class YConfig {\n  roomId: string\n  monaco: any\n  model: any\n  editor: any\n  userId: number\n  doc: Y.Doc | undefined\n  initialEncodedYDoc: string | undefined\n  awareness: awarenessProtocol.Awareness | undefined\n  webRTCProvider: WebrtcProvider | undefined\n  indexedDBPersistence: IndexeddbPersistence | undefined\n  monacoBinding: MonacoBinding | undefined\n  mux: mutex.mutex\n  updateTimeoutId: ReturnType<typeof setTimeout> | null\n\n  constructor (\n    monaco: any,\n    editor: any,\n    userId: number\n  ) {\n    this.roomId = ROOM_ID\n    // this.onNonProgrammaticDocUpdateImmediate = onNonProgrammaticDocUpdateImmediate\n    // this.onLocalEditorUpdateDebounced = onLocalEditorUpdateDebounced\n    this.monaco = monaco\n    this.editor = editor\n    this.model = editor.getModel()\n    this.userId = userId\n    this.mux = mutex.createMutex()\n    this.updateTimeoutId = null\n    this.initYDoc()\n  }\n\n  initYDoc (): void {\n    this.doc = new Y.Doc()\n\n    this.awareness = new awarenessProtocol.Awareness(this.doc)\n\n    this.webRTCProvider = new WebrtcProvider(this.roomId, this.doc, {\n      signaling: WEBRTC_SIGNALING_SERVERS,\n      password: null,\n      awareness: this.awareness,\n      maxConns: 50 + Math.floor(Math.random() * 15),\n      filterBcConns: true,\n      peerOpts: {}\n    })\n\n    this.monacoBinding = new MonacoBinding(\n      this.monaco,\n      this.doc.getText(),\n      this.model,\n      this.editor,\n      this.awareness,\n      this.userId\n    )\n\n    this.indexedDBPersistence = new IndexeddbPersistence(\n      'y-indexeddb',\n      this.doc\n    )\n\n    if (!this.webRTCProvider.connected) {\n      this.webRTCProvider.connect()\n    }\n  }\n\n  destroy (): void {\n    this.awareness?.destroy()\n    this.monacoBinding?.destroy()\n    this.webRTCProvider?.disconnect()\n    this.webRTCProvider?.destroy()\n    this.doc?.destroy()\n  }\n}\n"
  },
  {
    "path": "examples/single-file-sync/lib/y-monaco.ts",
    "content": "import * as Y from 'yjs'\nimport * as error from 'lib0/error.js'\nimport { createMutex, mutex } from 'lib0/mutex.js'\nimport * as awarenessProtocol from 'y-protocols/awareness.js'\nimport { YDOC_UPDATE_ORIGIN_CURRENT_EDITOR } from './constants'\n\ntype Direction = any\ntype Selection = {\n  start: Y.RelativePosition\n  end: Y.RelativePosition\n  direction: Direction\n}\n\nclass RelativeSelection {\n  start: Y.RelativePosition\n  end: Y.RelativePosition\n  direction: Direction\n\n  constructor (\n    start: Y.RelativePosition,\n    end: Y.RelativePosition,\n    direction: Direction,\n  ) {\n    this.start = start\n    this.end = end\n    this.direction = direction\n  }\n}\n\nconst createRelativeSelection = (\n  editor: any,\n  monacoModel: any,\n  type: Y.AbstractType<any>,\n) => {\n  const sel = editor.getSelection()\n  if (sel !== null) {\n    const startPos = sel.getStartPosition()\n    const endPos = sel.getEndPosition()\n    const start = Y.createRelativePositionFromTypeIndex(\n      type,\n      monacoModel.getOffsetAt(startPos),\n    )\n    const end = Y.createRelativePositionFromTypeIndex(\n      type,\n      monacoModel.getOffsetAt(endPos),\n    )\n    return new RelativeSelection(start, end, sel.getDirection())\n  }\n  return null\n}\n\nconst createMonacoSelectionFromRelativeSelection = (\n  monaco: any,\n  editor: any,\n  type: Y.AbstractType<any>,\n  relSel: Selection,\n  doc: Y.Doc,\n) => {\n  const start = Y.createAbsolutePositionFromRelativePosition(relSel.start, doc)\n  const end = Y.createAbsolutePositionFromRelativePosition(relSel.end, doc)\n  if (\n    start !== null &&\n    end !== null &&\n    start.type === type &&\n    end.type === type\n  ) {\n    const model = editor.getModel()\n    if (!model) {\n      return null\n    }\n    const startPos = model.getPositionAt(start.index)\n    const endPos = model.getPositionAt(end.index)\n    return monaco.Selection.createWithDirection(\n      startPos.lineNumber,\n      startPos.column,\n      endPos.lineNumber,\n      endPos.column,\n      relSel.direction,\n    )\n  }\n  return null\n}\n\nexport class MonacoBinding {\n  doc: Y.Doc\n  ytext: Y.Text\n  monacoModel: any\n  editor: any\n  mux: mutex\n  color: number\n  awareness: awarenessProtocol.Awareness | undefined\n  _savedSelections: Map<any, Selection>\n  _decorations: any\n  _rerenderDecorations: any\n  _monacoChangeHandler: any\n  _beforeTransaction: () => void\n  _ytextObserver: (event: Y.YTextEvent) => void\n\n  constructor (\n    monaco: any,\n    ytext: Y.Text,\n    monacoModel: any,\n    editor: any,\n    awareness: awarenessProtocol.Awareness,\n    userId: number,\n  ) {\n    this.doc = ytext.doc as Y.Doc\n    this.ytext = ytext\n    this.monacoModel = monacoModel\n    this.editor = editor\n    this.mux = createMutex()\n    this._savedSelections = new Map()\n    this.color = userId % 8\n\n    this._beforeTransaction = () => {\n      this.mux(() => {\n        this._savedSelections = new Map()\n        if (editor.getModel() === monacoModel) {\n          const rsel = createRelativeSelection(editor, monacoModel, ytext)\n          if (rsel !== null) {\n            this._savedSelections.set(editor, rsel)\n          }\n        }\n      })\n    }\n\n    this.doc.on('beforeAllTransactions', this._beforeTransaction)\n\n    this._decorations = new Map()\n\n    this._rerenderDecorations = () => {\n      if (awareness && editor.getModel() === monacoModel) {\n        // render decorations\n        const currentDecorations = this._decorations.get(editor) || []\n        const newDecorations: any = []\n        awareness.getStates().forEach((state, clientID) => {\n          if (\n            clientID !== this.doc.clientID &&\n            state.selection != null &&\n            state.selection.anchor != null &&\n            state.selection.head != null\n          ) {\n            const anchorAbs = Y.createAbsolutePositionFromRelativePosition(\n              state.selection.anchor,\n              this.doc,\n            )\n            const headAbs = Y.createAbsolutePositionFromRelativePosition(\n              state.selection.head,\n              this.doc,\n            )\n            if (\n              anchorAbs !== null &&\n              headAbs !== null &&\n              anchorAbs.type === ytext &&\n              headAbs.type === ytext\n            ) {\n              let start, end, afterContentClassName, beforeContentClassName\n              if (anchorAbs.index < headAbs.index) {\n                start = monacoModel.getPositionAt(anchorAbs.index)\n                end = monacoModel.getPositionAt(headAbs.index)\n                afterContentClassName = `ySelectionHead yBorder-${this.color}`\n                beforeContentClassName = null\n              } else {\n                start = monacoModel.getPositionAt(headAbs.index)\n                end = monacoModel.getPositionAt(anchorAbs.index)\n                afterContentClassName = null\n                beforeContentClassName = `ySelectionHead yBorder-${this.color}`\n              }\n              newDecorations.push({\n                range: new monaco.Range(\n                  start.lineNumber,\n                  start.column,\n                  end.lineNumber,\n                  end.column,\n                ),\n                options: {\n                  className: `ySelection-${this.color}`,\n                  afterContentClassName,\n                  beforeContentClassName,\n                },\n              })\n            }\n          }\n        })\n        this._decorations.set(\n          editor,\n          editor.deltaDecorations(currentDecorations, newDecorations),\n        )\n      } else {\n        // ignore decorations\n        this._decorations.delete(editor)\n      }\n    }\n\n    this._ytextObserver = event => {\n      this.mux(() => {\n        let index = 0\n        event.delta.forEach(op => {\n          if (op.retain !== undefined) {\n            index += op.retain\n          } else if (op.insert !== undefined) {\n            const pos = monacoModel.getPositionAt(index)\n            const range = new monaco.Selection(\n              pos.lineNumber,\n              pos.column,\n              pos.lineNumber,\n              pos.column,\n            )\n            monacoModel.applyEdits([{ range, text: op.insert as string }])\n            index += (op.insert as any).length\n          } else if (op.delete !== undefined) {\n            const pos = monacoModel.getPositionAt(index)\n            const endPos = monacoModel.getPositionAt(index + op.delete)\n            const range = new monaco.Selection(\n              pos.lineNumber,\n              pos.column,\n              endPos.lineNumber,\n              endPos.column,\n            )\n            monacoModel.applyEdits([{ range, text: '' }])\n          } else {\n            throw error.unexpectedCase()\n          }\n        })\n        this._savedSelections.forEach((rsel, editor) => {\n          const sel = createMonacoSelectionFromRelativeSelection(\n            monaco,\n            editor,\n            ytext,\n            rsel,\n            this.doc,\n          )\n          if (sel !== null) {\n            editor.setSelection(sel)\n          }\n        })\n      })\n      this._rerenderDecorations()\n    }\n\n    ytext.observe(this._ytextObserver)\n    monacoModel.setValue(ytext.toString())\n\n    this._monacoChangeHandler = monacoModel.onDidChangeContent((event: any) => {\n      // Apply changes from right to left\n      this.mux(() => {\n        this.doc.transact(() => {\n          event.changes\n            .sort(\n              (change1: any, change2: any) =>\n                change2.rangeOffset - change1.rangeOffset,\n            )\n            .forEach((change: any) => {\n              ytext.delete(change.rangeOffset, change.rangeLength)\n              ytext.insert(change.rangeOffset, change.text)\n            })\n        }, YDOC_UPDATE_ORIGIN_CURRENT_EDITOR)\n      })\n    })\n\n    monacoModel.onWillDispose(() => {\n      this.destroy()\n    })\n\n    if (awareness) {\n      editor.onDidChangeCursorSelection(() => {\n        if (editor.getModel() === monacoModel) {\n          const sel = editor.getSelection()\n          if (sel === null) {\n            return\n          }\n          let anchor = monacoModel.getOffsetAt(sel.getStartPosition())\n          let head = monacoModel.getOffsetAt(sel.getEndPosition())\n          if (sel.getDirection() === monaco.SelectionDirection.RTL) {\n            const tmp = anchor\n            anchor = head\n            head = tmp\n          }\n          awareness.setLocalStateField('selection', {\n            anchor: Y.createRelativePositionFromTypeIndex(ytext, anchor),\n            head: Y.createRelativePositionFromTypeIndex(ytext, head),\n          })\n        }\n      })\n      awareness.on('change', this._rerenderDecorations)\n      this.awareness = awareness\n    }\n  }\n\n  destroy (): void {\n    this._monacoChangeHandler.dispose()\n    this.ytext.unobserve(this._ytextObserver)\n    this.doc.off('beforeAllTransactions', this._beforeTransaction)\n    if (this.awareness) {\n      this.awareness.off('change', this._rerenderDecorations)\n    }\n  }\n}\n"
  },
  {
    "path": "examples/single-file-sync/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "examples/single-file-sync/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "examples/single-file-sync/package.json",
    "content": "{\n  \"name\": \"yfs-single-file-sync-example\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@monaco-editor/react\": \"^4.4.4\",\n    \"@yfs/react\": \"^0.0.15\",\n    \"lib0\": \"^0.2.51\",\n    \"next\": \"12.1.5\",\n    \"react\": \"18.1.0\",\n    \"react-dom\": \"18.1.0\",\n    \"y-indexeddb\": \"^9.0.7\",\n    \"y-webrtc\": \"^10.2.3\",\n    \"yjs\": \"^13.5.35\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^17.0.30\",\n    \"@types/react\": \"^18.0.8\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.22.0\",\n    \"@typescript-eslint/parser\": \"^5.22.0\",\n    \"eslint\": \"8.14.0\",\n    \"eslint-config-next\": \"12.1.5\",\n    \"typescript\": \"^4.6.4\"\n  }\n}\n"
  },
  {
    "path": "examples/single-file-sync/pages/_app.js",
    "content": "import '../styles/globals.css'\nimport '../styles/yjs.css'\n\nfunction MyApp({ Component, pageProps }) {\n  return <Component {...pageProps} />\n}\n\nexport default MyApp\n"
  },
  {
    "path": "examples/single-file-sync/pages/index.tsx",
    "content": "import React, { useCallback, useRef } from 'react'\nimport useFileSync from '@yfs/react'\nimport Editor from 'components/Editor'\nimport Head from 'components/Head'\nimport Navbar from 'components/Navbar'\nimport Footer from 'components/Footer'\nimport Header from 'components/Header'\nimport { useInterval } from 'lib/use-interval'\nimport styles from '../styles/Shared.module.css'\nimport { YConfig } from 'lib/y-config'\nimport { TEST_FILE_NAME } from 'lib/constants'\n\nexport default function Home () {\n  const config = useRef<YConfig | undefined>(undefined)\n  const {\n    isSupported,\n    setRootDirectory,\n    unsetRootDirectory,\n    grantWritePermission,\n    directoryName,\n    isWritePermissionGranted,\n    syncDoc\n  } = useFileSync()\n\n  const onEditorDidMount = useCallback((monaco: any, editor: any) => {\n    config.current?.destroy()\n    config.current = new YConfig(monaco, editor, Math.floor(Math.random() * 8))\n  }, [])\n\n  const sync = useCallback(() => {\n    if (!config.current?.doc) {\n      return\n    }\n    syncDoc(TEST_FILE_NAME, config.current.doc)\n  }, [syncDoc])\n\n  useInterval(sync, isWritePermissionGranted ? 10000 : null)\n\n  return (\n    <div className={styles.container}>\n      <Head />\n      <Navbar />\n      <main className={styles.main}>\n        <Header\n          isFSAPISupported={isSupported}\n          directoryName={directoryName}\n          setRootDirectory={setRootDirectory}\n        />\n        <div className={styles.editorContainer}>\n          <Editor onDidMount={onEditorDidMount} />\n        </div>\n        <Footer\n          directoryName={directoryName}\n          isWritePermissionGranted={isWritePermissionGranted}\n          unsetRootDirectory={unsetRootDirectory}\n          grantWritePermission={grantWritePermission}\n        />\n      </main>\n    </div>\n  )\n}\n"
  },
  {
    "path": "examples/single-file-sync/styles/Shared.module.css",
    "content": ".container {\n  min-height: 100vh;\n  padding: 2rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  color: var(--text-dark);\n}\n\n.container {\n  padding: 0;\n}\n\n.subtleLink {\n  text-decoration: underline dotted var(--text-extralight);\n  cursor: pointer;\n}\n\n.subtleLink:hover {\n  opacity: 0.9;\n}\n\n.main {\n  position: relative;\n  padding: 0 1.5rem 2rem 1.5rem;\n  width: 100%;\n  max-width: 980px;\n  margin-top: 50px;\n  display: flex;\n  gap: 10px;\n  flex-direction: column;\n  align-items: center;\n}\n\n.editorContainer {\n  width: 100%;\n  height: 100vh;\n  max-height: 480px;\n  padding: 0 1rem;\n  border-radius: 5px;\n  border-width: 1px;\n  overflow: hidden;\n  background-color: white;\n  border-color: var(--border-light);\n  border-style: solid;\n}\n"
  },
  {
    "path": "examples/single-file-sync/styles/globals.css",
    "content": "@import url('https://rsms.me/inter/inter.css');\n\n:root {\n  --background: #eeede9;\n  --border-light: #e3e3e3;\n  --text-dark: #1c1917;\n  --text-light: #78716c;\n  --text-extralight: #a8a29e;\n  --text-warning: #f97316;\n  --font-size-base: 16px;\n  --font-size-sm: 14px;\n  --button-dark: #0f172a;\n  --orange-light: #ffedd5;\n  --green-light: #dcfce7;\n  --sky-light: #e0f2fe;\n  --violet-light: #ede9fe;\n  --rose-light: #ffe4e6;\n  --amber-light: #fef3c7;\n  --lime-light: #ecfccb;\n  --fuchsia-light: #fae8ff;\n  --orange: #fb923c;\n  --green: #4ade80;\n  --sky: #38bdf8;\n  --violet: #a78bfa;\n  --rose: #fb7185;\n  --amber: #fbbf24;\n  --lime: #a3e635;\n  --fuchsia: #e879f9;\n}\n\nhtml,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,\n    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n  background-color: var(--background);\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n"
  },
  {
    "path": "examples/single-file-sync/styles/yjs.css",
    "content": "/* Selection */\n\n.ySelection-0 {\n  background-color: var(--orange-light);\n}\n.ySelection-1 {\n  background-color: var(--green-light);\n}\n.ySelection-2 {\n  background-color: var(--sky-light);\n}\n.ySelection-3 {\n  background-color: var(--violet-light);\n}\n.ySelection-4 {\n  background-color: var(--rose-light);\n}\n.ySelection-5 {\n  background-color: var(--amber-light);\n}\n.ySelection-6 {\n  background-color: var(--lime-light);\n}\n.ySelection-7 {\n  background-color: var(--fuchsia-light);\n}\n\n/* Background */\n\n.yBackground-0 {\n  background-color: var(--orange);\n}\n.yBackground-1 {\n  background-color: var(--green);\n}\n.yBackground-2 {\n  background-color: var(--sky);\n}\n.yBackground-3 {\n  background-color: var(--violet);\n}\n.yBackground-4 {\n  background-color: var(--rose);\n}\n.yBackground-5 {\n  background-color: var(--amber);\n}\n.yBackground-6 {\n  background-color: var(--lime);\n}\n.yBackground-7 {\n  background-color: var(--fuchsia);\n}\n\n/* Border */\n\n.yBorder-0 {\n  border-color: var(--orange);\n}\n.yBorder-1 {\n  border-color: var(--green);\n}\n.yBorder-2 {\n  border-color: var(--sky);\n}\n.yBorder-3 {\n  border-color: var(--violet);\n}\n.yBorder-4 {\n  border-color: var(--rose);\n}\n.yBorder-5 {\n  border-color: var(--amber);\n}\n.yBorder-6 {\n  border-color: var(--lime);\n}\n.yBorder-7 {\n  border-color: var(--fuchsia);\n}\n\n/* Selection head */\n\n.ySelectionHead {\n  position: absolute;\n  box-sizing: border-box;\n  height: 100%;\n  border-left-width: 2px;\n  border-top-width: 1px;\n  border-bottom-width: 1px;\n}\n"
  },
  {
    "path": "examples/single-file-sync/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"baseUrl\": \"./\",\n    \"target\": \"esnext\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\",\n      \"webworker\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"downlevelIteration\": true,\n    \"incremental\": true\n  },\n  \"exclude\": [\n    \"node_modules\"\n  ],\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\"\n  ]\n}\n"
  },
  {
    "path": "examples/single-file-sync/types.d.ts",
    "content": "declare module 'y-monaco'\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@yfs/react\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Synchronize text files between the browser and the file system\",\n  \"license\": \"MIT\",\n  \"repository\": \"git@github.com:motifland/yfs.git\",\n  \"author\": {\n    \"name\": \"Michael Fester\",\n    \"email\": \"michael.fester@gmail.com\",\n    \"url\": \"https://motif.land\"\n  },\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\"dist\"],\n  \"scripts\": {\n    \"build\": \"rimraf ./dist && tsc\",\n    \"prepublish\": \"npm run tsc\",\n    \"clean\": \"rimraf ./dist\"\n  },\n  \"dependencies\": {\n    \"diff\": \"^5.0.0\",\n    \"idb-keyval\": \"^6.1.0\",\n    \"react\": \"18.1.0\",\n    \"yjs\": \"^13.5.35\"\n  },\n  \"devDependencies\": {\n    \"@types/diff\": \"^5.0.2\",\n    \"@types/node\": \"^17.0.30\",\n    \"@types/react\": \"^18.0.8\",\n    \"typescript\": \"^4.6.4\"\n  }\n}\n"
  },
  {
    "path": "src/cache.ts",
    "content": "import { set as idbSet, get as idbGet } from 'idb-keyval'\nimport { STORE_KEY_CACHED_FS_FILE } from './constants'\n\ntype LastWriteCacheData = {\n  name: string\n  content: string\n  lastModified: number\n}\n\nconst getLastWriteCacheKey = (name: string) => {\n  return `${STORE_KEY_CACHED_FS_FILE}-${name}`\n}\n\nexport const setLastWriteCacheData = async (\n  name: string,\n  content: string,\n  lastModified: number\n) => {\n  await idbSet(\n    getLastWriteCacheKey(name),\n    JSON.stringify({\n      name,\n      content,\n      lastModified\n    })\n  )\n}\n\nexport const getLastWriteCacheData = async (\n  name: string\n): Promise<LastWriteCacheData | undefined> => {\n  const jsonFile = await idbGet(getLastWriteCacheKey(name))\n  if (jsonFile) {\n    return JSON.parse(jsonFile)\n  }\n  return undefined\n}\n"
  },
  {
    "path": "src/constants.ts",
    "content": "export const STORE_KEY_DIRECTORY_HANDLE = 'STORE_KEY_DIRECTORY_HANDLE'\nexport const STORE_KEY_CACHED_FS_FILE = 'STORE_KEY_CACHED_FS_FILE'\n"
  },
  {
    "path": "src/helpers.ts",
    "content": "export type HandleWithPath = {\n  handle: FileSystemHandle\n  path: string[]\n  type: 'file' | 'directory'\n}\n\nconst readWriteOptions = { mode: 'readwrite' }\n\nexport const isReadWritePermissionGranted = async (\n  handle: FileSystemFileHandle | FileSystemDirectoryHandle\n) => {\n  return (await (handle as any).queryPermission(readWriteOptions)) === 'granted'\n}\n\nexport const askReadWritePermissionsIfNeeded = async (\n  handle: FileSystemFileHandle | FileSystemDirectoryHandle\n) => {\n  if (await isReadWritePermissionGranted(handle)) {\n    return true\n  }\n\n  const permission = await (handle as any).requestPermission(readWriteOptions)\n  return permission === 'granted'\n}\n\nconst createEmptyFileInFolder = async (\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  name: string\n): Promise<FileSystemFileHandle> => {\n  return await parentDirectoryHandle.getFileHandle(name, { create: true })\n}\n\nexport const createFolderInFolder = async (\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  name: string\n): Promise<FileSystemDirectoryHandle> => {\n  return await parentDirectoryHandle.getDirectoryHandle(name, { create: true })\n}\n\nconst writeContentToFile = async (\n  fileHandle: FileSystemFileHandle,\n  content: string\n) => {\n  const writable = await (fileHandle as any).createWritable()\n  await writable.write(content)\n  await writable.close()\n}\n\nexport const writeContentToFileIfChanged = async (\n  fsFile: globalThis.File,\n  fileHandle: FileSystemFileHandle,\n  content: string\n) => {\n  const fsFileContent = await fsFile.text()\n  if (fsFileContent === content) {\n    return\n  }\n  await writeContentToFile(fileHandle, content)\n}\n\nexport const renameFile = async (\n  fsFile: globalThis.File,\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  name: string\n) => {\n  // Move and rename is not currently supported by the FileSystem\n  // Access API so we need to do this they manual way by creating\n  // a new file and deleting the old one.\n  const content = await fsFile.text()\n  await createFile(parentDirectoryHandle, name, content)\n  await deleteFile(parentDirectoryHandle, fsFile.name)\n}\n\nexport const moveFile = async (\n  fsFile: globalThis.File,\n  sourceDirectoryHandle: FileSystemDirectoryHandle,\n  destinationDirectoryHandle: FileSystemDirectoryHandle\n) => {\n  // Same comment as renameFile\n  const content = await fsFile.text()\n  await createFile(destinationDirectoryHandle, fsFile.name, content)\n  await deleteFile(sourceDirectoryHandle, fsFile.name)\n}\n\nexport const moveFolderContent = async (\n  sourceFolderHandle: FileSystemDirectoryHandle,\n  destinationFolderHandle: FileSystemDirectoryHandle\n) => {\n  for await (const handle of (sourceFolderHandle as any).values()) {\n    if (handle.kind === 'file') {\n      const fsFile = await (handle as FileSystemFileHandle).getFile()\n      await moveFile(fsFile, sourceFolderHandle, destinationFolderHandle)\n    } else if (handle.kind === 'directory') {\n      await moveFolder(handle, sourceFolderHandle, destinationFolderHandle)\n    }\n  }\n}\n\nexport const moveFolder = async (\n  folderHandle: FileSystemDirectoryHandle,\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  destinationFolderHandle: FileSystemDirectoryHandle\n) => {\n  const newFolderHandle = await createFolderInFolder(\n    destinationFolderHandle,\n    folderHandle.name\n  )\n  await moveFolderContent(folderHandle, newFolderHandle)\n  await deleteFolder(folderHandle.name, parentDirectoryHandle)\n}\n\nexport const renameFolder = async (\n  folderHandle: FileSystemDirectoryHandle,\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  newName: string\n) => {\n  const newFolderHandle = await createFolderInFolder(\n    parentDirectoryHandle,\n    newName\n  )\n  try {\n    await moveFolderContent(folderHandle, newFolderHandle)\n    await deleteFolder(folderHandle.name, parentDirectoryHandle)\n  } catch {\n    // Do nothing\n  }\n}\n\nexport const createFile = async (\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  name: string,\n  content: string\n): Promise<FileSystemFileHandle> => {\n  const newFileHandle = await createEmptyFileInFolder(\n    parentDirectoryHandle,\n    name\n  )\n  await writeContentToFile(newFileHandle, content)\n  return newFileHandle\n}\n\nexport const deleteFile = async (\n  parentDirectoryHandle: FileSystemDirectoryHandle,\n  name: string\n) => {\n  await parentDirectoryHandle.removeEntry(name)\n}\n\nexport const deleteFolder = async (\n  name: string,\n  parentDirectoryHandle: FileSystemDirectoryHandle\n) => {\n  await parentDirectoryHandle.removeEntry(name, {\n    recursive: true\n  })\n}\n\nexport const getFSFileHandle = async (\n  name: string,\n  directoryHandle: FileSystemDirectoryHandle\n): Promise<FileSystemFileHandle | undefined> => {\n  for await (const handle of (directoryHandle as any).values()) {\n    const relativePath = (await directoryHandle.resolve(handle)) || []\n    if (relativePath?.length === 1 && relativePath[0] === name) {\n      return handle\n    }\n  }\n  return undefined\n}\n\nexport const isHandlesEqual = async (\n  handle: FileSystemHandle | undefined,\n  otherHandle: FileSystemHandle | undefined\n) => {\n  if (!handle && !otherHandle) {\n    return true\n  }\n\n  if (handle && otherHandle) {\n    return await (handle as any)?.isSameEntry(otherHandle)\n  }\n\n  return false\n}\n\nexport const isIgnoredPath = (path: string[]): boolean => {\n  // Return true if the file at the given path should be ignored for\n  // syncing. This is the case currently if the path contains a component\n  // that starts with a period, e.g. \".git\" or \".DS_Store\".\n  return !!path.find(p => p.startsWith('.') || p.endsWith('.crswap'))\n}\n\nexport const isTextMimeType = (file: globalThis.File) => {\n  // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types\n  return file.type.startsWith('text/') || !file.type\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { set as idbSet, get as idbGet, del as idbDel } from 'idb-keyval'\nimport * as Y from 'yjs'\nimport { STORE_KEY_DIRECTORY_HANDLE } from './constants'\nimport {\n  askReadWritePermissionsIfNeeded,\n  createFile,\n  getFSFileHandle,\n  writeContentToFileIfChanged\n} from './helpers'\nimport { getLastWriteCacheData, setLastWriteCacheData } from './cache'\nimport { getDeltaOperations } from './yjs'\n\nconst useFileSync = () => {\n  const [isSupported, setSupported] = useState(false)\n  const [isWritePermissionGranted, setWritePermissionGranted] = useState(false)\n  const [directoryHandle, setDirectoryHandle] = useState<\n    FileSystemDirectoryHandle | undefined\n  >(undefined)\n\n  useEffect(() => {\n    setSupported(typeof (window as any).showDirectoryPicker === 'function')\n  }, [])\n\n  useEffect(() => {\n    const loadHandle = async () => {\n      const handle = await idbGet(STORE_KEY_DIRECTORY_HANDLE)\n      if (handle) {\n        setDirectoryHandle(handle)\n      }\n    }\n    loadHandle()\n  }, [])\n\n  useEffect(() => {\n    if (directoryHandle) {\n      idbSet(STORE_KEY_DIRECTORY_HANDLE, directoryHandle)\n    }\n  }, [directoryHandle])\n\n  const grantWritePermission = useCallback(async () => {\n    if (!isSupported || !directoryHandle) {\n      return\n    }\n    try {\n      const granted = await askReadWritePermissionsIfNeeded(directoryHandle)\n      setWritePermissionGranted(granted)\n    } catch {}\n  }, [isSupported, directoryHandle])\n\n  const setRootDirectory = useCallback(\n    async (withWritePermission: boolean) => {\n      if (!isSupported) {\n        return\n      }\n      try {\n        const handle = await (window as any).showDirectoryPicker()\n        if (handle) {\n          setDirectoryHandle(handle)\n          if (withWritePermission) {\n            const granted = await askReadWritePermissionsIfNeeded(handle)\n            setWritePermissionGranted(granted)\n          }\n        }\n      } catch {}\n    },\n    [isSupported, grantWritePermission]\n  )\n\n  const unsetRootDirectory = useCallback(async () => {\n    setDirectoryHandle(undefined)\n    idbDel(STORE_KEY_DIRECTORY_HANDLE)\n  }, [])\n\n  const syncDoc = useCallback(\n    async (name: string, doc: Y.Doc) => {\n      if (!directoryHandle) {\n        return\n      }\n\n      const updateFileContent = async (\n        file: globalThis.File,\n        fileHandle: FileSystemFileHandle,\n        newContent: string\n      ) => {\n        // When we write to the file system, we also save a version\n        // in cache in order to be able to watch for subsequent changes\n        // to the file.\n        await writeContentToFileIfChanged(file, fileHandle, newContent)\n        await setLastWriteCacheData(name, newContent, file.lastModified)\n      }\n\n      let fileHandle = await getFSFileHandle(name, directoryHandle)\n      const docContent = doc.getText().toString()\n\n      if (!fileHandle) {\n        // File is not present in the file system, so create it.\n        const newFileHandle = await createFile(directoryHandle, name, '')\n        const newFile = await newFileHandle.getFile()\n        await updateFileContent(newFile, newFileHandle, docContent)\n        return\n      }\n\n      const file = await fileHandle.getFile()\n\n      // File exists, so compare it with the last-write-cache.\n      const lastWriteCacheData = await getLastWriteCacheData(name)\n\n      if (!lastWriteCacheData) {\n        // Cached version does not exist. This should never happen. Indeed,\n        // even if the user clears the app data, the directory handle will\n        // be cleared as well, so the user will be asked to select a directory\n        // again, in which case a hard overwrite will happen, and the\n        // last-write-cache will be populated. So in case `lastWriteCacheData`\n        // does not exist, we can consider this situation as similar to the\n        // initial file dump situation and simply overwrite the FS file.\n        await updateFileContent(file, fileHandle, docContent)\n        return\n      }\n\n      // Cached version exists. This allows us to see the changes in the\n      // local file, and compute the diff which in turn gives us as\n      // state update vector for our CRDT. We can then apply it\n      // to the app file for a seamless merging of the two versions.\n\n      if (file.lastModified === lastWriteCacheData.lastModified) {\n        // File has not changed in the file system. Since the FS file cache\n        // is only set when a project file is synced, this means that the\n        // only option is that the app file has changed, in which\n        // case it should be written to the FS file.\n        await updateFileContent(file, fileHandle, docContent)\n        return\n      }\n\n      // File has changed in the file system.\n\n      const fileContent = await file.text()\n      const lastWriteFileContent = lastWriteCacheData.content\n      const deltas = getDeltaOperations(lastWriteFileContent, fileContent)\n\n      if (deltas.length === 0) {\n        // Same comment as above: no difference between FS file and\n        // and last-write-cache, so just write the app file to FS.\n        await updateFileContent(file, fileHandle, docContent)\n        return\n      }\n\n      // A change has happened in the file, since it differs\n      // from the cached version. So we merge it with the app doc.\n      doc.getText().applyDelta(deltas)\n\n      const mergedContent = doc.getText().toString()\n      await updateFileContent(file, fileHandle, mergedContent)\n    },\n    [directoryHandle]\n  )\n\n  const directoryName = useMemo(() => {\n    return directoryHandle?.name\n  }, [directoryHandle])\n\n  return {\n    isSupported,\n    setRootDirectory,\n    unsetRootDirectory,\n    grantWritePermission,\n    isWritePermissionGranted,\n    directoryName,\n    syncDoc\n  }\n}\n\nexport default useFileSync\n"
  },
  {
    "path": "src/yjs.ts",
    "content": "import * as Diff from 'diff'\n\ntype YDelta = { retain: number } | { delete: number } | { insert: string }\n\n// Compute the set of Yjs delta operations (that is, `insert` and\n// `delete`) required to go from initialText to finalText.\n// Based on https://github.com/kpdecker/jsdiff.\nexport const getDeltaOperations = (\n  initialText: string,\n  finalText: string\n): YDelta[] => {\n  if (initialText === finalText) {\n    return []\n  }\n\n  const edits = Diff.diffChars(initialText, finalText)\n  let prevOffset = 0\n  let deltas: YDelta[] = []\n\n  for (const edit of edits) {\n    if (edit.removed && edit.value) {\n      deltas = [\n        ...deltas,\n        ...[\n          ...(prevOffset > 0 ? [{ retain: prevOffset }] : []),\n          { delete: edit.value.length }\n        ]\n      ]\n      prevOffset = 0\n    } else if (edit.added && edit.value) {\n      deltas = [...deltas, ...[{ retain: prevOffset }, { insert: edit.value }]]\n      prevOffset = edit.value.length\n    } else {\n      prevOffset = edit.value.length\n    }\n  }\n  return deltas\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ES2020\",\n    \"moduleResolution\": \"node\",\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"target\": \"ES2020\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  }
]