working nix builds

can compile both the react application statically so it can be served by
nginx
and then builds a docker container with nginx for easy deployment to
something like fly.io
I mean since it's static I could probably host it for free on a website
like netlify or something, but I still wanted a docker container for
now.

Notably I did have to move the generated javascript parser into the
mast-react-vite folder so that `buildNpmPackage` was scoped to the
mast-react-vite folder. I'm not sure how to get it working in a truly
monorepo style build working for it yet, but I'm not exactly inclined to
make it super shiny right now either.
This commit is contained in:
Anish Lakhwara 2024-11-15 22:46:53 -08:00
parent f24e56159a
commit 0ca396148b
8 changed files with 1792 additions and 40 deletions

114
flake.nix
View file

@ -9,20 +9,108 @@
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
in username = "admin";
{ passwordHash =
devShells.default = pkgs.mkShell { "$apr1$bhZ6EAAr$E8cv5p/2RUBrxLZD.9Jpi."; # Replace with your hash
buildInputs = with pkgs; [
go
pigeon
nodejs
sqlite
];
shellHook = '' nginxConfig = pkgs.writeText "nginx.conf" ''
echo "System: ${system}" worker_processes auto;
daemon off;
events {
worker_connections 1024;
}
http {
include ${pkgs.nginx}/conf/mime.types;
server {
listen 8080;
location / {
auth_basic "Restricted Content";
auth_basic_user_file /etc/nginx/htpasswd;
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
}
}
'';
htpasswd = pkgs.writeText "htpasswd" ''
${username}:${passwordHash}
'';
in {
packages = rec {
react = pkgs.buildNpmPackage {
pname = "mast-react";
version = "0.0.1";
src = ./mast-react-vite;
npmDepsHash = "sha256-oqq55MEzXGEiF9UW7rZFyQrkyTPrUEkh2enZmbbZ7Ks=";
buildInputs = with pkgs; [ nodejs typescript ];
nativeBuildInputs = with pkgs; [ nodejs ];
# npmWorkspace = ./mast-react-vite;
npmBuildScript = "build";
installPhase = ''
mkdir -p $out
cp -r dist $out/
''; '';
}; };
} react-server = pkgs.dockerTools.buildImage {
); name = "react-server";
tag = "latest";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = [
pkgs.nginx
react
(pkgs.writeScriptBin "start-server" ''
#!/usr/bin/env bash
# Create necessary directories
mkdir -p /etc/nginx
cp ${htpasswd} /etc/nginx/htpasswd
# Create app directory and copy built files
mkdir -p /app
cp -r ${react}/* /app/
# Start nginx
${pkgs.nginx}/bin/nginx -c ${nginxConfig}
'')
];
pathsToLink = [ "/bin" ];
};
config = {
Cmd = [ "/bin/start-server" ];
ExposedPorts = { "8080/tcp" = { }; };
};
};
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ go pigeon nodejs sqlite git-bug openssl ];
shellHook = ''
alias start-dev='npm run dev'
alias build-dev='npm run build'
alias clean-dev='rm -rf dist node_modules'
alias ws-server='go run ./server/main.go'
alias gen-pass='openssl passwd -apr1 '
echo "System: ${system}"
echo "Available commands:"
echo " start-dev - Start Vite dev server"
echo " build-dev - Build the application"
echo " clean-dev - Clean build artifacts"
echo " ws-server - Run the websocket server"
echo " gen-pass - Generate a basic_auth password"
'';
};
});
} }

View file

@ -8,10 +8,12 @@
"name": "mast-react", "name": "mast-react",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@vlcn.io/react": "^3.1.0", "@vlcn.io/react": "^3.1.0",
"@vlcn.io/rx-tbl": "^0.15.0", "@vlcn.io/rx-tbl": "^0.15.0",
"@vlcn.io/ws-browserdb": "^0.2.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
@ -692,6 +694,68 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@radix-ui/primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz",
"integrity": "sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-icons": { "node_modules/@radix-ui/react-icons": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
@ -700,6 +764,144 @@
"react": "^16.x || ^17.x || ^18.x" "react": "^16.x || ^17.x || ^18.x"
} }
}, },
"node_modules/@radix-ui/react-presence": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"dependencies": {
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
"integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.2", "version": "4.24.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz",
@ -1203,13 +1405,13 @@
"version": "15.7.13", "version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true "devOptional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.12", "version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -1219,7 +1421,7 @@
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
} }
@ -1503,6 +1705,19 @@
"resolved": "https://registry.npmjs.org/@vlcn.io/wa-sqlite/-/wa-sqlite-0.22.0.tgz", "resolved": "https://registry.npmjs.org/@vlcn.io/wa-sqlite/-/wa-sqlite-0.22.0.tgz",
"integrity": "sha512-OujKro0mAqP7/efUeCGB6zBiyMoSCFVe7jQKPF0n47U9ZhOaIW3kQUVCwF+CmzvzQfN1Vl4PrFQRNNxlSwTCNQ==" "integrity": "sha512-OujKro0mAqP7/efUeCGB6zBiyMoSCFVe7jQKPF0n47U9ZhOaIW3kQUVCwF+CmzvzQfN1Vl4PrFQRNNxlSwTCNQ=="
}, },
"node_modules/@vlcn.io/ws-browserdb": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@vlcn.io/ws-browserdb/-/ws-browserdb-0.2.0.tgz",
"integrity": "sha512-TKPygQomMPurQ8mwAwEefN5BROw30FagL21S3FzvUunyCybOByysAEoV/OrJ7xNCvYXN9yjg0qyf7iZZfeOsYg==",
"dependencies": {
"@types/throttle-debounce": "^5.0.0",
"@vlcn.io/crsqlite-wasm": "0.16.0",
"@vlcn.io/rx-tbl": "0.15.0",
"@vlcn.io/ws-client": "0.2.0",
"@vlcn.io/ws-common": "0.2.0",
"@vlcn.io/xplat-api": "0.15.0"
}
},
"node_modules/@vlcn.io/ws-client": { "node_modules/@vlcn.io/ws-client": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@vlcn.io/ws-client/-/ws-client-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@vlcn.io/ws-client/-/ws-client-0.2.0.tgz",
@ -1909,7 +2124,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true "devOptional": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.7", "version": "4.3.7",

View file

@ -5,15 +5,17 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@vlcn.io/react": "^3.1.0", "@vlcn.io/react": "^3.1.0",
"@vlcn.io/rx-tbl": "^0.15.0", "@vlcn.io/rx-tbl": "^0.15.0",
"@vlcn.io/ws-browserdb": "^0.2.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",

View file

@ -1,18 +1,20 @@
import { useState } from 'react' import { useState } from 'react'
import { useCachedState, useQuery } from "@vlcn.io/react"; import { useQuery } from "@vlcn.io/react";
import { import {
ColumnDef, ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { DataTable } from "@/components/ui/data-table" import { DataTable } from "@/components/ui/data-table"
import * as addParser from '../../parser/add_parser.js'; import * as commandParser from '@/lib/command_parser.js';
import * as commandParser from '../../parser/command_parser.js';
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
function Marshall(parsed) { interface MarshalledCommand {
description: string;
tags: string;
project: string;
}
function Marshall(parsed): MarshalledCommand {
let project = ""; let project = "";
const tags = []; const tags = [];
for (const attr of parsed.attributes) { for (const attr of parsed.attributes) {
@ -171,6 +173,10 @@ function App({ ctx }) {
} }
} }
// React to key presses for selection // React to key presses for selection
// TODO:
// We should use the react way
// Where we update based on changes to newText
// Rather than changes to the input value
else if (e.target.value.trim() !== "") { else if (e.target.value.trim() !== "") {
try { try {
const parsed = commandParser.parse(e.target.value); const parsed = commandParser.parse(e.target.value);

View file

@ -0,0 +1,78 @@
import App from "./App.tsx";
import schemaContent from "./schemas/main2.sql?raw";
import { DBProvider } from "@vlcn.io/react";
import { useEffect, useState } from "react";
/**
* Generates a random room name to sync with or pulls one from local storage.
*/
function getRoom(hash: HashBag): string {
return hash.room || localStorage.getItem("room") || newRoom();
}
function hashChanged() {
const hash = parseHash();
const room = getRoom(hash);
if (room != hash.room) {
hash.room = room;
window.location.hash = writeHash(hash);
}
localStorage.setItem("room", room);
return room;
}
const room = hashChanged();
export default function Root() {
const [theRoom, setTheRoom] = useState(room);
useEffect(() => {
const cb = () => {
const room = hashChanged();
if (room != theRoom) {
setTheRoom(room);
}
};
addEventListener("hashchange", cb);
return () => {
removeEventListener("hashchange", cb);
};
}, []); // ignore -- theRoom is managed by the effect
return (
<DBProvider
dbname={theRoom}
schema={{
name: "main2.sql",
content: schemaContent,
}}
Render={() => <App dbname={theRoom} />}
></DBProvider>
);
}
type HashBag = { [key: string]: string };
function parseHash(): HashBag {
const hash = window.location.hash;
const ret: { [key: string]: string } = {};
if (hash.length > 1) {
const substr = hash.substring(1);
const parts = substr.split(",");
for (const part of parts) {
const [key, value] = part.split("=");
ret[key] = value;
}
}
return ret;
}
function writeHash(hash: HashBag) {
const parts = [];
for (const key in hash) {
parts.push(`${key}=${hash[key]}`);
}
return parts.join(",");
}
function newRoom() {
return crypto.randomUUID().replaceAll("-", "");
}

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,16 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from "./App.tsx"; import App from "./App.tsx";
import Root from "./Root.tsx";
import { start } from "@vlcn.io/ws-client/worker.js";
import { createDbProvider } from "@vlcn.io/ws-browserdb";
import initWasm from '@vlcn.io/crsqlite-wasm'; import initWasm from '@vlcn.io/crsqlite-wasm';
import wasmUrl from '@vlcn.io/crsqlite-wasm/crsqlite.wasm?url'; import wasmUrl from '@vlcn.io/crsqlite-wasm/crsqlite.wasm?url';
import tblrx from '@vlcn.io/rx-tbl'; import tblrx from '@vlcn.io/rx-tbl';
async function initializeDatabase() {
const sqlite = await initWasm(() => wasmUrl); const sqlite = await initWasm(() => wasmUrl);
const db = await sqlite.open("todo.db"); const db = await sqlite.open("todo.db");
await db.exec(` await db.exec(`
CREATE TABLE IF NOT EXISTS todos ( CREATE TABLE IF NOT EXISTS todos (
@ -24,9 +27,64 @@ await db.exec(`
); );
SELECT crsql_as_crr('todos'); SELECT crsql_as_crr('todos');
`); `);
db.onUpdate(async () => {
const changes = await db.execA(`SELECT * FROM crsql_changes WHERE db_version > 0 AND site_id = crsql_site_id()`);
sendChangesToServer(changes);
});
return db;
}
const config = {
dbProvider: createDbProvider(),
transportProvider: (dbName) => new WebSocket(`ws://localhost:8080/sync/${dbName}`),
};
start(config);
const ws = new WebSocket('ws://localhost:8080/sync');
ws.onmessage = async function(event) {
const { type, data } = JSON.parse(event.data);
if (type === 'changes') {
await applyChanges(data);
}
};
function sendChangesToServer(changes) {
const formattedChanges = changes.map(change => ({
TableName: change[0],
PK: Array.isArray(change[1]) ? btoa(String.fromCharCode.apply(null, change[1])) : btoa(change[1]),
ColumnName: change[2],
Value: Array.isArray(change[3]) ? btoa(String.fromCharCode.apply(null, change[3])) : change[3],
ColVersion: Number(change[4]),
DBVersion: Number(change[5]),
SiteID: Array.isArray(change[6]) ? btoa(new TextDecoder('utf8').decode(change[6])) : btoa(change[6]),
CL: Number(change[7]),
Seq: Number(change[8])
}));
console.log(formattedChanges)
const message = {
type: "changes",
data: formattedChanges
};
ws.send(JSON.stringify(message));
}
async function applyChanges(changes) {
const db = initializeDatabase();
const rx = tblrx(db); const rx = tblrx(db);
const ctx = { db, rx }; const ctx = { db, rx };
await db.tx(async (tx) => {
for (const change of changes) {
await tx.exec(
`INSERT INTO crsql_changes VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
change
);
}
});
}
document.getElementById("root")!.classList.add("dark"); document.getElementById("root")!.classList.add("dark");

View file

@ -9,6 +9,7 @@
} }
], ],
"compilerOptions": { "compilerOptions": {
"noEmitOnError": false,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": [ "@/*": [