diff --git a/app.config.ts b/app.config.ts
index 5de386a..01ecda3 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -1,12 +1,21 @@
import { defineConfig } from "@solidjs/start/config";
import unocss from "unocss/vite";
+import { client, router } from "./socket";
-export default defineConfig({
+const app = defineConfig({
+ ssr: false,
server: {
preset: "netlify",
+ experimental: {
+ websocket: true,
+ },
},
vite: {
- plugins: [unocss()],
+ plugins: [unocss(), client()],
ssr: { external: ["@prisma/client"] },
},
});
+
+app.addRouter(router);
+
+export default app;
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..7af5848
Binary files /dev/null and b/bun.lockb differ
diff --git a/package.json b/package.json
index 9023bd3..6b79871 100644
--- a/package.json
+++ b/package.json
@@ -19,23 +19,31 @@
},
"dependencies": {
"@formkit/auto-animate": "^0.8.1",
+ "@kobalte/core": "^0.13.7",
"@libsql/client": "^0.6.1",
"@prisma/adapter-libsql": "^5.14.0",
"@prisma/client": "^5.7.0",
+ "@solid-primitives/memo": "^1.3.10",
+ "@solid-primitives/mouse": "^2.0.20",
+ "@solid-primitives/rootless": "^1.4.5",
+ "@solid-primitives/scheduled": "^1.4.4",
"@solidjs/meta": "^0.29.3",
"@solidjs/router": "^0.13.3",
"@solidjs/start": "^1.0.1",
"@unocss/reset": "^0.58.5",
+ "@vinxi/plugin-directives": "^0.4.3",
"better-sqlite3": "^9.4.3",
"immer": "^10.0.4",
"kysely": "^0.27.3",
"lowdb": "^7.0.1",
"prisma": "^5.7.0",
+ "rxjs-for-await": "^1.0.0",
"solid-icons": "^1.1.0",
"solid-js": "^1.8.17",
"unimport": "^3.7.1",
+ "unique-names-generator": "^4.7.1",
"unocss-preset-theme": "^0.12.0",
- "vinxi": "^0.3.11"
+ "vinxi": "^0.3.14"
},
"engines": {
"node": ">=18"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f39a50e..6e0daf4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,10 +28,13 @@ importers:
version: 0.13.6(solid-js@1.8.21)
'@solidjs/start':
specifier: ^1.0.1
- version: 1.0.6(solid-js@1.8.21)(vinxi@0.3.14)(vite@5.4.2)
+ version: 1.0.6(rollup@4.21.0)(solid-js@1.8.21)(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
'@unocss/reset':
specifier: ^0.58.5
version: 0.58.9
+ '@vinxi/plugin-directives':
+ specifier: ^0.4.3
+ version: 0.4.3(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
better-sqlite3:
specifier: ^9.4.3
version: 9.6.0
@@ -47,6 +50,9 @@ importers:
prisma:
specifier: ^5.7.0
version: 5.18.0
+ rxjs-for-await:
+ specifier: ^1.0.0
+ version: 1.0.0(rxjs@7.8.1)
solid-icons:
specifier: ^1.1.0
version: 1.1.0(solid-js@1.8.21)
@@ -60,8 +66,8 @@ importers:
specifier: ^0.12.0
version: 0.12.0(@unocss/core@0.62.2)
vinxi:
- specifier: ^0.3.11
- version: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)
+ specifier: ^0.3.14
+ version: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)
devDependencies:
'@tailwindcss/forms':
specifier: ^0.5.7
@@ -74,7 +80,7 @@ importers:
version: 20.16.1
'@unscatty/unocss-preset-daisy':
specifier: ^1.0.0
- version: 1.0.0(unocss@0.58.9)
+ version: 1.0.0(unocss@0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)))
autoprefixer:
specifier: ^10.4.16
version: 10.4.20(postcss@8.4.41)
@@ -86,7 +92,7 @@ importers:
version: 3.4.10
unocss:
specifier: ^0.58.5
- version: 0.58.9(postcss@8.4.41)(vite@5.4.2)
+ version: 0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
packages:
@@ -1111,6 +1117,11 @@ packages:
peerDependencies:
vinxi: ^0.4.0
+ '@vinxi/plugin-directives@0.4.3':
+ resolution: {integrity: sha512-Ey+TRIwyk8871PKhQel8NyZ9B6N0Tvhjo1QIttTyrV0d7BfUpri5GyGygmBY7fHClSE/vqaNCCZIKpTL3NJAEg==}
+ peerDependencies:
+ vinxi: ^0.4.3
+
'@vinxi/server-components@0.4.1':
resolution: {integrity: sha512-rMS+RCGr1tujO1xWgILMLpOWIyw2OwDO46EtkuhTfqaVgLLt/w7+hxzOnh4s3O9sXoKKuUswtj9/MpQQkFoMOQ==}
peerDependencies:
@@ -2056,7 +2067,6 @@ packages:
libsql@0.3.19:
resolution: {integrity: sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA==}
- cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lilconfig@2.1.0:
@@ -2580,6 +2590,14 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ rxjs-for-await@1.0.0:
+ resolution: {integrity: sha512-MJhvf1vtQaljd5wlzsasvOjcohVogzkHkUI0gFE9nGhZ15/fT2vR1CjkLEh37oRqWwpv11vHo5D+sLM+Aw9Y8g==}
+ peerDependencies:
+ rxjs: ^7.0.0
+
+ rxjs@7.8.1:
+ resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
+
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -3742,7 +3760,7 @@ snapshots:
async-mutex: 0.5.0
'@prisma/client@5.18.0(prisma@5.18.0)':
- dependencies:
+ optionalDependencies:
prisma: 5.18.0
'@prisma/debug@5.18.0': {}
@@ -3772,8 +3790,9 @@ snapshots:
'@rollup/plugin-alias@5.1.0(rollup@4.21.0)':
dependencies:
- rollup: 4.21.0
slash: 4.0.0
+ optionalDependencies:
+ rollup: 4.21.0
'@rollup/plugin-commonjs@25.0.8(rollup@4.21.0)':
dependencies:
@@ -3783,6 +3802,7 @@ snapshots:
glob: 8.1.0
is-reference: 1.2.1
magic-string: 0.30.11
+ optionalDependencies:
rollup: 4.21.0
'@rollup/plugin-inject@5.0.5(rollup@4.21.0)':
@@ -3790,11 +3810,13 @@ snapshots:
'@rollup/pluginutils': 5.1.0(rollup@4.21.0)
estree-walker: 2.0.2
magic-string: 0.30.11
+ optionalDependencies:
rollup: 4.21.0
'@rollup/plugin-json@6.1.0(rollup@4.21.0)':
dependencies:
'@rollup/pluginutils': 5.1.0(rollup@4.21.0)
+ optionalDependencies:
rollup: 4.21.0
'@rollup/plugin-node-resolve@15.2.3(rollup@4.21.0)':
@@ -3805,20 +3827,23 @@ snapshots:
is-builtin-module: 3.2.1
is-module: 1.0.0
resolve: 1.22.8
+ optionalDependencies:
rollup: 4.21.0
'@rollup/plugin-replace@5.0.7(rollup@4.21.0)':
dependencies:
'@rollup/pluginutils': 5.1.0(rollup@4.21.0)
magic-string: 0.30.11
+ optionalDependencies:
rollup: 4.21.0
'@rollup/plugin-terser@0.4.4(rollup@4.21.0)':
dependencies:
- rollup: 4.21.0
serialize-javascript: 6.0.2
smob: 1.5.0
terser: 5.31.6
+ optionalDependencies:
+ rollup: 4.21.0
'@rollup/pluginutils@4.2.1':
dependencies:
@@ -3830,6 +3855,7 @@ snapshots:
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
+ optionalDependencies:
rollup: 4.21.0
'@rollup/rollup-android-arm-eabi@4.21.0':
@@ -3890,11 +3916,11 @@ snapshots:
dependencies:
solid-js: 1.8.21
- '@solidjs/start@1.0.6(solid-js@1.8.21)(vinxi@0.3.14)(vite@5.4.2)':
+ '@solidjs/start@1.0.6(rollup@4.21.0)(solid-js@1.8.21)(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))':
dependencies:
- '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14)
- '@vinxi/server-components': 0.4.1(vinxi@0.3.14)
- '@vinxi/server-functions': 0.4.1(vinxi@0.3.14)
+ '@vinxi/plugin-directives': 0.4.3(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
+ '@vinxi/server-components': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
+ '@vinxi/server-functions': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
defu: 6.1.4
error-stack-parser: 2.1.4
glob: 10.4.5
@@ -3905,8 +3931,8 @@ snapshots:
shikiji: 0.9.19
source-map-js: 1.2.0
terracotta: 1.0.5(solid-js@1.8.21)
- vite-plugin-inspect: 0.7.42(vite@5.4.2)
- vite-plugin-solid: 2.10.2(solid-js@1.8.21)(vite@5.4.2)
+ vite-plugin-inspect: 0.7.42(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
+ vite-plugin-solid: 2.10.2(solid-js@1.8.21)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
transitivePeerDependencies:
- '@nuxt/kit'
- '@testing-library/jest-dom'
@@ -3973,16 +3999,17 @@ snapshots:
dependencies:
'@types/node': 20.16.1
- '@unocss/astro@0.58.9(vite@5.4.2)':
+ '@unocss/astro@0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))':
dependencies:
'@unocss/core': 0.58.9
'@unocss/reset': 0.58.9
- '@unocss/vite': 0.58.9(vite@5.4.2)
- vite: 5.4.2(@types/node@20.16.1)
+ '@unocss/vite': 0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
+ optionalDependencies:
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
transitivePeerDependencies:
- rollup
- '@unocss/cli@0.58.9':
+ '@unocss/cli@0.58.9(rollup@4.21.0)':
dependencies:
'@ampproject/remapping': 2.3.0
'@rollup/pluginutils': 5.1.0(rollup@4.21.0)
@@ -4118,7 +4145,7 @@ snapshots:
dependencies:
'@unocss/core': 0.58.9
- '@unocss/vite@0.58.9(vite@5.4.2)':
+ '@unocss/vite@0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))':
dependencies:
'@ampproject/remapping': 2.3.0
'@rollup/pluginutils': 5.1.0(rollup@4.21.0)
@@ -4130,11 +4157,11 @@ snapshots:
chokidar: 3.6.0
fast-glob: 3.3.2
magic-string: 0.30.11
- vite: 5.4.2(@types/node@20.16.1)
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
transitivePeerDependencies:
- rollup
- '@unscatty/unocss-preset-daisy@1.0.0(unocss@0.58.9)':
+ '@unscatty/unocss-preset-daisy@1.0.0(unocss@0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)))':
dependencies:
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e(postcss@8.4.41)
'@unocss/rule-utils': 0.57.7
@@ -4143,7 +4170,7 @@ snapshots:
parsel-js: 1.1.2
postcss: 8.4.41
postcss-js: 4.0.1(postcss@8.4.41)
- unocss: 0.58.9(postcss@8.4.41)(vite@5.4.2)
+ unocss: 0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
'@vercel/nft@0.26.5':
dependencies:
@@ -4185,7 +4212,7 @@ snapshots:
transitivePeerDependencies:
- uWebSockets.js
- '@vinxi/plugin-directives@0.4.1(vinxi@0.3.14)':
+ '@vinxi/plugin-directives@0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))':
dependencies:
'@babel/parser': 7.25.3
acorn: 8.12.1
@@ -4196,29 +4223,42 @@ snapshots:
magicast: 0.2.11
recast: 0.23.9
tslib: 2.6.3
- vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)
+ vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)
- '@vinxi/server-components@0.4.1(vinxi@0.3.14)':
+ '@vinxi/plugin-directives@0.4.3(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))':
dependencies:
- '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14)
+ '@babel/parser': 7.25.3
acorn: 8.12.1
+ acorn-jsx: 5.3.2(acorn@8.12.1)
acorn-loose: 8.4.0
acorn-typescript: 1.4.13(acorn@8.12.1)
astring: 1.8.6
magicast: 0.2.11
recast: 0.23.9
- vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)
+ tslib: 2.6.3
+ vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)
- '@vinxi/server-functions@0.4.1(vinxi@0.3.14)':
+ '@vinxi/server-components@0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))':
dependencies:
- '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14)
+ '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
acorn: 8.12.1
acorn-loose: 8.4.0
acorn-typescript: 1.4.13(acorn@8.12.1)
astring: 1.8.6
magicast: 0.2.11
recast: 0.23.9
- vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)
+ vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)
+
+ '@vinxi/server-functions@0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))':
+ dependencies:
+ '@vinxi/plugin-directives': 0.4.1(vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6))
+ acorn: 8.12.1
+ acorn-loose: 8.4.0
+ acorn-typescript: 1.4.13(acorn@8.12.1)
+ astring: 1.8.6
+ magicast: 0.2.11
+ recast: 0.23.9
+ vinxi: 0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6)
abbrev@1.1.1: {}
@@ -4582,7 +4622,7 @@ snapshots:
undici-types: 5.28.4
db0@0.1.4(@libsql/client@0.6.2)(better-sqlite3@9.6.0):
- dependencies:
+ optionalDependencies:
'@libsql/client': 0.6.2
better-sqlite3: 9.6.0
@@ -5562,8 +5602,9 @@ snapshots:
postcss-load-config@4.0.2(postcss@8.4.41):
dependencies:
lilconfig: 3.1.2
- postcss: 8.4.41
yaml: 2.5.0
+ optionalDependencies:
+ postcss: 8.4.41
postcss-nested@5.0.6(postcss@8.4.41):
dependencies:
@@ -5714,9 +5755,10 @@ snapshots:
dependencies:
open: 8.4.2
picomatch: 2.3.1
- rollup: 4.21.0
source-map: 0.7.4
yargs: 17.7.2
+ optionalDependencies:
+ rollup: 4.21.0
rollup@4.21.0:
dependencies:
@@ -5748,6 +5790,14 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
+ rxjs-for-await@1.0.0(rxjs@7.8.1):
+ dependencies:
+ rxjs: 7.8.1
+
+ rxjs@7.8.1:
+ dependencies:
+ tslib: 2.6.3
+
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
@@ -6120,10 +6170,10 @@ snapshots:
'@unocss/core': 0.62.2
'@unocss/rule-utils': 0.58.9
- unocss@0.58.9(postcss@8.4.41)(vite@5.4.2):
+ unocss@0.58.9(postcss@8.4.41)(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)):
dependencies:
- '@unocss/astro': 0.58.9(vite@5.4.2)
- '@unocss/cli': 0.58.9
+ '@unocss/astro': 0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
+ '@unocss/cli': 0.58.9(rollup@4.21.0)
'@unocss/core': 0.58.9
'@unocss/extractor-arbitrary-variants': 0.58.9
'@unocss/postcss': 0.58.9(postcss@8.4.41)
@@ -6141,8 +6191,9 @@ snapshots:
'@unocss/transformer-compile-class': 0.58.9
'@unocss/transformer-directives': 0.58.9
'@unocss/transformer-variant-group': 0.58.9
- '@unocss/vite': 0.58.9(vite@5.4.2)
- vite: 5.4.2(@types/node@20.16.1)
+ '@unocss/vite': 0.58.9(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
+ optionalDependencies:
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
transitivePeerDependencies:
- postcss
- rollup
@@ -6160,14 +6211,15 @@ snapshots:
anymatch: 3.1.3
chokidar: 3.6.0
destr: 2.0.3
- h3: 1.11.1
- ioredis: 5.4.1
+ h3: 1.12.0
listhen: 1.7.2
lru-cache: 10.4.3
mri: 1.2.0
node-fetch-native: 1.6.4
ofetch: 1.3.4
ufo: 1.5.4
+ optionalDependencies:
+ ioredis: 5.4.1
transitivePeerDependencies:
- uWebSockets.js
@@ -6202,7 +6254,7 @@ snapshots:
validate-html-nesting@1.2.2: {}
- vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0):
+ vinxi@0.3.14(@libsql/client@0.6.2)(@types/node@20.16.1)(better-sqlite3@9.6.0)(ioredis@5.4.1)(terser@5.31.6):
dependencies:
'@babel/core': 7.25.2
'@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2)
@@ -6236,7 +6288,7 @@ snapshots:
unctx: 2.3.1
unenv: 1.10.0
unstorage: 1.10.2(ioredis@5.4.1)
- vite: 5.4.2(@types/node@20.16.1)
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
zod: 3.23.8
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -6270,7 +6322,7 @@ snapshots:
- uWebSockets.js
- xml2js
- vite-plugin-inspect@0.7.42(vite@5.4.2):
+ vite-plugin-inspect@0.7.42(rollup@4.21.0)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)):
dependencies:
'@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.1.0(rollup@4.21.0)
@@ -6280,12 +6332,12 @@ snapshots:
open: 9.1.0
picocolors: 1.0.1
sirv: 2.0.4
- vite: 5.4.2(@types/node@20.16.1)
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
transitivePeerDependencies:
- rollup
- supports-color
- vite-plugin-solid@2.10.2(solid-js@1.8.21)(vite@5.4.2):
+ vite-plugin-solid@2.10.2(solid-js@1.8.21)(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)):
dependencies:
'@babel/core': 7.25.2
'@types/babel__core': 7.20.5
@@ -6293,23 +6345,24 @@ snapshots:
merge-anything: 5.1.7
solid-js: 1.8.21
solid-refresh: 0.6.3(solid-js@1.8.21)
- vite: 5.4.2(@types/node@20.16.1)
- vitefu: 0.2.5(vite@5.4.2)
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
+ vitefu: 0.2.5(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6))
transitivePeerDependencies:
- supports-color
- vite@5.4.2(@types/node@20.16.1):
+ vite@5.4.2(@types/node@20.16.1)(terser@5.31.6):
dependencies:
- '@types/node': 20.16.1
esbuild: 0.21.5
postcss: 8.4.41
rollup: 4.21.0
optionalDependencies:
+ '@types/node': 20.16.1
fsevents: 2.3.3
+ terser: 5.31.6
- vitefu@0.2.5(vite@5.4.2):
- dependencies:
- vite: 5.4.2(@types/node@20.16.1)
+ vitefu@0.2.5(vite@5.4.2(@types/node@20.16.1)(terser@5.31.6)):
+ optionalDependencies:
+ vite: 5.4.2(@types/node@20.16.1)(terser@5.31.6)
web-streams-polyfill@3.3.3: {}
diff --git a/prisma/dev.db b/prisma/dev.db
index 123a99f..5ff34e9 100644
Binary files a/prisma/dev.db and b/prisma/dev.db differ
diff --git a/socket/imports/compiler/babel.ts b/socket/imports/compiler/babel.ts
new file mode 100644
index 0000000..1d3736b
--- /dev/null
+++ b/socket/imports/compiler/babel.ts
@@ -0,0 +1,87 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as babel from "@babel/core";
+import { ImportPluginOptions } from ".";
+
+const specificImports = [
+ "createMemo",
+ "createRoot",
+ "createSignal",
+ "createEffect",
+ "from",
+ "observable",
+ "untrack",
+ "onCleanup",
+];
+
+export function createTransform$(opts?: ImportPluginOptions) {
+ return function transform$({
+ types: t,
+ template: temp,
+ }: {
+ types: typeof babel.types;
+ template: typeof babel.template;
+ }): babel.PluginObj {
+ return {
+ visitor: {
+ ImportDeclaration(path) {
+ if (path.node.source.value === "solid-js") {
+ const specificSpecifiers = path.node.specifiers.filter(
+ (specifier) =>
+ t.isImportSpecifier(specifier) &&
+ specificImports.includes((specifier.imported as any).name)
+ );
+ const otherSpecifiers = path.node.specifiers.filter(
+ (specifier) =>
+ t.isImportSpecifier(specifier) &&
+ !specificImports.includes((specifier.imported as any).name)
+ );
+ if (specificSpecifiers.length > 0) {
+ const newImportDeclaration = t.importDeclaration(
+ specificSpecifiers,
+ t.stringLiteral("solid-js/dist/solid")
+ );
+ path.insertAfter(newImportDeclaration);
+ if (otherSpecifiers.length > 0) {
+ path.node.specifiers = otherSpecifiers;
+ } else {
+ path.remove();
+ }
+ }
+ }
+ },
+ },
+ };
+ };
+}
+
+export async function compilepImports(
+ code: string,
+ id: string,
+ opts?: ImportPluginOptions
+) {
+ try {
+ const plugins: babel.ParserOptions["plugins"] = ["typescript", "jsx"];
+ const transform$ = createTransform$(opts);
+ const transformed = await babel.transformAsync(code, {
+ presets: [["@babel/preset-typescript"], ...(opts?.babel?.presets ?? [])],
+ parserOpts: {
+ plugins,
+ },
+ plugins: [[transform$], ...(opts?.babel?.plugins ?? [])],
+ filename: id,
+ });
+ if (transformed) {
+ if (opts?.log) {
+ console.log(id, transformed.code);
+ }
+ return {
+ code: transformed.code ?? "",
+ map: transformed.map,
+ };
+ }
+ return null;
+ } catch (e) {
+ console.error("err$$", e);
+ return null;
+ }
+}
diff --git a/socket/imports/compiler/index.ts b/socket/imports/compiler/index.ts
new file mode 100644
index 0000000..5db11b6
--- /dev/null
+++ b/socket/imports/compiler/index.ts
@@ -0,0 +1,15 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type babel from "@babel/core";
+import { type FilterPattern } from "@rollup/pluginutils";
+export { compilepImports } from "./babel";
+
+export type ImportPluginOptions = {
+ babel?: babel.TransformOptions;
+ filter?: {
+ include?: FilterPattern;
+ exclude?: FilterPattern;
+ };
+ log?: boolean;
+};
+
+export * from "./babel";
diff --git a/socket/imports/index.ts b/socket/imports/index.ts
new file mode 100644
index 0000000..8c205b3
--- /dev/null
+++ b/socket/imports/index.ts
@@ -0,0 +1,29 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { Plugin } from "vite";
+import { compilepImports, type ImportPluginOptions } from "./compiler";
+import { repushPlugin, getFilter } from "./utils";
+
+export function importsPlugin(opts?: ImportPluginOptions): Plugin {
+ const filter = getFilter(opts?.filter);
+ const plugin: Plugin = {
+ enforce: "pre",
+ name: "imports",
+ async transform(code, id) {
+ if (!filter(id)) {
+ return code;
+ }
+ if (id.endsWith(".ts") || id.endsWith(".tsx")) {
+ return await compilepImports(code, id, opts);
+ }
+ return undefined;
+ },
+ configResolved(config) {
+ repushPlugin(config.plugins as Plugin[], plugin, [
+ "vite-server-references",
+ "solid",
+ "vinxi:routes",
+ ]);
+ },
+ };
+ return plugin;
+}
diff --git a/socket/imports/utils.ts b/socket/imports/utils.ts
new file mode 100644
index 0000000..495a017
--- /dev/null
+++ b/socket/imports/utils.ts
@@ -0,0 +1,54 @@
+import { createFilter, FilterPattern } from "@rollup/pluginutils";
+import { Plugin } from "vite";
+
+export const DEFAULT_INCLUDE = "{src,socket}/**/*.{jsx,tsx,ts,js,mjs,cjs}";
+export const DEFAULT_EXCLUDE = "node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}";
+
+export function getFileName(_filename: string): string {
+ if (_filename.includes("?")) {
+ // might be useful for the future
+ const [actualId] = _filename.split("?");
+ return actualId;
+ }
+ return _filename;
+}
+
+export const getFilter = (f?: {
+ include?: FilterPattern;
+ exclude?: FilterPattern;
+}) => {
+ const filter = createFilter(
+ f?.include ?? DEFAULT_INCLUDE,
+ f?.exclude ?? DEFAULT_EXCLUDE
+ );
+ return (id: string) => {
+ const actualName = getFileName(id);
+ return filter(actualName);
+ };
+};
+
+// From: https://github.com/bluwy/whyframe/blob/master/packages/jsx/src/index.js#L27-L37
+export function repushPlugin(
+ plugins: Plugin[],
+ plugin: Plugin | string,
+ pluginNames: string[]
+): void {
+ const namesSet = new Set(pluginNames);
+ const name = typeof plugin === "string" ? plugin : plugin.name;
+ const currentPlugin = plugins.find((e) => e.name === name)!;
+ let baseIndex = -1;
+ let targetIndex = -1;
+ for (let i = 0, len = plugins.length; i < len; i += 1) {
+ const current = plugins[i];
+ if (namesSet.has(current.name) && baseIndex === -1) {
+ baseIndex = i;
+ }
+ if (current.name === name) {
+ targetIndex = i;
+ }
+ }
+ if (baseIndex !== -1 && targetIndex !== -1 && baseIndex < targetIndex) {
+ plugins.splice(targetIndex, 1);
+ plugins.splice(baseIndex, 0, currentPlugin);
+ }
+}
diff --git a/socket/index.ts b/socket/index.ts
new file mode 100644
index 0000000..bdb789e
--- /dev/null
+++ b/socket/index.ts
@@ -0,0 +1,23 @@
+import { normalize } from "vinxi/lib/path";
+export { client } from "./plugin/client";
+import { server } from "./plugin/server";
+import { fileURLToPath } from "url";
+import { importsPlugin } from "./imports";
+
+export const router = {
+ name: "socket-fns",
+ type: "http",
+ base: "/_ws",
+ handler: "./socket/plugin/server-handler.ts",
+ target: "server",
+ plugins: () => [
+ server({
+ runtime: normalize(
+ fileURLToPath(
+ new URL("./socket/plugin/server-runtime.js", import.meta.url)
+ )
+ ),
+ }),
+ importsPlugin(),
+ ],
+};
diff --git a/socket/lib/client.tsx b/socket/lib/client.tsx
new file mode 100644
index 0000000..710334b
--- /dev/null
+++ b/socket/lib/client.tsx
@@ -0,0 +1,232 @@
+import { from as rxFrom, mergeMap, Observable, tap } from "rxjs";
+import {
+ createSeriazliedMemo,
+ SerializedMemo,
+ SerializedRef,
+ SerializedThing,
+ WsMessage,
+ WsMessageDown,
+ WsMessageUp,
+} from "./shared";
+import {
+ createComputed,
+ createEffect,
+ createMemo,
+ createSignal,
+ from,
+ getOwner,
+ onCleanup,
+ runWithOwner,
+ untrack,
+} from "solid-js";
+import { createAsync } from "@solidjs/router";
+import { createLazyMemo } from "@solid-primitives/memo";
+import { createCallback } from "@solid-primitives/rootless";
+
+const globalWsPromise = new Promise((resolve) => {
+ const ws = new WebSocket("ws://localhost:3000/_ws");
+ ws.onopen = () => resolve(ws);
+});
+
+export type Listener = (ev: { data: any }) => any;
+export type SimpleWs = {
+ removeEventListener(type: "message", listener: Listener): void;
+ addEventListener(type: "message", listener: Listener): void;
+ send(data: string): void;
+};
+
+function wsRpc(message: WsMessageUp, wsPromise: Promise) {
+ const id = crypto.randomUUID() as string;
+
+ return new Promise<{ value: T; dispose: () => void }>(async (res, rej) => {
+ const ws = await wsPromise;
+
+ function dispose() {
+ ws.send(
+ JSON.stringify({ type: "dispose", id } satisfies WsMessage)
+ );
+ }
+
+ function handler(event: { data: string }) {
+ // console.log(`handler ${id}`, message, { data: event.data });
+ const data = JSON.parse(event.data) as WsMessage>;
+ if (data.id === id && data.type === "value") {
+ res({ value: data.value, dispose });
+ ws.removeEventListener("message", handler);
+ }
+ }
+
+ ws.addEventListener("message", handler);
+ ws.send(
+ JSON.stringify({ ...message, id } satisfies WsMessage)
+ );
+ });
+}
+
+function wsSub(message: WsMessageUp, wsPromise: Promise) {
+ const id = crypto.randomUUID();
+
+ return rxFrom(Promise.resolve(wsPromise)).pipe(
+ mergeMap((ws) => {
+ return new Observable((obs) => {
+ // console.log(`attaching sub handler`);
+ function handler(event: { data: string }) {
+ const data = JSON.parse(event.data) as WsMessage>;
+ // console.log(`data`, data, id);
+ if (data.id === id && data.type === "value") obs.next(data.value);
+ }
+
+ ws.addEventListener("message", handler);
+ ws.send(
+ JSON.stringify({ ...message, id } satisfies WsMessage)
+ );
+
+ return () => {
+ // console.log(`detaching sub handler`);
+ ws.removeEventListener("message", handler);
+ };
+ });
+ })
+ );
+}
+
+export function createRef(
+ ref: SerializedRef,
+ wsPromise: Promise
+) {
+ return (...input: any[]) =>
+ wsRpc(
+ {
+ type: "invoke",
+ ref,
+ input,
+ },
+ wsPromise
+ ).then(({ value }) => value);
+}
+
+export function createSocketMemoConsumer(
+ ref: SerializedMemo,
+ wsPromise: Promise
+) {
+ // console.log({ ref });
+ const memo = createLazyMemo(
+ () =>
+ from(
+ wsSub(
+ {
+ type: "subscribe",
+ ref,
+ },
+ wsPromise
+ )
+ ),
+ () => ref.initial
+ );
+
+ return () => {
+ const memoValue = memo()();
+ // console.log({ memoValue });
+ return memoValue;
+ };
+}
+
+type SerializedValue = SerializedThing | Record;
+
+const deserializeValue = (value: SerializedValue) => {
+ if (value.__type === "ref") {
+ return createRef(value, globalWsPromise);
+ } else if (value.__type === "memo") {
+ return createSocketMemoConsumer(value, globalWsPromise);
+ } else {
+ return Object.entries(value).reduce((res, [name, value]) => {
+ return {
+ ...res,
+ [name]:
+ value.__type === "ref"
+ ? createRef(value, globalWsPromise)
+ : value.__type === "memo"
+ ? createSocketMemoConsumer(value, globalWsPromise)
+ : value,
+ };
+ }, {} as any);
+ }
+};
+
+export function createEndpoint(
+ name: string,
+ input?: any,
+ wsPromise = globalWsPromise
+) {
+ const inputScope = crypto.randomUUID();
+ const serializedInput =
+ input?.type === "memo"
+ ? createSeriazliedMemo({
+ name: `input`,
+ scope: inputScope,
+ initial: untrack(input),
+ })
+ : input;
+ // console.log({ serializedInput });
+
+ const scopePromise = wsRpc(
+ { type: "create", name, input: serializedInput },
+ wsPromise
+ );
+
+ if (input?.type === "memo") {
+ const [inputSignal, setInput] = createSignal(input());
+ createComputed(() => setInput(input()));
+
+ const onSubscribe = createCallback(
+ (ws: SimpleWs, data: WsMessage>) => {
+ createEffect(() => {
+ const value = inputSignal();
+ // console.log(`sending input update to server`, value, input);
+ ws.send(
+ JSON.stringify({
+ type: "value",
+ id: data.id,
+ value,
+ } satisfies WsMessage)
+ );
+ });
+ }
+ );
+
+ const onWs = createCallback((ws: SimpleWs) => {
+ function handler(event: { data: string }) {
+ const data = JSON.parse(event.data) as WsMessage>;
+
+ if (data.type === "subscribe" && data.ref.scope === inputScope) {
+ onSubscribe(ws, data);
+ }
+ }
+ ws.addEventListener("message", handler);
+ onCleanup(() => ws.removeEventListener("message", handler));
+ });
+
+ wsPromise.then(onWs);
+ }
+
+ onCleanup(() => {
+ // console.log(`cleanup endpoint`);
+ scopePromise.then(({ dispose }) => dispose());
+ });
+
+ const scope = createAsync(() => scopePromise);
+ const deserializedScope = createMemo(
+ () => scope() && deserializeValue(scope()!.value)
+ );
+
+ return new Proxy((() => {}) as any, {
+ get(_, path) {
+ const res = deserializedScope()?.[path];
+ return res || (() => {});
+ },
+ apply(_, __, args) {
+ const res = deserializedScope()?.(...args);
+ return res;
+ },
+ });
+}
diff --git a/socket/lib/server.tsx b/socket/lib/server.tsx
new file mode 100644
index 0000000..ebd7884
--- /dev/null
+++ b/socket/lib/server.tsx
@@ -0,0 +1,233 @@
+import {
+ createSeriazliedMemo,
+ SerializedMemo,
+ SerializedRef,
+ SerializedStream,
+ SerializedThing,
+ WsMessage,
+ WsMessageDown,
+ WsMessageUp,
+} from "./shared";
+import {
+ createMemo,
+ createRoot,
+ createSignal,
+ observable,
+ onCleanup,
+ untrack,
+} from "solid-js";
+import { getManifest } from "vinxi/manifest";
+
+export type Callable = (arg: unknown) => T | Promise;
+
+export type Endpoint = (
+ input: I
+) => Callable | Record>;
+export type Endpoints = Record>;
+
+export type SimplePeer = {
+ id: string;
+ send(message: any): void;
+};
+
+export class LiveSolidServer {
+ private closures = new Map void }>();
+ observers = new Map();
+
+ constructor(public peer: SimplePeer) {}
+
+ send(message: WsMessage>) {
+ // console.log(`send`, message);
+ this.peer.send(JSON.stringify(message));
+ }
+
+ handleMessage(message: WsMessage) {
+ if (message.type === "create") {
+ this.create(message.id, message.name, message.input);
+ }
+
+ if (message.type === "subscribe") {
+ this.subscribe(message.id, message.ref);
+ }
+
+ if (message.type === "dispose") {
+ this.dispose(message.id);
+ }
+
+ if (message.type === "invoke") {
+ this.invoke(message.id, message.ref, message.input);
+ }
+
+ if (message.type === "value") {
+ this.observers.get(message.id)?.(message.value);
+ }
+ }
+
+ async create(id: string, name: string, input?: SerializedThing) {
+ const [filepath, functionName] = name.split("#");
+ // @ts-expect-error
+ const module = await getManifest(import.meta.env.ROUTER_NAME).chunks[
+ filepath
+ ].import();
+ const endpoint = module[functionName];
+
+ if (!endpoint) throw new Error(`Endpoint ${name} not found`);
+
+ const { payload, disposal } = createRoot((disposal) => {
+ const deserializedInput =
+ input?.__type === "memo"
+ ? createSocketMemoConsumer(input, this)
+ : input;
+
+ const payload = endpoint(deserializedInput);
+
+ return { payload, disposal };
+ });
+
+ this.closures.set(id, { payload, disposal });
+
+ if (typeof payload === "function") {
+ if (payload.type === "memo") {
+ const value = createSeriazliedMemo({
+ name,
+ scope: id,
+ initial: untrack(payload),
+ });
+ this.send({ value, id, type: "value" });
+ } else {
+ const value = createSeriazliedRef({
+ name,
+ scope: id,
+ });
+ this.send({ value, id, type: "value" });
+ }
+ } else {
+ const value = Object.entries(payload).reduce((res, [name, value]) => {
+ return {
+ ...res,
+ [name]:
+ typeof value === "function"
+ ? // @ts-expect-error
+ value.type === "memo"
+ ? createSeriazliedMemo({
+ name,
+ scope: id,
+ initial: untrack(() => value()),
+ })
+ : createSeriazliedRef({ name, scope: id })
+ : value,
+ };
+ }, {} as Record);
+ this.send({ value, id, type: "value" });
+ }
+ }
+
+ async invoke(id: string, ref: SerializedRef, input: any[]) {
+ const closure = this.closures.get(ref.scope);
+ if (!closure) throw new Error(`Callable ${ref.scope} not found`);
+ const { payload } = closure;
+
+ if (typeof payload === "function") {
+ const response = await payload(...input);
+ this.send({ id, value: response, type: "value" });
+ } else {
+ const response = await payload[ref.name](...input);
+ this.send({ id, value: response, type: "value" });
+ }
+ }
+
+ dispose(id: string) {
+ // console.log(`Disposing ${id}`);
+ const closure = this.closures.get(id);
+ if (closure) {
+ closure.disposal();
+ this.closures.delete(id);
+ }
+ }
+
+ subscribe(id: string, ref: SerializedMemo) {
+ // console.log(`subscribe`, ref);
+
+ const closure = this.closures.get(ref.scope);
+ if (!closure) throw new Error(`Callable ${ref.scope} not found`);
+ const { payload } = closure;
+
+ const func = typeof payload === "function" ? payload : payload[ref.name];
+
+ const response$ = observable(func);
+ const sub = response$.subscribe((value) => {
+ this.send({ id, value, type: "value" });
+ });
+ this.closures.set(id, { payload: sub, disposal: () => sub.unsubscribe() });
+ }
+
+ stream(stream: SerializedStream) {}
+
+ cleanup() {
+ for (const [key, closure] of this.closures.entries()) {
+ // console.log(`Disposing ${key}`);
+ closure.disposal();
+ this.closures.delete(key);
+ }
+ }
+}
+
+function createSeriazliedRef(
+ opts: Omit
+): SerializedRef {
+ return { ...opts, __type: "ref" };
+}
+
+export function createSocketFn(
+ fn: () => (i?: I) => O
+): () => (i?: I) => Promise;
+
+export function createSocketFn(
+ fn: () => Record O>
+): () => Record Promise>;
+
+export function createSocketFn(
+ fn: () => ((i: I) => O) | Record O>
+): () => ((i: I) => Promise) | Record Promise> {
+ return fn as any;
+}
+
+function createLazyMemo(
+ calc: (prev: T | undefined) => T,
+ value?: T
+): () => T {
+ let isReading = false,
+ isStale: boolean | undefined = true;
+
+ const [track, trigger] = createSignal(void 0, { equals: false }),
+ memo = createMemo(
+ (p) => (isReading ? calc(p) : ((isStale = !track()), p)),
+ value as T,
+ { equals: false }
+ );
+
+ return (): T => {
+ isReading = true;
+ if (isStale) isStale = trigger();
+ const v = memo();
+ isReading = false;
+ return v;
+ };
+}
+
+export function createSocketMemoConsumer(
+ ref: SerializedMemo,
+ server: LiveSolidServer
+) {
+ const inputSubId = crypto.randomUUID();
+
+ const memo = createLazyMemo(() => {
+ const [get, set] = createSignal(ref.initial);
+ server.observers.set(inputSubId, set);
+ server.send({ type: "subscribe", id: inputSubId, ref });
+ onCleanup(() => server.observers.delete(inputSubId));
+ return get;
+ });
+
+ return () => memo()();
+}
diff --git a/socket/lib/shared.tsx b/socket/lib/shared.tsx
new file mode 100644
index 0000000..9d2fc0b
--- /dev/null
+++ b/socket/lib/shared.tsx
@@ -0,0 +1,68 @@
+export type WsMessage = T & { id: string };
+
+export type WsMessageUp =
+ | {
+ type: "create";
+ name: string;
+ input?: I;
+ }
+ | {
+ type: "subscribe";
+ ref: SerializedMemo;
+ }
+ | {
+ type: "dispose";
+ }
+ | {
+ type: "invoke";
+ ref: SerializedRef;
+ input?: I;
+ }
+ | {
+ type: "value";
+ value: I;
+ };
+
+export type WsMessageDown =
+ | {
+ type: "value";
+ value: T;
+ }
+ | {
+ type: "subscribe";
+ ref: SerializedMemo;
+ };
+
+export type SerializedRef = {
+ __type: "ref";
+ name: string;
+ scope: string;
+};
+
+export type SerializedMemo = {
+ __type: "memo";
+ name: string;
+ scope: string;
+ initial: O;
+};
+
+export type SerializedThing = SerializedRef | SerializedMemo;
+
+export type SerializedStream = {
+ __type: "stream";
+ name: string;
+ scope: string;
+ value: O;
+};
+
+export function createSeriazliedMemo(
+ opts: Omit
+): SerializedMemo {
+ return { ...opts, __type: "memo" };
+}
+
+export function createSocketMemo(source: () => T): () => T | undefined {
+ // @ts-expect-error
+ source.type = "memo";
+ return source;
+}
diff --git a/socket/plugin/client-runtime.js b/socket/plugin/client-runtime.js
new file mode 100644
index 0000000..f01217e
--- /dev/null
+++ b/socket/plugin/client-runtime.js
@@ -0,0 +1,6 @@
+import { createEndpoint } from "../lib/client";
+
+export function createServerReference(fn, id, name) {
+ // console.log("createServerReference", id, name);
+ return (input) => createEndpoint(`${id}#${name}`, input);
+}
diff --git a/socket/plugin/client.js b/socket/plugin/client.js
new file mode 100644
index 0000000..f68c97e
--- /dev/null
+++ b/socket/plugin/client.js
@@ -0,0 +1,58 @@
+import { directives, shimExportsPlugin } from "@vinxi/plugin-directives";
+import { fileURLToPath } from "url";
+import { chunkify } from "vinxi/lib/chunks";
+import { normalize } from "vinxi/lib/path";
+
+import { CLIENT_REFERENCES_MANIFEST } from "./constants.js";
+
+export function client({
+ runtime = normalize(
+ fileURLToPath(new URL("./socket/plugin/client-runtime.js", import.meta.url))
+ ),
+ manifest = CLIENT_REFERENCES_MANIFEST,
+} = {}) {
+ const serverModules = new Set();
+ const clientModules = new Set();
+ return [
+ directives({
+ hash: chunkify,
+ runtime,
+ transforms: [
+ shimExportsPlugin({
+ runtime: {
+ module: runtime,
+ function: "createServerReference",
+ },
+ onModuleFound: (mod) => {
+ serverModules.add(mod);
+ },
+ hash: chunkify,
+ apply: (code, id, options) => {
+ return !options.ssr;
+ },
+ pragma: "use socket",
+ }),
+ ],
+ onReference(type, reference) {
+ if (type === "server") {
+ serverModules.add(reference);
+ } else {
+ clientModules.add(reference);
+ }
+ },
+ }),
+ {
+ name: "references-manifest",
+ generateBundle() {
+ this.emitFile({
+ fileName: manifest,
+ type: "asset",
+ source: JSON.stringify({
+ server: [...serverModules],
+ client: [...clientModules],
+ }),
+ });
+ },
+ },
+ ];
+}
diff --git a/socket/plugin/constants.js b/socket/plugin/constants.js
new file mode 100644
index 0000000..51cb6bb
--- /dev/null
+++ b/socket/plugin/constants.js
@@ -0,0 +1 @@
+export const CLIENT_REFERENCES_MANIFEST = `server-functions-manifest.json`;
diff --git a/socket/plugin/server-handler.ts b/socket/plugin/server-handler.ts
new file mode 100644
index 0000000..f9b0e26
--- /dev/null
+++ b/socket/plugin/server-handler.ts
@@ -0,0 +1,26 @@
+import { eventHandler } from "vinxi/http";
+import { LiveSolidServer } from "../lib/server";
+import { WsMessage, WsMessageUp } from "../lib/shared";
+
+const clients = new Map();
+
+export default eventHandler({
+ handler() {},
+ websocket: {
+ open(peer) {
+ clients.set(peer.id, new LiveSolidServer(peer));
+ },
+ message(peer, e) {
+ const message = JSON.parse(e.text()) as WsMessage;
+ const client = clients.get(peer.id);
+ if (!client) return;
+ client.handleMessage(message);
+ },
+ async close(peer) {
+ const client = clients.get(peer.id);
+ if (!client) return;
+ client.cleanup();
+ clients.delete(peer.id);
+ },
+ },
+});
diff --git a/socket/plugin/server-runtime.ts b/socket/plugin/server-runtime.ts
new file mode 100644
index 0000000..41b478c
--- /dev/null
+++ b/socket/plugin/server-runtime.ts
@@ -0,0 +1,4 @@
+export function createServerReference(fn, id, name) {
+ // console.log(`server runtime reference`, id, name);
+ return fn;
+}
diff --git a/socket/plugin/server.js b/socket/plugin/server.js
new file mode 100644
index 0000000..5a0f6f0
--- /dev/null
+++ b/socket/plugin/server.js
@@ -0,0 +1,92 @@
+import { directives, wrapExportsPlugin } from "@vinxi/plugin-directives";
+import { readFileSync } from "fs";
+import { fileURLToPath } from "url";
+import { chunkify } from "vinxi/lib/chunks";
+import { handlerModule, join, normalize } from "vinxi/lib/path";
+
+import { CLIENT_REFERENCES_MANIFEST } from "./constants.js";
+
+export function serverTransform({ runtime }) {
+ return directives({
+ hash: chunkify,
+ runtime: runtime,
+ transforms: [
+ wrapExportsPlugin({
+ runtime: {
+ module: runtime,
+ function: "createServerReference",
+ },
+ hash: chunkify,
+ apply: (code, id, options) => {
+ return options.ssr;
+ },
+ pragma: "use socket",
+ }),
+ ],
+ onReference(type, reference) {},
+ });
+}
+
+/**
+ *
+ * @returns {import('vinxi').Plugin}
+ */
+export const serverBuild = ({ client, manifest }) => {
+ let input;
+ return {
+ name: "server-functions:build",
+ enforce: "post",
+ apply: "build",
+ config(config, env) {
+ // @ts-ignore
+ const router = config.router;
+ // @ts-ignore
+ const app = config.app;
+
+ const rscRouter = app.getRouter(client);
+
+ const serverFunctionsManifest = JSON.parse(
+ readFileSync(join(rscRouter.outDir, rscRouter.base, manifest), "utf-8")
+ );
+
+ input = {
+ entry: handlerModule(router),
+ ...Object.fromEntries(
+ serverFunctionsManifest.server.map((key) => {
+ return [chunkify(key), key];
+ })
+ ),
+ };
+
+ return {
+ build: {
+ rollupOptions: {
+ output: {
+ chunkFileNames: "[name].mjs",
+ entryFileNames: "[name].mjs",
+ },
+ treeshake: true,
+ },
+ },
+ };
+ },
+
+ configResolved(config) {
+ config.build.rollupOptions.input = input;
+ },
+ };
+};
+
+/**
+ *
+ * @returns {import('vinxi').Plugin[]}
+ */
+export function server({
+ client = "client",
+ manifest = CLIENT_REFERENCES_MANIFEST,
+ runtime = normalize(
+ fileURLToPath(new URL("./server-runtime.js", import.meta.url))
+ ),
+} = {}) {
+ return [serverTransform({ runtime }), serverBuild({ client, manifest })];
+}
diff --git a/src/components/Board.tsx b/src/components/Board.tsx
index 7382307..4c0e3d1 100644
--- a/src/components/Board.tsx
+++ b/src/components/Board.tsx
@@ -1,24 +1,5 @@
-import { Action, useSubmissions } from "@solidjs/router";
-import { For, batch, createEffect, createMemo, untrack } from "solid-js";
-import { createStore, produce, reconcile } from "solid-js/store";
-import {
- AddColumn,
- Column,
- ColumnGap,
- ColumnId,
- createColumn,
- deleteColumn,
- moveColumn,
- renameColumn,
-} from "./Column";
-import {
- Note,
- NoteId,
- createNote,
- deleteNote,
- editNote,
- moveNote,
-} from "./Note";
+import { Column } from "./Column";
+import { Note } from "./Note";
export enum DragTypes {
Note = "application/note",
@@ -38,334 +19,3 @@ export type BoardData = {
columns: Column[];
notes: Note[];
};
-
-type Mutation =
- | {
- type: "createNote";
- id: NoteId;
- column: ColumnId;
- board: BoardId;
- body: string;
- order: number;
- timestamp: number;
- }
- | {
- type: "editNote";
- id: NoteId;
- content: string;
- timestamp: number;
- }
- | {
- type: "moveNote";
- id: NoteId;
- column: ColumnId;
- order: number;
- timestamp: number;
- }
- | {
- type: "deleteNote";
- id: NoteId;
- timestamp: number;
- }
- | {
- type: "createColumn";
- id: ColumnId;
- board: string;
- title: string;
- timestamp: number;
- }
- | {
- type: "renameColumn";
- id: ColumnId;
- title: string;
- timestamp: number;
- }
- | {
- type: "moveColumn";
- id: ColumnId;
- order: number;
- timestamp: number;
- }
- | {
- type: "deleteColumn";
- id: ColumnId;
- timestamp: number;
- };
-
-export function Board(props: { board: BoardData }) {
- const [boardStore, setBoardStore] = createStore({
- columns: props.board.columns,
- notes: props.board.notes,
- timestamp: 0,
- });
-
- const createNoteSubmission = useSubmissions(createNote);
- const editNoteSubmission = useSubmissions(editNote);
- const moveNoteSubmission = useSubmissions(moveNote);
- const deleteNoteSubmission = useSubmissions(deleteNote);
- const createColumnSubmission = useSubmissions(createColumn);
- const renameColumnSubmission = useSubmissions(renameColumn);
- const moveColumnSubmission = useSubmissions(moveColumn);
- const deleteColumnSubmission = useSubmissions(deleteColumn);
-
- function getMutations() {
- const mutations: Mutation[] = [];
-
- for (const note of createNoteSubmission.values()) {
- if (!note.pending) continue;
- const [{ id, column, body, order, timestamp }] = note.input;
- mutations.push({
- type: "createNote",
- board: props.board.board.id,
- id,
- column,
- body,
- order,
- timestamp,
- });
- }
-
- for (const note of editNoteSubmission.values()) {
- if (!note.pending) continue;
- const [id, content, timestamp] = note.input;
- mutations.push({
- type: "editNote",
- id,
- content,
- timestamp,
- });
- }
-
- for (const note of moveNoteSubmission.values()) {
- if (!note.pending) continue;
- const [id, column, order, timestamp] = note.input;
- mutations.push({
- type: "moveNote",
- id,
- column,
- order,
- timestamp,
- });
- }
-
- for (const note of deleteNoteSubmission.values()) {
- if (!note.pending) continue;
- const [id, timestamp] = note.input;
- mutations.push({
- type: "deleteNote",
- id,
- timestamp,
- });
- }
-
- for (const column of createColumnSubmission.values()) {
- if (!column.pending) continue;
- const [id, board, title, timestamp] = column.input;
- mutations.push({
- type: "createColumn",
- id,
- board,
- title,
- timestamp,
- });
- }
-
- for (const column of renameColumnSubmission.values()) {
- if (!column.pending) continue;
- const [id, title, timestamp] = column.input;
- mutations.push({
- type: "renameColumn",
- id,
- title,
- timestamp,
- });
- }
-
- for (const column of moveColumnSubmission.values()) {
- if (!column.pending) continue;
- const [id, order, timestamp] = column.input;
- mutations.push({
- type: "moveColumn",
- id,
- order,
- timestamp,
- });
- }
-
- for (const column of deleteColumnSubmission.values()) {
- if (!column.pending) continue;
- const [id, timestamp] = column.input;
- mutations.push({
- type: "deleteColumn",
- id,
- timestamp,
- });
- }
-
- return mutations;
- }
-
- // current approach
- // createEffect(() => {
- // const mutations = getMutations();
-
- // const newNotes = [...props.board.notes];
- // const newColumns = [...props.board.columns];
-
- // applyMutations(mutations, newNotes, newColumns);
-
- // batch(() => {
- // setBoardStore("notes", reconcile(newNotes));
- // setBoardStore("columns", reconcile(newColumns));
- // });
- // });
-
- createEffect(() => {
- const mutations = untrack(() => getMutations());
-
- const notes = [...props.board.notes];
- const columns = [...props.board.columns];
- applyMutations(mutations, notes, columns);
-
- console.log(
- `got server data, reset the board with mutations`,
- ...mutations
- );
-
- batch(() => {
- setBoardStore("notes", reconcile(notes));
- setBoardStore("columns", reconcile(columns));
- setBoardStore("timestamp", Date.now());
- });
- });
-
- createEffect(() => {
- const mutations = getMutations();
- const prevTimestamp = untrack(() => boardStore.timestamp);
- const latestMutations = mutations.filter(
- (m) => m.timestamp > prevTimestamp
- );
-
- console.log(
- `found submission, apply optimistic update with mutations`,
- ...latestMutations
- );
-
- setBoardStore(
- produce((b) => {
- applyMutations(latestMutations, b.notes, b.columns);
- b.timestamp = Date.now();
- })
- );
- });
-
- const sortedColumns = createMemo(() =>
- boardStore.columns.slice().sort((a, b) => a.order - b.order)
- );
-
- let scrollContainerRef: HTMLDivElement | undefined;
-
- return (
- {
- scrollContainerRef = el;
- }}
- class="pb-8 h-[calc(100vh-160px)] min-w-full overflow-x-auto overflow-y-hidden flex flex-start items-start flex-nowrap"
- >
-
-
- {(column, i) => (
- <>
-
-
- >
- )}
-
-
{
- scrollContainerRef &&
- (scrollContainerRef.scrollLeft = scrollContainerRef.scrollWidth);
- }}
- />
-
- );
-}
-
-function applyMutations(
- mutations: Mutation[],
- notes: Note[],
- columns: Column[]
-) {
- for (const mut of mutations.sort((a, b) => a.timestamp - b.timestamp)) {
- switch (mut.type) {
- case "createNote": {
- const index = notes.findIndex((n) => n.id === mut.id);
- if (index === -1)
- notes.push({
- id: mut.id,
- column: mut.column,
- body: mut.body,
- order: mut.order,
- board: mut.board,
- });
- break;
- }
- case "moveNote": {
- const index = notes.findIndex((n) => n.id === mut.id);
- if (index !== -1)
- notes[index] = {
- ...notes[index],
- column: mut.column,
- order: mut.order,
- };
- break;
- }
- case "editNote": {
- const index = notes.findIndex((n) => n.id === mut.id);
- if (index !== -1) notes[index] = { ...notes[index], body: mut.content };
- break;
- }
- case "deleteNote": {
- const index = notes.findIndex((n) => n.id === mut.id);
- if (index !== -1) notes.splice(index, 1);
- break;
- }
- case "createColumn": {
- const index = columns.findIndex((c) => c.id === mut.id);
- if (index === -1)
- columns.push({
- id: mut.id,
- board: mut.board,
- title: mut.title,
- order: columns.length + 1,
- });
- break;
- }
- case "renameColumn": {
- const index = columns.findIndex((c) => c.id === mut.id);
- if (index !== -1)
- columns[index] = { ...columns[index], title: mut.title };
- break;
- }
- case "moveColumn": {
- const index = columns.findIndex((c) => c.id === mut.id);
- if (index !== -1)
- columns[index] = { ...columns[index], order: mut.order };
- break;
- }
- case "deleteColumn": {
- const index = columns.findIndex((c) => c.id === mut.id);
- if (index !== -1) columns.splice(index, 1);
- break;
- }
- }
- }
-}
diff --git a/src/components/Column.tsx b/src/components/Column.tsx
index 020ba53..e4b8568 100644
--- a/src/components/Column.tsx
+++ b/src/components/Column.tsx
@@ -1,4 +1,3 @@
-import { action, useAction } from "@solidjs/router";
import { BsPlus, BsTrash } from "solid-icons/bs";
import { RiEditorDraggable } from "solid-icons/ri";
import {
@@ -9,74 +8,9 @@ import {
createSignal,
onMount,
} from "solid-js";
-import { type Board, type BoardId, DragTypes } from "./Board";
import { getIndexBetween } from "~/lib/utils";
-import { AddNote, Note, NoteId, moveNote } from "./Note";
-import { getAuthUser } from "~/lib/auth";
-import { db } from "~/lib/db";
-
-export const renameColumn = action(
- async (id: ColumnId, name: string, timestamp: number) => {
- "use server";
- const accountId = await getAuthUser();
-
- await db.column.update({
- where: { id, Board: { accountId } },
- data: { name },
- });
-
- return true;
- }
-);
-
-export const createColumn = action(
- async (id: ColumnId, board: BoardId, name: string, timestamp: number) => {
- "use server";
-
- const accountId = await getAuthUser();
-
- let columnCount = await db.column.count({
- where: { boardId: +board, Board: { accountId } },
- });
- await db.column.create({
- data: {
- id,
- boardId: +board,
- name,
- order: columnCount + 1,
- },
- });
-
- return true;
- },
- "create-column"
-);
-
-export const moveColumn = action(
- async (id: ColumnId, order: number, timestamp: number) => {
- "use server";
- const accountId = await getAuthUser();
-
- await db.column.update({
- where: { id, Board: { accountId } },
- data: { order },
- });
-
- return;
- },
- "create-column"
-);
-
-export const deleteColumn = action(async (id: ColumnId, timestamp: number) => {
- "use server";
- const accountId = await getAuthUser();
-
- await db.column.delete({
- where: { id, Board: { accountId } },
- });
-
- return true;
-}, "create-column");
+import { type BoardId, DragTypes } from "./Board";
+import { AddNote, Note, NoteId } from "./Note";
export type ColumnId = string & { __brand?: "ColumnId" };
@@ -87,13 +21,24 @@ export type Column = {
order: number;
};
-export function Column(props: { column: Column; board: Board; notes: Note[] }) {
+export function Column(props: {
+ boardId: string;
+ column: Column;
+ notes: Note[];
+ renameColumn: (columnId: ColumnId, name: string) => void;
+ deleteColumn: (columnId: ColumnId) => void;
+ moveNote: (noteId: NoteId, column: ColumnId, order: number) => void;
+ createNote: (
+ noteId: NoteId,
+ column: ColumnId,
+ body: string,
+ order: number
+ ) => void;
+ editNote: (noteId: NoteId, body: string) => void;
+ deleteNote: (noteId: NoteId) => void;
+}) {
let parent: HTMLDivElement | undefined;
- const renameAction = useAction(renameColumn);
- const deleteAction = useAction(deleteColumn);
- const moveNoteAction = useAction(moveNote);
-
const [acceptDrop, setAcceptDrop] = createSignal(false);
const filteredNotes = createMemo(() =>
@@ -130,14 +75,13 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) {
| NoteId
| undefined;
if (noteId && !filteredNotes().find((n) => n.id === noteId)) {
- moveNoteAction(
+ props.moveNote(
noteId,
props.column.id,
getIndexBetween(
filteredNotes()[filteredNotes().length - 1]?.order,
undefined
- ),
- new Date().getTime()
+ )
);
}
}
@@ -154,11 +98,7 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) {
required
onBlur={(e) => {
if (e.target.reportValidity()) {
- renameAction(
- props.column.id,
- e.target.value,
- new Date().getTime()
- );
+ props.renameColumn(props.column.id, e.target.value);
}
}}
onKeyDown={(e) => {
@@ -170,7 +110,7 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) {
/>
@@ -185,14 +125,18 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) {
note={n}
previous={filteredNotes()[i() - 1]}
next={filteredNotes()[i() + 1]}
+ moveNote={props.moveNote}
+ editNote={props.editNote}
+ deleteNote={props.deleteNote}
/>
)}
{
parent && (parent.scrollTop = parent.scrollHeight);
}}
@@ -201,9 +145,13 @@ export function Column(props: { column: Column; board: Board; notes: Note[] }) {
);
}
-export function ColumnGap(props: { left?: Column; right?: Column }) {
+export function ColumnGap(props: {
+ left?: Column;
+ right?: Column;
+ moveColumn: (columnId: ColumnId, order: number) => void;
+}) {
const [active, setActive] = createSignal(false);
- const moveColumnAction = useAction(moveColumn);
+
return (
void }) {
+export function AddColumn(props: {
+ board: BoardId;
+ createColumn: (columnId: ColumnId, title: string) => void;
+}) {
const [active, setActive] = createSignal(false);
- const addColumn = useAction(createColumn);
-
let inputRef: HTMLInputElement | undefined;
let plusRef: HTMLButtonElement | undefined;
@@ -267,14 +216,11 @@ export function AddColumn(props: { board: BoardId; onAdd: () => void }) {
);
diff --git a/src/components/Note.tsx b/src/components/Note.tsx
index 61c609a..c8114b2 100644
--- a/src/components/Note.tsx
+++ b/src/components/Note.tsx
@@ -1,4 +1,3 @@
-import { action, useAction } from "@solidjs/router";
import { BsPlus, BsTrash } from "solid-icons/bs";
import { RiEditorDraggable } from "solid-icons/ri";
import { Match, Switch, createSignal } from "solid-js";
@@ -8,106 +7,6 @@ import { getIndexBetween } from "~/lib/utils";
import { getAuthUser } from "~/lib/auth";
import { db } from "~/lib/db";
-export const createNote = action(
- async ({
- id,
- column,
- body,
- order,
- timestamp,
- board,
- }: {
- id: NoteId;
- board: BoardId;
- column: ColumnId;
- body: string;
- order: number;
- timestamp: number;
- }) => {
- "use server";
- const accountId = await getAuthUser();
- const mutation = {
- id: String(id),
- title: String(body),
- order,
- boardId: +board,
- columnId: String(column),
- };
-
- await db.item.upsert({
- where: {
- id: mutation.id,
- Board: {
- accountId,
- },
- },
- create: mutation,
- update: mutation,
- });
-
- return true;
- },
- "create-item"
-);
-
-export const editNote = action(
- async (id: NoteId, content: string, timestamp: number) => {
- "use server";
- const accountId = await getAuthUser();
- const mutation = {
- id: String(id),
- title: String(content),
- };
-
- await db.item.update({
- where: {
- id: mutation.id,
- Board: {
- accountId,
- },
- },
- data: mutation,
- });
-
- return true;
- },
- "edit-item"
-);
-
-export const moveNote = action(
- async (note: NoteId, column: ColumnId, order: number, timestamp: number) => {
- "use server";
- const accountId = await getAuthUser();
- const mutation = {
- id: String(note),
- columnId: String(column),
- order,
- };
-
- await db.item.update({
- where: {
- id: mutation.id,
- Board: {
- accountId,
- },
- },
- data: mutation,
- });
-
- return true;
- },
- "move-item"
-);
-
-export const deleteNote = action(async (id: NoteId, timestamp: number) => {
- "use server";
- const accountId = await getAuthUser();
-
- await db.item.delete({ where: { id, Board: { accountId } } });
-
- return true;
-}, "delete-card");
-
export type NoteId = string & { __brand?: "NoteId" };
export type Note = {
@@ -118,11 +17,14 @@ export type Note = {
body: string;
};
-export function Note(props: { note: Note; previous?: Note; next?: Note }) {
- const updateAction = useAction(editNote);
- const deleteAction = useAction(deleteNote);
- const moveNoteAction = useAction(moveNote);
-
+export function Note(props: {
+ note: Note;
+ previous?: Note;
+ next?: Note;
+ moveNote: (noteId: NoteId, column: ColumnId, order: number) => void;
+ editNote: (noteId: NoteId, body: string) => void;
+ deleteNote: (noteId: NoteId) => void;
+}) {
let input: HTMLTextAreaElement | undefined;
const [isBeingDragged, setIsBeingDragged] = createSignal(false);
@@ -189,11 +91,10 @@ export function Note(props: { note: Note; previous?: Note; next?: Note }) {
if (props.previous && props.previous?.id === noteId) {
break action;
}
- moveNoteAction(
+ props.moveNote(
noteId,
props.note.column,
- getIndexBetween(props.previous?.order, props.note.order),
- new Date().getTime()
+ getIndexBetween(props.previous?.order, props.note.order)
);
}
@@ -201,11 +102,10 @@ export function Note(props: { note: Note; previous?: Note; next?: Note }) {
if (props.previous && props.next?.id === noteId) {
break action;
}
- moveNoteAction(
+ props.moveNote(
noteId,
props.note.column,
- getIndexBetween(props.note.order, props.next?.order),
- new Date().getTime()
+ getIndexBetween(props.note.order, props.next?.order)
);
}
}
@@ -224,18 +124,14 @@ export function Note(props: { note: Note; previous?: Note; next?: Note }) {
resize: "none",
}}
onBlur={(e) =>
- updateAction(
- props.note.id,
- (e.target as HTMLTextAreaElement).value,
- new Date().getTime()
- )
+ props.editNote(props.note.id, (e.target as HTMLTextAreaElement).value)
}
>
{`${props.note.body}`}
@@ -248,9 +144,14 @@ export function AddNote(props: {
length: number;
onAdd: () => void;
board: BoardId;
+ createNote: (
+ noteId: NoteId,
+ column: ColumnId,
+ body: string,
+ order: number
+ ) => void;
}) {
const [active, setActive] = createSignal(false);
- const addNote = useAction(createNote);
let inputRef: HTMLInputElement | undefined;
@@ -262,20 +163,18 @@ export function AddNote(props: {
class="flex flex-col space-y-2 card w-full"
onSubmit={(e) => {
e.preventDefault();
- const body = inputRef?.value.trim() ?? 'Note'
- if (body === '') {
- inputRef?.setCustomValidity('Please fill out this field.');
+ const body = inputRef?.value.trim() ?? "Note";
+ if (body === "") {
+ inputRef?.setCustomValidity("Please fill out this field.");
inputRef?.reportValidity();
return;
}
- addNote({
- id: crypto.randomUUID() as NoteId,
- board: props.board,
- column: props.column,
+ props.createNote(
+ crypto.randomUUID() as NoteId,
+ props.column,
body,
- order: props.length + 1,
- timestamp: new Date().getTime(),
- });
+ props.length + 1
+ );
inputRef && (inputRef.value = "");
props.onAdd();
}}
diff --git a/src/components/Presence.tsx b/src/components/Presence.tsx
new file mode 100644
index 0000000..b560475
--- /dev/null
+++ b/src/components/Presence.tsx
@@ -0,0 +1,109 @@
+import { createComputed, createEffect, createSignal, For } from "solid-js";
+import { PresenceUser, usePresence } from "./board-data";
+import { createSocketMemo } from "../../socket/lib/shared";
+import {
+ createPositionToElement,
+ useMousePosition,
+} from "@solid-primitives/mouse";
+import { debounce } from "@solid-primitives/scheduled";
+import { createStore, reconcile } from "solid-js/store";
+import { Tooltip } from "@kobalte/core/tooltip";
+import { RiDevelopmentCursorLine } from "solid-icons/ri";
+
+export function Presence() {
+ const pos = useMousePosition();
+ const users = usePresence(createSocketMemo(() => pos));
+ const [presenceStore, setPresenceStore] = createStore([]);
+
+ createComputed(() =>
+ setPresenceStore(reconcile(Object.values(users() || {})))
+ );
+
+ return (
+
+
+ {(user) => {
+ createEffect(() => {
+ console.log(user.name, user.x, user.y);
+ });
+ return (
+
+
+
+ {user.name
+ .split(" ")
+ .map((n) => n[0])
+ .join("")}
+
+
+
+
+
+ {user.name}
+
+
+
+ );
+ }}
+
+
+ {(user) => {
+ return (
+
+
+
+ );
+ }}
+
+
+ );
+}
+
+function createDebouncedMousePos(ref: () => HTMLElement | undefined) {
+ const pos = useMousePosition();
+ const relative = createPositionToElement(ref, () => pos);
+ const [debouncedPos, setDebouncedPos] = createSignal<{
+ x: number;
+ y: number;
+ }>();
+ const trigger = debounce(
+ (pos: { x: number; y: number }) => setDebouncedPos(pos),
+ 5
+ );
+ createEffect(() => {
+ const { x, y } = relative;
+ x && y && trigger({ x, y });
+ });
+ return debouncedPos;
+}
diff --git a/src/components/board-data.ts b/src/components/board-data.ts
new file mode 100644
index 0000000..d490b8c
--- /dev/null
+++ b/src/components/board-data.ts
@@ -0,0 +1,224 @@
+"use socket";
+
+import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
+import { BoardData, BoardId } from "./Board";
+import {
+ uniqueNamesGenerator,
+ adjectives,
+ colors,
+ animals,
+} from "unique-names-generator";
+import { createSocketMemo } from "../../socket/lib/shared";
+import { ColumnId } from "./Column";
+import { NoteId } from "./Note";
+
+const [boards, setBoards] = createSignal>({});
+
+export const useBoards = () => {
+ return {
+ boards: createSocketMemo(boards),
+ async createBoard(title: string, color: string) {
+ const boardId = `${Object.keys(boards()).length + 1}`;
+ setBoards((b) => ({
+ ...b,
+ [boardId]: {
+ board: { id: boardId, title, color },
+ columns: [],
+ notes: [],
+ },
+ }));
+ return boardId;
+ },
+ deleteBoard(boardId: BoardId) {
+ setBoards((b) => {
+ const { [boardId]: _, ...rest } = b;
+ return rest;
+ });
+ },
+ };
+};
+
+export const useBoard = (boardId: () => string | undefined) => {
+ createEffect(() => console.log(`boardId`, boardId()));
+ function moveColumn(columnId: ColumnId, order: number) {
+ const id = boardId();
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ columns: b[id].columns.map((c) =>
+ c.id === columnId ? { ...c, order } : c
+ ),
+ },
+ }));
+ }
+
+ function renameColumn(columnId: ColumnId, name: string) {
+ const id = boardId();
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ columns: b[id].columns.map((c) =>
+ c.id === columnId ? { ...c, name } : c
+ ),
+ },
+ }));
+ }
+
+ function deleteColumn(columnId: ColumnId) {
+ const id = boardId();
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ columns: b[id].columns.filter((c) => c.id !== columnId),
+ },
+ }));
+ }
+
+ function createColumn(columnId: ColumnId, title: string) {
+ const id = boardId() as BoardId;
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ columns: [
+ ...b[id].columns,
+ {
+ id: columnId,
+ order: b[id].columns.length + 1,
+ board: id,
+ title,
+ },
+ ],
+ },
+ }));
+ }
+
+ function moveNote(noteId: NoteId, column: ColumnId, order: number) {
+ console.log(`moveNote`, noteId, column, order);
+ const id = boardId();
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ notes: b[id].notes.map((n) =>
+ n.id === noteId ? { ...n, column, order } : n
+ ),
+ },
+ }));
+ }
+
+ function createNote(
+ noteId: NoteId,
+ column: ColumnId,
+ body: string,
+ order: number
+ ) {
+ const id = boardId() as BoardId;
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ notes: [
+ ...b[id].notes,
+ {
+ id: noteId,
+ column,
+ body,
+ order,
+ board: id,
+ },
+ ],
+ },
+ }));
+ }
+
+ function editNote(noteId: NoteId, content: string) {
+ const id = boardId() as BoardId;
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ notes: b[id].notes.map((n) =>
+ n.id === noteId ? { ...n, body: content } : n
+ ),
+ },
+ }));
+ }
+
+ function deleteNote(noteId: NoteId) {
+ const id = boardId() as BoardId;
+ if (!id) return;
+ setBoards((b) => ({
+ ...b,
+ [id]: {
+ ...b[id],
+ notes: b[id].notes.filter((n) => n.id !== noteId),
+ },
+ }));
+ }
+
+ return {
+ board: createSocketMemo(() =>
+ boardId() ? boards()[boardId()!] : undefined
+ ),
+ async setBoard(data: BoardData) {
+ setBoards((b) => ({ ...b, [data.board.id]: data }));
+ },
+ moveColumn,
+ moveNote,
+ renameColumn,
+ deleteColumn,
+ createColumn,
+ createNote,
+ editNote,
+ deleteNote,
+ };
+};
+
+export type PresenceUser = {
+ name: string;
+ x: number;
+ y: number;
+ color: string;
+};
+
+const [users, setUsers] = createSignal>({});
+
+export const usePresence = (
+ mousePos: () => { x: number; y: number } | undefined
+) => {
+ const id = crypto.randomUUID();
+ const color = Math.floor(Math.random() * 16777215).toString(16);
+ const name = uniqueNamesGenerator({
+ dictionaries: [adjectives, colors, animals],
+ style: "capital",
+ separator: " ",
+ });
+
+ createEffect(() => {
+ const { x, y } = mousePos() || {};
+ console.log(name, x, y);
+ x && y && setUsers((u) => ({ ...u, [id]: { name, x, y, color } }));
+ });
+
+ onCleanup(() => {
+ setUsers(({ [id]: _, ...rest }) => rest);
+ });
+
+ const otherUsers = createMemo(() => {
+ const { [id]: _, ...rest } = users();
+ return rest;
+ });
+
+ return createSocketMemo(otherUsers);
+};
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 0c2c97c..6031213 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -1,14 +1,6 @@
import { PrismaClient } from "@prisma/client";
-import { PrismaLibSQL } from "@prisma/adapter-libsql";
-import { createClient } from "@libsql/client";
-const libsql = createClient({
- url: `${process.env.TURSO_DATABASE_URL}`,
- authToken: `${process.env.TURSO_AUTH_TOKEN}`,
-});
-
-const adapter = new PrismaLibSQL(libsql);
-const db = new PrismaClient({ adapter });
+const db = new PrismaClient();
process.on("beforeExit", () => {
db.$disconnect();
diff --git a/src/routes/board/[id].tsx b/src/routes/board/[id].tsx
index ca78470..bc149c5 100644
--- a/src/routes/board/[id].tsx
+++ b/src/routes/board/[id].tsx
@@ -1,106 +1,82 @@
import { Title } from "@solidjs/meta";
-import {
- RouteDefinition,
- RouteSectionProps,
- action,
- cache,
- createAsync,
- redirect,
- useAction,
- useSubmission,
-} from "@solidjs/router";
-import { Show } from "solid-js";
-import { Board, BoardData } from "~/components/Board";
+import { RouteSectionProps } from "@solidjs/router";
+import { createComputed, createEffect, createMemo, For, Show } from "solid-js";
+import { useBoard } from "~/components/board-data";
import EditableText from "~/components/EditableText";
-import { getAuthUser } from "~/lib/auth";
-import { db } from "~/lib/db";
-
-const fetchBoard = cache(async (boardId: number) => {
- "use server";
- const accountId = await getAuthUser();
-
- const boardFromDataBase = await db.board.findUnique({
- where: {
- id: boardId,
- accountId,
- },
- include: {
- items: true,
- columns: { orderBy: { order: "asc" } },
- },
- });
-
- if (!boardFromDataBase) throw redirect("/");
-
- // mapping the db to what the board expects
- return {
- board: {
- id: String(boardFromDataBase.id),
- title: boardFromDataBase.name,
- color: boardFromDataBase.color,
- },
- notes:
- boardFromDataBase.items.map((note) => ({
- ...note,
- board: String(note.boardId),
- column: note.columnId,
- body: note.title || "",
- })) || [],
- columns:
- boardFromDataBase.columns.map((column) => ({
- ...column,
- board: String(column.boardId),
- title: column.name,
- })) || [],
- } satisfies BoardData;
-}, "get-board-data");
-
-const updateBoardName = action(async (boardId: number, name: string) => {
- "use server";
- const accountId = await getAuthUser();
-
- return db.board.update({
- where: { id: boardId, accountId },
- data: { name },
- });
-}, "update-board-name");
-
-export const route: RouteDefinition = {
- load: (props) => fetchBoard(+props.params.id),
-};
+import { createSocketMemo } from "../../../socket/lib/shared";
+import { AddColumn, Column, ColumnGap } from "~/components/Column";
+import { createStore } from "solid-js/store";
+import { reconcile } from "solid-js/store";
+import { Presence } from "~/components/Presence";
export default function Page(props: RouteSectionProps) {
- const board = createAsync(() => fetchBoard(+props.params.id));
- const submission = useSubmission(updateBoardName);
- const updateBoardNameAction = useAction(updateBoardName);
+ const boardId = createSocketMemo(() => props.params.id);
+ const serverBoard = useBoard(boardId);
+ let scrollContainerRef: HTMLDivElement | undefined;
return (
-
- {(board) => (
-
- {board().board.title} | Strello
+
+ {(board) => {
+ const [boardStore, setBoardStore] = createStore(board());
+ createComputed(() => setBoardStore(reconcile(board())));
+
+ const sortedColumns = createMemo(
+ () =>
+ boardStore.columns.slice().sort((a, b) => a.order - b.order) || []
+ );
-
-
- updateBoardNameAction(+props.params.id, value)
- }
- />
-
+ return (
+
+ {board().board.title} | Strello
+
+
+ {}}
+ />
+
-
-
-
-
- )}
+
+
+
+ {(column, i) => (
+ <>
+
+
+ >
+ )}
+
+
serverBoard.createColumn(...p)}
+ />
+
+
+ );
+ }}
);
}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 32e0136..e2afe95 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,88 +1,15 @@
import { Title } from "@solidjs/meta";
-import {
- action,
- cache,
- createAsync,
- redirect,
- useSubmission,
- useSubmissions,
- type RouteDefinition,
-} from "@solidjs/router";
+import { A, useNavigate, type RouteDefinition } from "@solidjs/router";
import { BsTrash } from "solid-icons/bs";
import { For, Show, onMount } from "solid-js";
-import { getUser } from "~/lib";
-import { getSession } from "~/lib/auth";
-import { db } from "~/lib/db";
-
-const addBoard = action(async (formData: FormData) => {
- "use server";
-
- const session = await getSession();
- const userId = session.data.userId;
- const name = String(formData.get("name"));
- const color = String(formData.get("color"));
-
- const board = await db.board.create({
- data: {
- accountId: userId,
- name,
- color,
- },
- });
-
- return redirect(`/board/${board.id}`);
-}, "add-board");
-
-const deleteBoard = action(async (boardId: number) => {
- "use server";
- const session = await getSession();
- const userId = session.data.userId;
-
- await db.board.delete({
- where: { id: boardId, accountId: userId },
- });
-}, "delete-board");
-
-const getBoards = cache(async () => {
- "use server";
- const session = await getSession();
- const userId = session.data.userId;
-
- return db.board.findMany({
- where: {
- accountId: userId,
- },
- });
-}, "get-boards");
+import { useBoards } from "~/components/board-data";
export const route = {
- load: () => {
- getUser();
- getBoards();
- },
+ load: () => {},
} satisfies RouteDefinition;
export default function Home() {
- const user = createAsync(() => getUser(), { deferStream: true });
- const serverBoards = createAsync(() => getBoards());
- const addBoardSubmission = useSubmission(addBoard);
- const deleteBoardSubmissions = useSubmissions(deleteBoard);
-
- const boards = () => {
- if (deleteBoardSubmissions.pending) {
- const deletedBoards: number[] = [];
-
- for (const sub of deleteBoardSubmissions) {
- deletedBoards.push(sub.input[0]);
- }
-
- return serverBoards()?.filter(
- (board) => !deletedBoards.includes(board.id)
- );
- }
-
- return serverBoards();
- };
+ const serverBoards = useBoards();
let inputRef: HTMLInputElement | undefined;
@@ -90,95 +17,106 @@ export default function Home() {
inputRef?.focus();
});
+ const nav = useNavigate();
+
+ const boardsList = () => Object.values(serverBoards.boards() || {});
+
return (
-
- Boards | Strello
-
-