This commit is contained in:
Tobias Berger 2025-03-22 21:43:33 +01:00
parent 20ab699e38
commit 036414ab20
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
25 changed files with 2727 additions and 1380 deletions

9
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always",
"source.fixAll.ts": "always"
}
}

View file

@ -1,289 +1,270 @@
{
"rules": {
// Perf
"no-await-in-loop": "deny",
"no-useless-call": "deny",
"no-accumulating-spread": "deny",
"no-array-index-key": "deny",
"jsx-no-jsx-as-prop": "deny",
"jsx-no-new-array-as-prop": "deny",
"jsx-no-new-function-as-prop": "deny",
"jsx-no-new-object-as-prop": "deny",
"prefer-set-has": "deny",
// Suspicious
"no-extend-native": "deny",
"no-new": "deny",
"no-unexpected-multiline": "deny",
"no-unneeded-ternary": "deny",
"no-useless-concat": "deny",
"no-useless-constructor": "deny",
"no-absolute-path": "deny",
"no-duplicates": "deny",
"no-empty-named-blocks": "deny",
"no-named-as-default": "deny",
"no-named-as-default-member": "deny",
"no-self-import": "deny",
"no-commented-out-tests": "deny",
"approx-constant": "deny",
"misrefactored-assign-op": "deny",
"no-async-endpoint-handlers": "deny",
"no-promise-in-callback": "deny",
"iframe-missing-sandbox": "deny",
"jsx-no-comment-textnodes": "deny",
"jsx-no-script-url": "deny",
"no-namespace": "deny",
"style-prop-object": "deny",
"no-confusing-non-null-assertion": "deny",
"no-extraneous-class": "deny",
"no-unnecessary-type-constraint": "deny",
"consistent-function-scoping": "deny",
"prefer-add-event-listener": "deny",
"require-post-message-target-origin": "deny",
// Pedantic
"array-callback-return": "deny",
"eqeqeq": "deny",
"max-classes-per-file": "deny",
"max-depth": "deny",
"max-lines": "deny",
"max-lines-per-function": "deny",
"max-nested-callbacks": "deny",
"no-array-constructor": "deny",
"no-case-declarations": "deny",
"no-constructor-return": "deny",
"no-else-return": "deny",
"no-fallthrough": "deny",
"no-inner-declarations": "deny",
"no-lonely-if": "deny",
"no-negated-condition": "deny",
"no-new-wrappers": "deny",
"no-object-constructor": "deny",
"no-prototype-builtins": "deny",
"no-redeclare": "deny",
"no-self-compare": "deny",
"no-throw-literal": "deny",
"radix": "deny",
"require-await": "deny",
"sort-vars": "deny",
"symbol-description": "deny",
"max-dependencies": [
"deny",
{
"max": 16
}
],
"no-conditional-in-test": "deny",
"require-param": "deny",
"require-param-description": "deny",
"require-param-name": "deny",
"require-param-type": "deny",
"require-returns": "deny",
"require-returns-description": "deny",
"require-returns-type": "deny",
"checked-requires-onchange-or-readonly": "deny",
"jsx-no-useless-fragment": "deny",
"no-unescaped-entities": "deny",
"rules-of-hooks": "deny",
"ban-ts-comment": "deny",
"ban-types": "deny",
"no-unsafe-function-type": "deny",
"prefer-enum-initializers": "deny",
"prefer-ts-expect-error": "deny",
"consistent-empty-array-spread": "deny",
"escape-case": "deny",
"explicit-length-check": "deny",
"new-for-builtins": "deny",
"no-hex-escape": "deny",
"no-instanceof-array": "deny",
"no-negation-in-equality-check": "deny",
"no-new-buffer": "deny",
"no-object-as-default-parameter": "deny",
"no-static-only-class": "deny",
"no-this-assignment": "deny",
"no-typeof-undefined": "deny",
"no-unreadable-iife": "deny",
"no-useless-promise-resolve-reject": "deny",
"no-useless-switch-case": "deny",
"no-useless-undefined": "deny",
"prefer-array-flat": "deny",
"prefer-array-some": "deny",
"prefer-blob-reading-methods": "deny",
"prefer-code-point": "deny",
"prefer-date-now": "deny",
"prefer-dom-node-append": "deny",
"prefer-dom-node-dataset": "deny",
"prefer-dom-node-remove": "deny",
"prefer-event-target": "deny",
"prefer-math-min-max": "deny",
"prefer-math-trunc": "deny",
"prefer-native-coercion-functions": "deny",
"prefer-prototype-methods": "deny",
"prefer-query-selector": "deny",
"prefer-regexp-test": "deny",
"prefer-string-replace-all": "deny",
"prefer-string-slice": "deny",
"prefer-type-error": "deny",
"require-number-to-fixed-digits-argument": "deny",
// Style
"curly": "deny",
"default-case-last": "deny",
"default-param-last": "deny",
"func-names": "deny",
"func-style": "deny",
"grouped-accessor-pairs": "deny",
"guard-for-in": "deny",
"init-declarations": "deny",
"max-params": "deny",
"new-cap": "deny",
"no-continue": "deny",
"no-duplicate-imports": "deny",
"no-extra-label": "deny",
"no-label-var": "deny",
"no-labels": "deny",
"no-lone-blocks": "deny",
"no-multi-assign": "deny",
"no-multi-str": "deny",
"no-nested-ternary": "deny",
"no-new-func": "deny",
"no-return-assign": "deny",
"no-script-url": "deny",
"no-template-curly-in-string": "deny",
"no-ternary": "deny",
"operator-assignment": "deny",
"prefer-exponentiation-operator": "deny",
"prefer-numeric-literals": "deny",
"prefer-object-has-own": "deny",
"prefer-object-spread": "deny",
"prefer-promise-reject-errors": "deny",
"prefer-rest-params": "deny",
"sort-imports": "deny",
"sort-keys": "deny",
"vars-on-top": "deny",
"yoda": "deny",
"exports-last": "deny",
"first": "deny",
"no-anonymous-default-export": "deny",
"no-mutable-exports": "deny",
"no-named-default": "deny",
"consistent-test-it": "deny",
"max-expects": "deny",
"max-nested-describe": "deny",
"no-alias-methods": "deny",
"no-confusing-set-timeout": "deny",
"no-deprecated-functions": "deny",
"no-done-callback": "deny",
"no-duplicate-hooks": "deny",
"no-hooks": "deny",
"no-identical-title": "deny",
"no-interpolation-in-snapshots": "deny",
"no-jasmine-globals": "deny",
"no-large-snapshots": "deny",
"no-mocks-import": "deny",
"no-restricted-jest-methods": "deny",
"no-restricted-matchers": "deny",
"no-test-prefixes": "deny",
"no-test-return-statement": "deny",
"no-untyped-mock-factory": "deny",
"prefer-called-with": "deny",
"prefer-comparison-matcher": "deny",
"prefer-each": "deny",
"prefer-equality-matcher": "deny",
"prefer-expect-resolves": "deny",
"prefer-hooks-in-order": "deny",
"prefer-hooks-on-top": "deny",
"prefer-jest-mocked": "deny",
"prefer-lowercase-title": "deny",
"prefer-mock-promise-shorthand": "deny",
"prefer-spy-on": "deny",
"prefer-strict-equal": "deny",
"prefer-to-be": "deny",
"prefer-to-contain": "deny",
"prefer-to-have-length": "deny",
"prefer-todo": "deny",
"require-hook": "deny",
"require-top-level-describe": "deny",
"no-exports-assign": "deny",
"no-nesting": "deny",
"no-return-wrap": "deny",
"param-names": "deny",
"prefer-await-to-callbacks": "deny",
"prefer-await-to-then": "deny",
"prefer-catch": "deny",
"jsx-boolean-value": "deny",
"jsx-curly-brace-presence": "deny",
"no-set-state": "deny",
"prefer-es6-class": "deny",
"self-closing-comp": "deny",
"adjacent-overload-signatures": "deny",
"array-type": "deny",
"ban-tslint-comment": "deny",
"consistent-generic-constructors": "deny",
"consistent-indexed-object-style": "deny",
"consistent-type-definitions": "deny",
"no-empty-interface": "deny",
"no-inferrable-types": "deny",
"prefer-for-of": "deny",
"prefer-function-type": "deny",
"prefer-namespace-keyword": "deny",
"catch-error-name": "deny",
"consistent-date-clone": "deny",
"consistent-existence-index-check": "deny",
"empty-brace-spaces": "deny",
"error-message": "deny",
"filename-case": "deny",
"no-await-expression-member": "deny",
"no-console-spaces": "deny",
"no-null": "deny",
"no-unreadable-array-destructuring": "deny",
"no-zero-fractions": "deny",
"number-literal-case": "deny",
"numeric-separators-style": "deny",
"prefer-array-flat-map": "deny",
"prefer-dom-node-text-content": "deny",
"prefer-includes": "deny",
"prefer-logical-operator-over-ternary": "deny",
"prefer-modern-dom-apis": "deny",
"prefer-negative-index": "deny",
"prefer-optional-catch-binding": "deny",
"prefer-reflect-apply": "deny",
"prefer-spread": "deny",
"prefer-string-raw": "deny",
"prefer-string-trim-start-end": "deny",
"prefer-structured-clone": "deny",
"require-array-join-separator": "deny",
"switch-case-braces": "deny",
"text-encoding-identifier-case": "deny",
"throw-new-error": "deny",
"no-import-node-test": "deny",
"prefer-to-be-falsy": "deny",
"prefer-to-be-object": "deny",
"prefer-to-be-truthy": "deny",
// Nursery
"getter-return": "deny",
"no-undef": "deny",
"no-unreachable": "deny",
"export": "deny",
"named": "deny",
"no-map-spread": "deny",
"no-return-in-finally": "deny",
"exhaustive-deps": "deny",
"require-render-return": "deny",
"consistent-type-imports": "deny"
},
"plugins": [
"typescript",
"unicorn",
"oxc",
"import",
"promise",
"solid"
],
"ignorePatterns": [
"src/bindings/"
],
"env": {
"builtin": true,
"browser": true,
"es2024": true,
"worker": true,
}
}
"rules": {
// Perf
"no-await-in-loop": "deny",
"no-useless-call": "deny",
"no-accumulating-spread": "deny",
"no-array-index-key": "deny",
"jsx-no-jsx-as-prop": "deny",
"jsx-no-new-array-as-prop": "deny",
"jsx-no-new-function-as-prop": "deny",
"jsx-no-new-object-as-prop": "deny",
"prefer-set-has": "deny",
// Suspicious
"no-extend-native": "deny",
"no-new": "deny",
"no-unexpected-multiline": "deny",
"no-unneeded-ternary": "deny",
"no-useless-concat": "deny",
"no-useless-constructor": "deny",
"no-absolute-path": "deny",
"no-duplicates": "deny",
"no-empty-named-blocks": "deny",
"no-named-as-default": "deny",
"no-named-as-default-member": "deny",
"no-self-import": "deny",
"no-commented-out-tests": "deny",
"approx-constant": "deny",
"misrefactored-assign-op": "deny",
"no-async-endpoint-handlers": "deny",
"no-promise-in-callback": "deny",
"iframe-missing-sandbox": "deny",
"jsx-no-comment-textnodes": "deny",
"jsx-no-script-url": "deny",
"no-namespace": "deny",
"style-prop-object": "deny",
"no-confusing-non-null-assertion": "deny",
"no-extraneous-class": "deny",
"no-unnecessary-type-constraint": "deny",
"consistent-function-scoping": "deny",
"prefer-add-event-listener": "deny",
"require-post-message-target-origin": "deny",
// Pedantic
"array-callback-return": "deny",
"eqeqeq": "deny",
"max-classes-per-file": "deny",
"max-depth": "deny",
"max-lines": "deny",
"max-lines-per-function": "deny",
"max-nested-callbacks": "deny",
"no-array-constructor": "deny",
"no-case-declarations": "deny",
"no-constructor-return": "deny",
"no-else-return": "deny",
"no-fallthrough": "deny",
"no-inner-declarations": "deny",
"no-lonely-if": "deny",
"no-negated-condition": "deny",
"no-new-wrappers": "deny",
"no-object-constructor": "deny",
"no-prototype-builtins": "deny",
"no-redeclare": "deny",
"no-self-compare": "deny",
"no-throw-literal": "deny",
"radix": "deny",
"require-await": "deny",
"sort-vars": "deny",
"symbol-description": "deny",
"max-dependencies": ["deny", { "max": 16 }],
"no-conditional-in-test": "deny",
"require-param": "deny",
"require-param-description": "deny",
"require-param-name": "deny",
"require-param-type": "deny",
"require-returns": "deny",
"require-returns-description": "deny",
"require-returns-type": "deny",
"checked-requires-onchange-or-readonly": "deny",
"jsx-no-useless-fragment": "deny",
"no-unescaped-entities": "deny",
"rules-of-hooks": "deny",
"ban-ts-comment": "deny",
"ban-types": "deny",
"no-unsafe-function-type": "deny",
"prefer-enum-initializers": "deny",
"prefer-ts-expect-error": "deny",
"consistent-empty-array-spread": "deny",
"escape-case": "deny",
"explicit-length-check": "deny",
"new-for-builtins": "deny",
"no-hex-escape": "deny",
"no-instanceof-array": "deny",
"no-negation-in-equality-check": "deny",
"no-new-buffer": "deny",
"no-object-as-default-parameter": "deny",
"no-static-only-class": "deny",
"no-this-assignment": "deny",
"no-typeof-undefined": "deny",
"no-unreadable-iife": "deny",
"no-useless-promise-resolve-reject": "deny",
"no-useless-switch-case": "deny",
"no-useless-undefined": "deny",
"prefer-array-flat": "deny",
"prefer-array-some": "deny",
"prefer-blob-reading-methods": "deny",
"prefer-code-point": "deny",
"prefer-date-now": "deny",
"prefer-dom-node-append": "deny",
"prefer-dom-node-dataset": "deny",
"prefer-dom-node-remove": "deny",
"prefer-event-target": "deny",
"prefer-math-min-max": "deny",
"prefer-math-trunc": "deny",
"prefer-native-coercion-functions": "deny",
"prefer-prototype-methods": "deny",
"prefer-query-selector": "deny",
"prefer-regexp-test": "deny",
"prefer-string-replace-all": "deny",
"prefer-string-slice": "deny",
"prefer-type-error": "deny",
"require-number-to-fixed-digits-argument": "deny",
// Style
"curly": "deny",
"default-case-last": "deny",
"default-param-last": "deny",
"func-names": "deny",
"func-style": "deny",
"grouped-accessor-pairs": "deny",
"guard-for-in": "deny",
"init-declarations": "deny",
"max-params": "deny",
"new-cap": "deny",
"no-continue": "deny",
"no-duplicate-imports": "deny",
"no-extra-label": "deny",
"no-label-var": "deny",
"no-labels": "deny",
"no-lone-blocks": "deny",
"no-multi-assign": "deny",
"no-multi-str": "deny",
"no-nested-ternary": "deny",
"no-new-func": "deny",
"no-return-assign": "deny",
"no-script-url": "deny",
"no-template-curly-in-string": "deny",
"no-ternary": "deny",
"operator-assignment": "deny",
"prefer-exponentiation-operator": "deny",
"prefer-numeric-literals": "deny",
"prefer-object-has-own": "deny",
"prefer-object-spread": "deny",
"prefer-promise-reject-errors": "deny",
"prefer-rest-params": "deny",
"sort-imports": "deny",
"sort-keys": "deny",
"vars-on-top": "deny",
"yoda": "deny",
"exports-last": "deny",
"first": "deny",
"no-anonymous-default-export": "deny",
"no-mutable-exports": "deny",
"no-named-default": "deny",
"consistent-test-it": "deny",
"max-expects": "deny",
"max-nested-describe": "deny",
"no-alias-methods": "deny",
"no-confusing-set-timeout": "deny",
"no-deprecated-functions": "deny",
"no-done-callback": "deny",
"no-duplicate-hooks": "deny",
"no-hooks": "deny",
"no-identical-title": "deny",
"no-interpolation-in-snapshots": "deny",
"no-jasmine-globals": "deny",
"no-large-snapshots": "deny",
"no-mocks-import": "deny",
"no-restricted-jest-methods": "deny",
"no-restricted-matchers": "deny",
"no-test-prefixes": "deny",
"no-test-return-statement": "deny",
"no-untyped-mock-factory": "deny",
"prefer-called-with": "deny",
"prefer-comparison-matcher": "deny",
"prefer-each": "deny",
"prefer-equality-matcher": "deny",
"prefer-expect-resolves": "deny",
"prefer-hooks-in-order": "deny",
"prefer-hooks-on-top": "deny",
"prefer-jest-mocked": "deny",
"prefer-lowercase-title": "deny",
"prefer-mock-promise-shorthand": "deny",
"prefer-spy-on": "deny",
"prefer-strict-equal": "deny",
"prefer-to-be": "deny",
"prefer-to-contain": "deny",
"prefer-to-have-length": "deny",
"prefer-todo": "deny",
"require-hook": "deny",
"require-top-level-describe": "deny",
"no-exports-assign": "deny",
"no-nesting": "deny",
"no-return-wrap": "deny",
"param-names": "deny",
"prefer-await-to-callbacks": "deny",
"prefer-await-to-then": "deny",
"prefer-catch": "deny",
"jsx-boolean-value": "deny",
"jsx-curly-brace-presence": "deny",
"no-set-state": "deny",
"prefer-es6-class": "deny",
"self-closing-comp": "deny",
"adjacent-overload-signatures": "deny",
"array-type": "deny",
"ban-tslint-comment": "deny",
"consistent-generic-constructors": "deny",
"consistent-indexed-object-style": "deny",
"consistent-type-definitions": "deny",
"no-empty-interface": "deny",
"no-inferrable-types": "deny",
"prefer-for-of": "deny",
"prefer-function-type": "deny",
"prefer-namespace-keyword": "deny",
"catch-error-name": "deny",
"consistent-date-clone": "deny",
"consistent-existence-index-check": "deny",
"empty-brace-spaces": "deny",
"error-message": "deny",
"filename-case": "deny",
"no-await-expression-member": "deny",
"no-console-spaces": "deny",
"no-null": "deny",
"no-unreadable-array-destructuring": "deny",
"no-zero-fractions": "deny",
"number-literal-case": "deny",
"numeric-separators-style": "deny",
"prefer-array-flat-map": "deny",
"prefer-dom-node-text-content": "deny",
"prefer-includes": "deny",
"prefer-logical-operator-over-ternary": "deny",
"prefer-modern-dom-apis": "deny",
"prefer-negative-index": "deny",
"prefer-optional-catch-binding": "deny",
"prefer-reflect-apply": "deny",
"prefer-spread": "deny",
"prefer-string-raw": "deny",
"prefer-string-trim-start-end": "deny",
"prefer-structured-clone": "deny",
"require-array-join-separator": "deny",
"switch-case-braces": "deny",
"text-encoding-identifier-case": "deny",
"throw-new-error": "deny",
"no-import-node-test": "deny",
"prefer-to-be-falsy": "deny",
"prefer-to-be-object": "deny",
"prefer-to-be-truthy": "deny",
// Nursery
"getter-return": "deny",
"no-undef": "deny",
"no-unreachable": "deny",
"export": "deny",
"named": "deny",
"no-map-spread": "deny",
"no-return-in-finally": "deny",
"exhaustive-deps": "deny",
"require-render-return": "deny",
"consistent-type-imports": "deny"
},
"plugins": ["typescript", "unicorn", "oxc", "import", "promise", "solid"],
"ignorePatterns": ["src/bindings/"],
"env": { "builtin": true, "browser": true, "es2024": true, "worker": true }
}

3
client/.prettierignore Normal file
View file

@ -0,0 +1,3 @@
dist
node_modules
src/bindings

34
client/eslint.config.ts Normal file
View file

@ -0,0 +1,34 @@
// eslint.config.js
import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import oxlint from "eslint-plugin-oxlint";
import solid from "eslint-plugin-solid/configs/typescript";
import tseslint from "typescript-eslint";
import unicorn from "eslint-plugin-unicorn";
export default [
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
ignores: [
"src/bindings/",
"dist/",
"eslint.config.ts",
"vite.config.ts",
"prettier.config.js",
],
},
{ rules: { "solid/no-array-handlers": "warn", "solid/prefer-show": "warn" } },
solid,
eslint.configs.recommended,
unicorn.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
...oxlint.buildFromOxlintConfigFile(".oxlintrc.json"),
];

View file

@ -5,16 +5,27 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
"build": "vite build",
"preview": "vite preview",
"lint": "oxlint && eslint && prettier . -c",
"format": "oxlint --fix && eslint --fix && prettier . -w"
},
"dependencies": {
"@clockworklabs/spacetimedb-sdk": "^1.0.3",
"solid-js": "^1.9.5"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-oxlint": "^0.16.2",
"eslint-plugin-solid": "^0.14.5",
"eslint-plugin-unicorn": "^57.0.0",
"jiti": "^2.4.2",
"prettier": "^3.5.3",
"typescript": "~5.7.3",
"typescript-eslint": "^8.27.0",
"vite": "^6.2.2",
"vite-plugin-oxlint": "^1.3.0",
"vite-plugin-solid": "^2.11.6"

2857
client/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

22
client/prettier.config.js Normal file
View file

@ -0,0 +1,22 @@
/** @type {import("prettier").Config} */
const config = {
arrowParens: "always",
bracketSameLine: true,
bracketSpacing: true,
embeddedLanguageFormatting: "auto",
endOfLine: "lf",
experimentalOperatorPosition: "start",
experimentalTernaries: true,
htmlWhitespaceSensitivity: "strict",
jsxSingleQuote: false,
objectWrap: "collapse",
proseWrap: "always",
quoteProps: "consistent",
semi: true,
singleAttributePerLine: false,
singleQuote: false,
tabWidth: 2,
trailingComma: "all",
};
export default config;

View file

@ -1,4 +1,4 @@
#root {
margin: 0 auto;
padding: 2rem;
}
}

View file

@ -1,12 +1,19 @@
import './app.css'
import "./app.css";
import { DbConnection, type Note as DbNote, type ErrorContext, type EventContext } from './bindings';
import { For, Show, createEffect, createSignal } from 'solid-js'
import { type Identity, Timestamp } from '@clockworklabs/spacetimedb-sdk'
import { Note } from './lib';
import {
DbConnection as DatabaseConnection,
type Note as DatabaseNote,
type ErrorContext,
type EventContext,
} from "./bindings";
import { For, Show, createEffect, createSignal } from "solid-js";
import { type Identity, Timestamp } from "@clockworklabs/spacetimedb-sdk";
import { Note } from "./library";
export async function wait(ms: number) {
return await new Promise((resolve) => { setTimeout(resolve, ms) });
return await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
enum Status {
@ -17,131 +24,160 @@ enum Status {
}
export default function App() {
const [connection, setConnection] = createSignal<DbConnection | undefined>();
const [connection, setConnection] = createSignal<
DatabaseConnection | undefined
>();
const [status, setStatus] = createSignal<Status>(Status.Connecting);
const [notes, setNotes] = createSignal<Note[]>([])
const [notes, setNotes] = createSignal<Note[]>([]);
const noteOnInsert = (_context: EventContext, row: DatabaseNote) => {
const newNote = new Note(row);
// Asserting here that a newly inserted Note will always be at the end, and skip sorting
setNotes([...notes(), newNote]);
};
const noteOnUpdate = (
_context: EventContext,
oldRow: DatabaseNote,
newRow: DatabaseNote,
) => {
const note = notes().find(({ id }) => id === oldRow.id);
if (note) {
console.debug("server setting", oldRow.id, "to", newRow.value);
note.update(newRow);
}
};
const noteOnDelete = (_context: EventContext, row: DatabaseNote) => {
console.debug("server deleting", row.id);
setNotes(notes().filter(({ id }) => id !== row.id));
};
const onDisconnect = (_context: ErrorContext, error?: Error) => {
if (error) console.error("Disconnecting", error);
else console.error("Disconnecting");
setStatus(Status.Disconnected);
const conn = connection();
if (!conn) return;
const note = conn.db.note;
note.removeOnInsert(noteOnInsert);
note.removeOnUpdate(noteOnUpdate);
note.removeOnDelete(noteOnDelete);
};
createEffect(() => {
const noteOnInsert = (_ctx: EventContext, row: DbNote) => {
const newNote = new Note(row);
// Asserting here that a newly inserted Note will always be at the end, and skip sorting
setNotes([...notes(), newNote])
}
const noteOnUpdate = (_ctx: EventContext, oldRow: DbNote, newRow: DbNote) => {
const note = notes().find(({id}) => id === oldRow.id);
if (note) {
console.debug("server setting", oldRow.id, "to", newRow.value)
note.update(newRow);
}
};
const noteOnDelete = (_ctx: EventContext, row: DbNote) => {
console.debug("server deleting", row.id);
setNotes(notes().filter(({id}) => id !== row.id));
};
const onConnect = (connection: DbConnection, identity: Identity, token: string) => {
console.debug("onConnect")
const onConnect = (
connection: DatabaseConnection,
identity: Identity,
token: string,
) => {
console.debug("onConnect");
localStorage.setItem("auth_token", token);
console.debug("Connected with Identity", identity.toHexString());
const note = connection.db.note;
note.onInsert(noteOnInsert)
note.onUpdate(noteOnUpdate)
note.onDelete(noteOnDelete)
note.onInsert(noteOnInsert);
note.onUpdate(noteOnUpdate);
note.onDelete(noteOnDelete);
setStatus(Status.Loading);
connection.subscriptionBuilder().onApplied(() => {
console.debug("onApplied", "SELECT * FROM note");
setStatus(Status.Connected)
})
connection
.subscriptionBuilder()
.onApplied(() => {
console.debug("onApplied", "SELECT * FROM note");
setStatus(Status.Connected);
})
.onError(console.error)
.subscribe("SELECT * FROM note");
}
};
const onDisconnect = (_ctx: ErrorContext, error?: Error) => {
if (error)
console.error("Disconnecting", error)
else
console.error("Disconnecting")
setStatus(Status.Disconnected)
const conn = connection();
if (!conn) return;
const note = conn.db.note;
note.removeOnInsert(noteOnInsert);
note.removeOnUpdate(noteOnUpdate);
note.removeOnDelete(noteOnDelete);
}
setConnection(DbConnection.builder()
.withUri(`ws://${window.location.hostname}:3000`)
.withModuleName("test")
.withToken(localStorage.getItem("auth_token") ?? "")
.onConnect(onConnect)
.onDisconnect(onDisconnect)
.onConnectError(console.error)
.build())
})
setConnection(
DatabaseConnection.builder()
.withUri(`ws://${globalThis.location.hostname}:3000`)
.withModuleName("test")
.withToken(localStorage.getItem("auth_token") ?? "")
.onConnect(onConnect)
.onDisconnect(onDisconnect)
.onConnectError(console.error)
.build(),
);
});
// Not predicted because next ID is not easy to determine
const tryAddNote = () => {
connection()?.reducers.createNote();
}
};
const tryUpdateNote = (note: Note, value: string) => {
connection()?.reducers.updateNote(note.id, value);
// Predict!
note.lastUpdate(Timestamp.now())
}
note.lastUpdate(Timestamp.now());
};
const tryDeleteNote = (id: number) => {
connection()?.reducers.deleteNote(id);
console.debug("client deleting", id);
// Predict!
setNotes(notes().filter(({id: noteId}) => noteId !== id));
}
setNotes(notes().filter(({ id: noteId }) => noteId !== id));
};
const getStatusText = () => {
switch (status()) {
case Status.Connecting:
{
return "connecting...";
}
case Status.Loading:
{
return "loading...";
}
case Status.Connected:
{
return "connected"
}
case Status.Disconnected:
{
return "disconnected";
}
default:
{
return "Huh?";
}
case Status.Connecting: {
return "connecting...";
}
case Status.Loading: {
return "loading...";
}
case Status.Connected: {
return "connected";
}
case Status.Disconnected: {
return "disconnected";
}
default: {
return "Huh?";
}
}
}
};
return (
<Show when={status() === Status.Connected} fallback={<span>{getStatusText()}</span>}>
<Show
when={status() === Status.Connected}
fallback={<span>{getStatusText()}</span>}>
<button on:click={tryAddNote}>+</button>
<br />
<For each={notes()}>
{
(note) => {
return <>
<span attr:data-note-id={note.id} attr:data-last-updated={note.lastUpdate().toDate().toISOString()}>
<textarea id={`note-${note.id}`} on:input={ev => tryUpdateNote(note, ev.target.value)} value={note.value()}></textarea>
<button id={`delete-note-${note.id}`} on:click={_ => tryDeleteNote(note.id)}>-</button>
{(note) => {
return (
<>
<span
attr:data-note-id={note.id}
attr:data-last-updated={note
.lastUpdate()
.toDate()
.toISOString()}>
<textarea
id={`note-${note.id}`}
on:input={(event) => {
tryUpdateNote(note, event.target.value);
}}
value={note.value()}
/>
<button
id={`delete-note-${note.id}`}
on:click={(_) => {
tryDeleteNote(note.id);
}}>
-
</button>
</span>
<br />
</>
}
}
);
}}
</For>
</Show >
)
</Show>
);
}

View file

@ -38,12 +38,11 @@ export type CreateNote = {};
*/
export namespace CreateNote {
/**
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
export function getTypeScriptAlgebraicType(): AlgebraicType {
return AlgebraicType.createProductType([
]);
return AlgebraicType.createProductType([]);
}
export function serialize(writer: BinaryWriter, value: CreateNote): void {
@ -53,6 +52,4 @@ export namespace CreateNote {
export function deserialize(reader: BinaryReader): CreateNote {
return CreateNote.getTypeScriptAlgebraicType().deserialize(reader);
}
}

View file

@ -32,7 +32,7 @@ import {
} from "@clockworklabs/spacetimedb-sdk";
export type DeleteNote = {
id: number,
id: number;
};
/**
@ -40,9 +40,9 @@ export type DeleteNote = {
*/
export namespace DeleteNote {
/**
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
export function getTypeScriptAlgebraicType(): AlgebraicType {
return AlgebraicType.createProductType([
new ProductTypeElement("id", AlgebraicType.createU32Type()),
@ -56,6 +56,4 @@ export namespace DeleteNote {
export function deserialize(reader: BinaryReader): DeleteNote {
return DeleteNote.getTypeScriptAlgebraicType().deserialize(reader);
}
}

View file

@ -79,32 +79,42 @@ const REMOTE_MODULE = {
eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {
return {
...(imp as DbConnection),
event
}
event,
};
},
dbViewConstructor: (imp: DbConnectionImpl) => {
return new RemoteTables(imp);
},
reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {
reducersConstructor: (
imp: DbConnectionImpl,
setReducerFlags: SetReducerFlags,
) => {
return new RemoteReducers(imp, setReducerFlags);
},
setReducerFlagsConstructor: () => {
return new SetReducerFlags();
}
}
},
};
// A type representing all the possible variants of a reducer.
export type Reducer = never
| { name: "CreateNote", args: CreateNote }
| { name: "DeleteNote", args: DeleteNote }
| { name: "UpdateNote", args: UpdateNote }
;
export type Reducer =
| never
| { name: "CreateNote"; args: CreateNote }
| { name: "DeleteNote"; args: DeleteNote }
| { name: "UpdateNote"; args: UpdateNote };
export class RemoteReducers {
constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {}
constructor(
private connection: DbConnectionImpl,
private setCallReducerFlags: SetReducerFlags,
) {}
createNote() {
this.connection.callReducer("create_note", new Uint8Array(0), this.setCallReducerFlags.createNoteFlags);
this.connection.callReducer(
"create_note",
new Uint8Array(0),
this.setCallReducerFlags.createNoteFlags,
);
}
onCreateNote(callback: (ctx: ReducerEventContext) => void) {
@ -120,7 +130,11 @@ export class RemoteReducers {
let __writer = new BinaryWriter(1024);
DeleteNote.getTypeScriptAlgebraicType().serialize(__writer, __args);
let __argsBuffer = __writer.getBuffer();
this.connection.callReducer("delete_note", __argsBuffer, this.setCallReducerFlags.deleteNoteFlags);
this.connection.callReducer(
"delete_note",
__argsBuffer,
this.setCallReducerFlags.deleteNoteFlags,
);
}
onDeleteNote(callback: (ctx: ReducerEventContext, id: number) => void) {
@ -136,57 +150,101 @@ export class RemoteReducers {
let __writer = new BinaryWriter(1024);
UpdateNote.getTypeScriptAlgebraicType().serialize(__writer, __args);
let __argsBuffer = __writer.getBuffer();
this.connection.callReducer("update_note", __argsBuffer, this.setCallReducerFlags.updateNoteFlags);
this.connection.callReducer(
"update_note",
__argsBuffer,
this.setCallReducerFlags.updateNoteFlags,
);
}
onUpdateNote(callback: (ctx: ReducerEventContext, id: number, value: string) => void) {
onUpdateNote(
callback: (ctx: ReducerEventContext, id: number, value: string) => void,
) {
this.connection.onReducer("update_note", callback);
}
removeOnUpdateNote(callback: (ctx: ReducerEventContext, id: number, value: string) => void) {
removeOnUpdateNote(
callback: (ctx: ReducerEventContext, id: number, value: string) => void,
) {
this.connection.offReducer("update_note", callback);
}
}
export class SetReducerFlags {
createNoteFlags: CallReducerFlags = 'FullUpdate';
createNoteFlags: CallReducerFlags = "FullUpdate";
createNote(flags: CallReducerFlags) {
this.createNoteFlags = flags;
}
deleteNoteFlags: CallReducerFlags = 'FullUpdate';
deleteNoteFlags: CallReducerFlags = "FullUpdate";
deleteNote(flags: CallReducerFlags) {
this.deleteNoteFlags = flags;
}
updateNoteFlags: CallReducerFlags = 'FullUpdate';
updateNoteFlags: CallReducerFlags = "FullUpdate";
updateNote(flags: CallReducerFlags) {
this.updateNoteFlags = flags;
}
}
export class RemoteTables {
constructor(private connection: DbConnectionImpl) {}
get note(): NoteTableHandle {
return new NoteTableHandle(this.connection.clientCache.getOrCreateTable<Note>(REMOTE_MODULE.tables.note));
return new NoteTableHandle(
this.connection.clientCache.getOrCreateTable<Note>(
REMOTE_MODULE.tables.note,
),
);
}
}
export class SubscriptionBuilder extends SubscriptionBuilderImpl<RemoteTables, RemoteReducers, SetReducerFlags> { }
export class SubscriptionBuilder extends SubscriptionBuilderImpl<
RemoteTables,
RemoteReducers,
SetReducerFlags
> {}
export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {
static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {
return new DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext>(REMOTE_MODULE, (imp: DbConnectionImpl) => imp as DbConnection);
}
export class DbConnection extends DbConnectionImpl<
RemoteTables,
RemoteReducers,
SetReducerFlags
> {
static builder = (): DbConnectionBuilder<
DbConnection,
ErrorContext,
SubscriptionEventContext
> => {
return new DbConnectionBuilder<
DbConnection,
ErrorContext,
SubscriptionEventContext
>(REMOTE_MODULE, (imp: DbConnectionImpl) => imp as DbConnection);
};
subscriptionBuilder = (): SubscriptionBuilder => {
return new SubscriptionBuilder(this);
}
};
}
export type EventContext = EventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags, Reducer>;
export type ReducerEventContext = ReducerEventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags, Reducer>;
export type SubscriptionEventContext = SubscriptionEventContextInterface<RemoteTables, RemoteReducers, SetReducerFlags>;
export type ErrorContext = ErrorContextInterface<RemoteTables, RemoteReducers, SetReducerFlags>;
export type EventContext = EventContextInterface<
RemoteTables,
RemoteReducers,
SetReducerFlags,
Reducer
>;
export type ReducerEventContext = ReducerEventContextInterface<
RemoteTables,
RemoteReducers,
SetReducerFlags,
Reducer
>;
export type SubscriptionEventContext = SubscriptionEventContextInterface<
RemoteTables,
RemoteReducers,
SetReducerFlags
>;
export type ErrorContext = ErrorContextInterface<
RemoteTables,
RemoteReducers,
SetReducerFlags
>;

View file

@ -82,25 +82,28 @@ export class NoteTableHandle {
onInsert = (cb: (ctx: EventContext, row: Note) => void) => {
return this.tableCache.onInsert(cb);
}
};
removeOnInsert = (cb: (ctx: EventContext, row: Note) => void) => {
return this.tableCache.removeOnInsert(cb);
}
};
onDelete = (cb: (ctx: EventContext, row: Note) => void) => {
return this.tableCache.onDelete(cb);
}
};
removeOnDelete = (cb: (ctx: EventContext, row: Note) => void) => {
return this.tableCache.removeOnDelete(cb);
}
};
// Updates are only defined for tables with primary keys.
onUpdate = (cb: (ctx: EventContext, oldRow: Note, newRow: Note) => void) => {
return this.tableCache.onUpdate(cb);
}
};
removeOnUpdate = (cb: (ctx: EventContext, onRow: Note, newRow: Note) => void) => {
removeOnUpdate = (
cb: (ctx: EventContext, onRow: Note, newRow: Note) => void,
) => {
return this.tableCache.removeOnUpdate(cb);
}}
};
}

View file

@ -31,9 +31,9 @@ import {
deepEqual,
} from "@clockworklabs/spacetimedb-sdk";
export type Note = {
id: number,
lastUpdate: Timestamp,
value: string,
id: number;
lastUpdate: Timestamp;
value: string;
};
/**
@ -41,9 +41,9 @@ export type Note = {
*/
export namespace Note {
/**
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
export function getTypeScriptAlgebraicType(): AlgebraicType {
return AlgebraicType.createProductType([
new ProductTypeElement("id", AlgebraicType.createU32Type()),
@ -59,7 +59,4 @@ export namespace Note {
export function deserialize(reader: BinaryReader): Note {
return Note.getTypeScriptAlgebraicType().deserialize(reader);
}
}

View file

@ -32,8 +32,8 @@ import {
} from "@clockworklabs/spacetimedb-sdk";
export type UpdateNote = {
id: number,
value: string,
id: number;
value: string;
};
/**
@ -41,9 +41,9 @@ export type UpdateNote = {
*/
export namespace UpdateNote {
/**
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
* A function which returns this type represented as an AlgebraicType.
* This function is derived from the AlgebraicType used to generate this type.
*/
export function getTypeScriptAlgebraicType(): AlgebraicType {
return AlgebraicType.createProductType([
new ProductTypeElement("id", AlgebraicType.createU32Type()),
@ -58,6 +58,4 @@ export namespace UpdateNote {
export function deserialize(reader: BinaryReader): UpdateNote {
return UpdateNote.getTypeScriptAlgebraicType().deserialize(reader);
}
}

View file

@ -41,4 +41,4 @@ button:hover {
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
}

View file

@ -1,8 +1,8 @@
/* @refresh reload */
import './index.css'
import App from './app.tsx'
import { render } from 'solid-js/web'
import "./index.css";
import App from "./app.tsx";
import { render } from "solid-js/web";
const root = document.querySelector('#root')
const root = document.querySelector("#root");
render(() => <App />, root!)
render(() => <App />, root!);

View file

@ -1,46 +0,0 @@
import { type Signal, createSignal } from "solid-js"
import type { Note as DbNote } from "./bindings"
import type { Timestamp } from "@clockworklabs/spacetimedb-sdk"
export class Note {
#valueSignal: Signal<ValueType>
#lastUpdatedSignal: Signal<Timestamp>
id: number
#getValue(): ValueType {
return this.#valueSignal[0]()
}
#setValue(value: ValueType): ValueType {
return this.#valueSignal[1](value)
}
value(value?: ValueType): ValueType {
if (value === undefined) {
return this.#getValue();
}
return this.#setValue(value);
}
#getLastUpdate(): Timestamp {
return this.#lastUpdatedSignal[0]();
}
#setLastUpdate(lastUpdate: Timestamp): Timestamp {
return this.#lastUpdatedSignal[1](lastUpdate);
}
lastUpdate(lastUpdate?: Timestamp): Timestamp {
if (lastUpdate === undefined) {
return this.#getLastUpdate();
}
return this.#setLastUpdate(lastUpdate);
}
update(dbNote: DbNote) {
this.#setValue(dbNote.value);
this.#setLastUpdate(dbNote.lastUpdate);
}
constructor(dbNote: DbNote) {
console.debug(dbNote, dbNote.lastUpdate.toDate().toISOString())
this.id = dbNote.id;
this.#valueSignal = createSignal(dbNote.value);
this.#lastUpdatedSignal = createSignal(dbNote.lastUpdate);
}
}

47
client/src/library.ts Normal file
View file

@ -0,0 +1,47 @@
import { type Signal, createSignal } from "solid-js";
import type { Note as DatabaseNote } from "./bindings";
import type { Timestamp } from "@clockworklabs/spacetimedb-sdk";
type ValueType = string;
export class Note {
#valueSignal: Signal<ValueType>;
#lastUpdatedSignal: Signal<Timestamp>;
id: number;
#getValue(): ValueType {
return this.#valueSignal[0]();
}
#setValue(value: ValueType): ValueType {
return this.#valueSignal[1](value);
}
value(value?: ValueType): ValueType {
if (value === undefined) {
return this.#getValue();
}
return this.#setValue(value);
}
#getLastUpdate(): Timestamp {
return this.#lastUpdatedSignal[0]();
}
#setLastUpdate(lastUpdate: Timestamp): Timestamp {
return this.#lastUpdatedSignal[1](lastUpdate);
}
lastUpdate(lastUpdate?: Timestamp): Timestamp {
if (lastUpdate === undefined) {
return this.#getLastUpdate();
}
return this.#setLastUpdate(lastUpdate);
}
update(databaseNote: DatabaseNote) {
this.#setValue(databaseNote.value);
this.#setLastUpdate(databaseNote.lastUpdate);
}
constructor(databaseNote: DatabaseNote) {
console.debug(databaseNote, databaseNote.lastUpdate.toDate().toISOString());
this.id = databaseNote.id;
this.#valueSignal = createSignal(databaseNote.value);
this.#lastUpdatedSignal = createSignal(databaseNote.lastUpdate);
}
}

View file

@ -1,27 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View file

@ -1,7 +1,25 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View file

@ -1,24 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View file

@ -1,7 +1,5 @@
import { defineConfig } from 'vite'
import oxlint from 'vite-plugin-oxlint'
import solid from 'vite-plugin-solid'
import { defineConfig } from "vite";
import oxlint from "vite-plugin-oxlint";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid(), oxlint()],
})
export default defineConfig({ plugins: [solid(), oxlint()] });

View file

@ -75,6 +75,7 @@
pnpm
nodejs
oxlint
eslint
];
};
}