From a91d358263bb6ff7f30782d2fe51f11816e69508 Mon Sep 17 00:00:00 2001 From: vegard Date: Tue, 17 Mar 2026 13:55:37 +0100 Subject: [PATCH] STDB WebSocket-klient med reaktive Svelte-stores (oppgave 3.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend kobler til SpacetimeDB via WebSocket og abonnerer på node- og edge-tabellene. Data eksponeres som reaktive Svelte 5-stores (runes) som oppdateres automatisk ved insert/update/delete. Implementering: - spacetimedb SDK (npm) + genererte TypeScript-bindings - connection.svelte.ts: tilkoblingsmanager med reaktiv state - stores.svelte.ts: nodeStore og edgeStore med sekundærindekser (bySource, byTarget, byKind, byType osv.) - Layout initialiserer tilkobling ved autentisering - Hjemmesiden viser tilkoblingsstatus og antall noder/edges - .env.example med VITE_SPACETIMEDB_URL og VITE_SPACETIMEDB_MODULE Co-Authored-By: Claude Opus 4.6 --- frontend/.env.example | 10 + frontend/package-lock.json | 140 ++++++++++- frontend/package.json | 3 +- .../src/lib/spacetime/connection.svelte.ts | 104 +++++++++ frontend/src/lib/spacetime/index.ts | 10 + .../module_bindings/clear_all_reducer.ts | 13 ++ .../module_bindings/create_edge_reducer.ts | 21 ++ .../module_bindings/create_node_reducer.ts | 21 ++ .../module_bindings/delete_edge_reducer.ts | 15 ++ .../module_bindings/delete_node_reducer.ts | 15 ++ .../spacetime/module_bindings/edge_table.ts | 22 ++ .../lib/spacetime/module_bindings/index.ts | 149 ++++++++++++ .../spacetime/module_bindings/node_table.ts | 22 ++ .../lib/spacetime/module_bindings/types.ts | 36 +++ .../module_bindings/types/procedures.ts | 10 + .../module_bindings/types/reducers.ts | 24 ++ .../module_bindings/update_edge_reducer.ts | 17 ++ .../module_bindings/update_node_reducer.ts | 20 ++ frontend/src/lib/spacetime/stores.svelte.ts | 217 ++++++++++++++++++ frontend/src/routes/+layout.svelte | 13 ++ frontend/src/routes/+page.svelte | 33 ++- tasks.md | 3 +- 22 files changed, 908 insertions(+), 10 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/src/lib/spacetime/connection.svelte.ts create mode 100644 frontend/src/lib/spacetime/index.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/edge_table.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/index.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/node_table.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/types.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/types/procedures.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/types/reducers.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts create mode 100644 frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts create mode 100644 frontend/src/lib/spacetime/stores.svelte.ts diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..cbd800d --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,10 @@ +# Authentik OIDC +AUTHENTIK_ISSUER=https://auth.sidelinja.org/application/o/sidelinja/ +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= +AUTH_SECRET= +AUTH_TRUST_HOST=true + +# SpacetimeDB (sanntids WebSocket-tilkobling) +VITE_SPACETIMEDB_URL=wss://sidelinja.org/spacetime +VITE_SPACETIMEDB_MODULE=synops diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7393ecb..6463ed7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "dependencies": { "@auth/core": "^0.34.3", - "@auth/sveltekit": "^1.11.1" + "@auth/sveltekit": "^1.11.1", + "spacetimedb": "^2.0.4" }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", @@ -1510,6 +1511,26 @@ "node": ">= 0.4" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1715,6 +1736,12 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2086,6 +2113,18 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2171,12 +2210,43 @@ "preact": ">=10" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "license": "MIT" }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2269,6 +2339,15 @@ "node": ">=6" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", @@ -2298,6 +2377,59 @@ "node": ">=0.10.0" } }, + "node_modules/spacetimedb": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/spacetimedb/-/spacetimedb-2.0.4.tgz", + "integrity": "sha512-7GiZerC9SKXvHvcaOLCgLEjFL4XOcG30k5f/ogA9QqBuD+tO/om6DfhIplo5dUg976gfqxr3Hsp5XtY2pPSCKw==", + "license": "ISC", + "dependencies": { + "base64-js": "^1.5.1", + "headers-polyfill": "^4.0.3", + "object-inspect": "^1.13.4", + "prettier": "^3.3.3", + "pure-rand": "^7.0.1", + "safe-stable-stringify": "^2.5.0", + "statuses": "^2.0.2", + "url-polyfill": "^1.1.14" + }, + "peerDependencies": { + "@angular/core": ">=17.0.0", + "@tanstack/react-query": "^5.0.0", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "undici": "^6.19.2", + "vue": "^3.3.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@tanstack/react-query": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "undici": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2422,6 +2554,12 @@ "node": ">=14.17" } }, + "node_modules/url-polyfill": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz", + "integrity": "sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==", + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0dfcbe2..541f0c6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@auth/core": "^0.34.3", - "@auth/sveltekit": "^1.11.1" + "@auth/sveltekit": "^1.11.1", + "spacetimedb": "^2.0.4" } } diff --git a/frontend/src/lib/spacetime/connection.svelte.ts b/frontend/src/lib/spacetime/connection.svelte.ts new file mode 100644 index 0000000..dad524f --- /dev/null +++ b/frontend/src/lib/spacetime/connection.svelte.ts @@ -0,0 +1,104 @@ +/** + * SpacetimeDB connection manager with reactive state. + * + * Establishes WebSocket connection to SpacetimeDB, + * subscribes to nodes and edges tables, binds reactive stores. + * + * Usage: + * import { stdb, connectionState } from '$lib/spacetime/connection.svelte'; + * stdb.connect(); + */ + +import { DbConnection, type SubscriptionEventContext, type ErrorContext } from './module_bindings'; +import { bindStores } from './stores.svelte'; + +export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error'; + +let connection: DbConnection | null = null; +let _state = $state('disconnected'); + +/** Reactive connection state. Use connectionState.current in components. */ +export const connectionState = { + get current() { + return _state; + }, +}; + +/** + * Connect to SpacetimeDB and subscribe to nodes+edges. + * Token is optional — anonymous connections are supported. + */ +function connect(token?: string): DbConnection { + if (connection) return connection; + + const url = import.meta.env.VITE_SPACETIMEDB_URL; + const moduleName = import.meta.env.VITE_SPACETIMEDB_MODULE || 'synops'; + + if (!url) { + console.error('[stdb] VITE_SPACETIMEDB_URL not configured'); + _state = 'error'; + throw new Error('VITE_SPACETIMEDB_URL not configured'); + } + + _state = 'connecting'; + + const builder = DbConnection.builder() + .withUri(url) + .withDatabaseName(moduleName) + .onConnect((conn: DbConnection) => { + console.log('[stdb] Connected'); + _state = 'connected'; + + // Bind reactive stores to table callbacks + bindStores(conn); + + // Subscribe to all nodes and edges + conn.subscriptionBuilder() + .onApplied((_ctx: SubscriptionEventContext) => { + console.log('[stdb] Subscription applied — initial data loaded'); + }) + .onError((ctx: ErrorContext) => { + console.error('[stdb] Subscription error:', ctx); + }) + .subscribe([ + 'SELECT * FROM node', + 'SELECT * FROM edge', + ]); + }) + .onConnectError((_ctx: ErrorContext, err: Error) => { + console.error('[stdb] Connection error:', err); + _state = 'error'; + }) + .onDisconnect((_ctx: ErrorContext, error?: Error) => { + console.log('[stdb] Disconnected', error ?? ''); + connection = null; + _state = 'disconnected'; + }); + + if (token) { + builder.withToken(token); + } + + connection = builder.build(); + return connection; +} + +/** Disconnect and clean up. */ +function disconnect() { + if (connection) { + connection.disconnect(); + connection = null; + _state = 'disconnected'; + } +} + +/** Get the current connection (null if not connected). */ +function getConnection(): DbConnection | null { + return connection; +} + +export const stdb = { + connect, + disconnect, + getConnection, +}; diff --git a/frontend/src/lib/spacetime/index.ts b/frontend/src/lib/spacetime/index.ts new file mode 100644 index 0000000..ef6daf9 --- /dev/null +++ b/frontend/src/lib/spacetime/index.ts @@ -0,0 +1,10 @@ +/** + * Re-exports for convenient imports. + * + * Usage: + * import { stdb, connectionState, nodeStore, edgeStore } from '$lib/spacetime'; + */ + +export { stdb, connectionState } from './connection.svelte'; +export { nodeStore, edgeStore } from './stores.svelte'; +export type { Node, Edge } from './module_bindings/types'; diff --git a/frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts b/frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts b/frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts new file mode 100644 index 0000000..75105cf --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + id: __t.string(), + sourceId: __t.string(), + targetId: __t.string(), + edgeType: __t.string(), + metadata: __t.string(), + system: __t.bool(), + createdBy: __t.string(), +}; diff --git a/frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts b/frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts new file mode 100644 index 0000000..f1f8ee2 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + id: __t.string(), + nodeKind: __t.string(), + title: __t.string(), + content: __t.string(), + visibility: __t.string(), + metadata: __t.string(), + createdBy: __t.string(), +}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts new file mode 100644 index 0000000..ab36a29 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + id: __t.string(), +}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts new file mode 100644 index 0000000..ab36a29 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + id: __t.string(), +}; diff --git a/frontend/src/lib/spacetime/module_bindings/edge_table.ts b/frontend/src/lib/spacetime/module_bindings/edge_table.ts new file mode 100644 index 0000000..6fcff6c --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/edge_table.ts @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + id: __t.string().primaryKey(), + sourceId: __t.string().name("source_id"), + targetId: __t.string().name("target_id"), + edgeType: __t.string().name("edge_type"), + metadata: __t.string(), + system: __t.bool(), + createdAt: __t.timestamp().name("created_at"), + createdBy: __t.string().name("created_by"), +}); diff --git a/frontend/src/lib/spacetime/module_bindings/index.ts b/frontend/src/lib/spacetime/module_bindings/index.ts new file mode 100644 index 0000000..e49a151 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/index.ts @@ -0,0 +1,149 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +// This was generated using spacetimedb cli version 2.0.5 (commit d60138999206c06c776829072f46b5d1c1101f7e). + +/* eslint-disable */ +/* tslint:disable */ +import { + DbConnectionBuilder as __DbConnectionBuilder, + DbConnectionImpl as __DbConnectionImpl, + SubscriptionBuilderImpl as __SubscriptionBuilderImpl, + TypeBuilder as __TypeBuilder, + Uuid as __Uuid, + convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, + procedureSchema as __procedureSchema, + procedures as __procedures, + reducerSchema as __reducerSchema, + reducers as __reducers, + schema as __schema, + t as __t, + table as __table, + type AlgebraicTypeType as __AlgebraicTypeType, + type DbConnectionConfig as __DbConnectionConfig, + type ErrorContextInterface as __ErrorContextInterface, + type Event as __Event, + type EventContextInterface as __EventContextInterface, + type Infer as __Infer, + type QueryBuilder as __QueryBuilder, + type ReducerEventContextInterface as __ReducerEventContextInterface, + type RemoteModule as __RemoteModule, + type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, + type SubscriptionHandleImpl as __SubscriptionHandleImpl, +} from "spacetimedb"; + +// Import all reducer arg schemas +import ClearAllReducer from "./clear_all_reducer"; +import CreateEdgeReducer from "./create_edge_reducer"; +import CreateNodeReducer from "./create_node_reducer"; +import DeleteEdgeReducer from "./delete_edge_reducer"; +import DeleteNodeReducer from "./delete_node_reducer"; +import UpdateEdgeReducer from "./update_edge_reducer"; +import UpdateNodeReducer from "./update_node_reducer"; + +// Import all procedure arg schemas + +// Import all table schema definitions +import EdgeRow from "./edge_table"; +import NodeRow from "./node_table"; + +/** Type-only namespace exports for generated type groups. */ + +/** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ +const tablesSchema = __schema({ + edge: __table({ + name: 'edge', + indexes: [ + { accessor: 'id', name: 'edge_id_idx_btree', algorithm: 'btree', columns: [ + 'id', + ] }, + { accessor: 'source_id', name: 'edge_source_id_idx_btree', algorithm: 'btree', columns: [ + 'sourceId', + ] }, + { accessor: 'target_id', name: 'edge_target_id_idx_btree', algorithm: 'btree', columns: [ + 'targetId', + ] }, + ], + constraints: [ + { name: 'edge_id_key', constraint: 'unique', columns: ['id'] }, + ], + }, EdgeRow), + node: __table({ + name: 'node', + indexes: [ + { accessor: 'id', name: 'node_id_idx_btree', algorithm: 'btree', columns: [ + 'id', + ] }, + ], + constraints: [ + { name: 'node_id_key', constraint: 'unique', columns: ['id'] }, + ], + }, NodeRow), +}); + +/** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ +const reducersSchema = __reducers( + __reducerSchema("clear_all", ClearAllReducer), + __reducerSchema("create_edge", CreateEdgeReducer), + __reducerSchema("create_node", CreateNodeReducer), + __reducerSchema("delete_edge", DeleteEdgeReducer), + __reducerSchema("delete_node", DeleteNodeReducer), + __reducerSchema("update_edge", UpdateEdgeReducer), + __reducerSchema("update_node", UpdateNodeReducer), +); + +/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ +const proceduresSchema = __procedures( +); + +/** The remote SpacetimeDB module schema, both runtime and type information. */ +const REMOTE_MODULE = { + versionInfo: { + cliVersion: "2.0.5" as const, + }, + tables: tablesSchema.schemaType.tables, + reducers: reducersSchema.reducersType.reducers, + ...proceduresSchema, +} satisfies __RemoteModule< + typeof tablesSchema.schemaType, + typeof reducersSchema.reducersType, + typeof proceduresSchema +>; + +/** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ +export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); + +/** The reducers available in this remote SpacetimeDB module. */ +export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); + +/** The context type returned in callbacks for all possible events. */ +export type EventContext = __EventContextInterface; +/** The context type returned in callbacks for reducer events. */ +export type ReducerEventContext = __ReducerEventContextInterface; +/** The context type returned in callbacks for subscription events. */ +export type SubscriptionEventContext = __SubscriptionEventContextInterface; +/** The context type returned in callbacks for error events. */ +export type ErrorContext = __ErrorContextInterface; +/** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ +export type SubscriptionHandle = __SubscriptionHandleImpl; + +/** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ +export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} + +/** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ +export class DbConnectionBuilder extends __DbConnectionBuilder {} + +/** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ +export class DbConnection extends __DbConnectionImpl { + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ + static builder = (): DbConnectionBuilder => { + return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); + }; + + /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ + override subscriptionBuilder = (): SubscriptionBuilder => { + return new SubscriptionBuilder(this); + }; +} + diff --git a/frontend/src/lib/spacetime/module_bindings/node_table.ts b/frontend/src/lib/spacetime/module_bindings/node_table.ts new file mode 100644 index 0000000..8715992 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/node_table.ts @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + id: __t.string().primaryKey(), + nodeKind: __t.string().name("node_kind"), + title: __t.string(), + content: __t.string(), + visibility: __t.string(), + metadata: __t.string(), + createdAt: __t.timestamp().name("created_at"), + createdBy: __t.string().name("created_by"), +}); diff --git a/frontend/src/lib/spacetime/module_bindings/types.ts b/frontend/src/lib/spacetime/module_bindings/types.ts new file mode 100644 index 0000000..dc11d5a --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/types.ts @@ -0,0 +1,36 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export const Edge = __t.object("Edge", { + id: __t.string(), + sourceId: __t.string(), + targetId: __t.string(), + edgeType: __t.string(), + metadata: __t.string(), + system: __t.bool(), + createdAt: __t.timestamp(), + createdBy: __t.string(), +}); +export type Edge = __Infer; + +export const Node = __t.object("Node", { + id: __t.string(), + nodeKind: __t.string(), + title: __t.string(), + content: __t.string(), + visibility: __t.string(), + metadata: __t.string(), + createdAt: __t.timestamp(), + createdBy: __t.string(), +}); +export type Node = __Infer; + diff --git a/frontend/src/lib/spacetime/module_bindings/types/procedures.ts b/frontend/src/lib/spacetime/module_bindings/types/procedures.ts new file mode 100644 index 0000000..d5ac825 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/types/procedures.ts @@ -0,0 +1,10 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { type Infer as __Infer } from "spacetimedb"; + +// Import all procedure arg schemas + + diff --git a/frontend/src/lib/spacetime/module_bindings/types/reducers.ts b/frontend/src/lib/spacetime/module_bindings/types/reducers.ts new file mode 100644 index 0000000..c15d1c7 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/types/reducers.ts @@ -0,0 +1,24 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { type Infer as __Infer } from "spacetimedb"; + +// Import all reducer arg schemas +import ClearAllReducer from "../clear_all_reducer"; +import CreateEdgeReducer from "../create_edge_reducer"; +import CreateNodeReducer from "../create_node_reducer"; +import DeleteEdgeReducer from "../delete_edge_reducer"; +import DeleteNodeReducer from "../delete_node_reducer"; +import UpdateEdgeReducer from "../update_edge_reducer"; +import UpdateNodeReducer from "../update_node_reducer"; + +export type ClearAllParams = __Infer; +export type CreateEdgeParams = __Infer; +export type CreateNodeParams = __Infer; +export type DeleteEdgeParams = __Infer; +export type DeleteNodeParams = __Infer; +export type UpdateEdgeParams = __Infer; +export type UpdateNodeParams = __Infer; + diff --git a/frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts b/frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts new file mode 100644 index 0000000..4a0d616 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + id: __t.string(), + edgeType: __t.string(), + metadata: __t.string(), +}; diff --git a/frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts b/frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts new file mode 100644 index 0000000..0383185 --- /dev/null +++ b/frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + id: __t.string(), + nodeKind: __t.string(), + title: __t.string(), + content: __t.string(), + visibility: __t.string(), + metadata: __t.string(), +}; diff --git a/frontend/src/lib/spacetime/stores.svelte.ts b/frontend/src/lib/spacetime/stores.svelte.ts new file mode 100644 index 0000000..2c7596f --- /dev/null +++ b/frontend/src/lib/spacetime/stores.svelte.ts @@ -0,0 +1,217 @@ +/** + * Reactive Svelte 5 stores for SpacetimeDB nodes and edges. + * + * Uses Svelte 5 runes ($state) for fine-grained reactivity. + * Subscribes to table callbacks (onInsert/onDelete/onUpdate) and + * maintains Maps keyed by id for O(1) lookups. + * + * Usage: + * import { nodeStore, edgeStore } from '$lib/spacetime/stores.svelte'; + * + * // In a .svelte component: + * const nodes = $derived(nodeStore.all); + * const myEdges = $derived(edgeStore.bySource('user-123')); + */ + +import type { Node, Edge } from './module_bindings/types'; +import type { DbConnection, EventContext } from './module_bindings'; + +// --------------------------------------------------------------------------- +// Node store +// --------------------------------------------------------------------------- + +let _nodes = $state>(new Map()); +let _nodeVersion = $state(0); + +function createNodeStore() { + return { + /** All nodes as an array. */ + get all(): Node[] { + void _nodeVersion; + return [..._nodes.values()]; + }, + + /** Number of nodes. */ + get count(): number { + void _nodeVersion; + return _nodes.size; + }, + + /** Get a node by id. */ + get(id: string): Node | undefined { + void _nodeVersion; + return _nodes.get(id); + }, + + /** Get nodes filtered by node_kind. */ + byKind(kind: string): Node[] { + void _nodeVersion; + return [..._nodes.values()].filter((n) => n.nodeKind === kind); + }, + + /** Get nodes created by a specific user. */ + byCreator(userId: string): Node[] { + void _nodeVersion; + return [..._nodes.values()].filter((n) => n.createdBy === userId); + }, + + // -- Internal callbacks for SpacetimeDB -- + _onInsert(_ctx: EventContext, row: Node) { + _nodes.set(row.id, row); + _nodeVersion++; + }, + _onDelete(_ctx: EventContext, row: Node) { + _nodes.delete(row.id); + _nodeVersion++; + }, + _onUpdate(_ctx: EventContext, _oldRow: Node, newRow: Node) { + _nodes.set(newRow.id, newRow); + _nodeVersion++; + }, + _clear() { + _nodes = new Map(); + _nodeVersion++; + }, + }; +} + +export const nodeStore = createNodeStore(); + +// --------------------------------------------------------------------------- +// Edge store +// --------------------------------------------------------------------------- + +let _edges = $state>(new Map()); +let _edgeVersion = $state(0); + +// Secondary indexes for fast lookups +let _edgesBySource = $state>>(new Map()); +let _edgesByTarget = $state>>(new Map()); + +function addToIndex(index: Map>, key: string, edgeId: string) { + let set = index.get(key); + if (!set) { + set = new Set(); + index.set(key, set); + } + set.add(edgeId); +} + +function removeFromIndex(index: Map>, key: string, edgeId: string) { + const set = index.get(key); + if (set) { + set.delete(edgeId); + if (set.size === 0) index.delete(key); + } +} + +function createEdgeStore() { + return { + /** All edges as an array. */ + get all(): Edge[] { + void _edgeVersion; + return [..._edges.values()]; + }, + + /** Number of edges. */ + get count(): number { + void _edgeVersion; + return _edges.size; + }, + + /** Get an edge by id. */ + get(id: string): Edge | undefined { + void _edgeVersion; + return _edges.get(id); + }, + + /** Get all edges originating from a source node. */ + bySource(sourceId: string): Edge[] { + void _edgeVersion; + const ids = _edgesBySource.get(sourceId); + if (!ids) return []; + return [...ids].map((id) => _edges.get(id)!).filter(Boolean); + }, + + /** Get all edges pointing to a target node. */ + byTarget(targetId: string): Edge[] { + void _edgeVersion; + const ids = _edgesByTarget.get(targetId); + if (!ids) return []; + return [...ids].map((id) => _edges.get(id)!).filter(Boolean); + }, + + /** Get edges of a specific type. */ + byType(edgeType: string): Edge[] { + void _edgeVersion; + return [..._edges.values()].filter((e) => e.edgeType === edgeType); + }, + + /** Get edges between two specific nodes (any direction). */ + between(nodeA: string, nodeB: string): Edge[] { + void _edgeVersion; + return [..._edges.values()].filter( + (e) => + (e.sourceId === nodeA && e.targetId === nodeB) || + (e.sourceId === nodeB && e.targetId === nodeA), + ); + }, + + // -- Internal callbacks for SpacetimeDB -- + _onInsert(_ctx: EventContext, row: Edge) { + _edges.set(row.id, row); + addToIndex(_edgesBySource, row.sourceId, row.id); + addToIndex(_edgesByTarget, row.targetId, row.id); + _edgeVersion++; + }, + _onDelete(_ctx: EventContext, row: Edge) { + _edges.delete(row.id); + removeFromIndex(_edgesBySource, row.sourceId, row.id); + removeFromIndex(_edgesByTarget, row.targetId, row.id); + _edgeVersion++; + }, + _onUpdate(_ctx: EventContext, oldRow: Edge, newRow: Edge) { + if (oldRow.sourceId !== newRow.sourceId) { + removeFromIndex(_edgesBySource, oldRow.sourceId, oldRow.id); + addToIndex(_edgesBySource, newRow.sourceId, newRow.id); + } + if (oldRow.targetId !== newRow.targetId) { + removeFromIndex(_edgesByTarget, oldRow.targetId, oldRow.id); + addToIndex(_edgesByTarget, newRow.targetId, newRow.id); + } + _edges.set(newRow.id, newRow); + _edgeVersion++; + }, + _clear() { + _edges = new Map(); + _edgesBySource = new Map(); + _edgesByTarget = new Map(); + _edgeVersion++; + }, + }; +} + +export const edgeStore = createEdgeStore(); + +// --------------------------------------------------------------------------- +// Bind stores to a DbConnection +// --------------------------------------------------------------------------- + +/** + * Register table callbacks on a DbConnection. + * Called by connection manager after connect. + */ +export function bindStores(conn: DbConnection) { + // Clear any stale data + nodeStore._clear(); + edgeStore._clear(); + + // Register callbacks + conn.db.node.onInsert(nodeStore._onInsert); + conn.db.node.onDelete(nodeStore._onDelete); + conn.db.node.onUpdate(nodeStore._onUpdate); + + conn.db.edge.onInsert(edgeStore._onInsert); + conn.db.edge.onDelete(edgeStore._onDelete); + conn.db.edge.onUpdate(edgeStore._onUpdate); +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index b93e9ba..c13a297 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,7 +1,20 @@ {@render children()} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 5a83df8..9d9bf0f 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,6 +1,7 @@
@@ -11,12 +12,32 @@

Innlogget som {$page.data.session.user.name ?? $page.data.session.user.email}

- + +
+

+ SpacetimeDB: + + {connectionState.current} + +

+ {#if connectionState.current === 'connected'} +

+ {nodeStore.count} noder · {edgeStore.count} edges +

+ {/if} +
+ +
+ +
{/if}
diff --git a/tasks.md b/tasks.md index c72a34a..cbb9a3c 100644 --- a/tasks.md +++ b/tasks.md @@ -62,8 +62,7 @@ Uavhengige faser kan fortsatt plukkes. - [x] 3.1 SvelteKit-prosjekt: opprett `frontend/` med TypeScript, TailwindCSS. PWA-manifest. Lokal dev med HMR. - [x] 3.2 Authentik login: OIDC-flow (authorization code + PKCE). Session-håndtering. Redirect til login ved 401. -- [~] 3.3 STDB WebSocket-klient: abonner på noder og edges. Reaktiv Svelte-store som oppdateres ved endringer. - > Påbegynt: 2026-03-17T13:46 +- [x] 3.3 STDB WebSocket-klient: abonner på noder og edges. Reaktiv Svelte-store som oppdateres ved endringer. - [ ] 3.4 Mottaksflaten v0: vis noder med edge til innlogget bruker, sortert på `created_at`. Enkel liste med tittel og utdrag. - [ ] 3.5 TipTap-editor: enkel preset (tekst, markdown, lenker). Send `create_node`-intensjon til maskinrommet ved submit. - [ ] 3.6 Sanntidstest: åpne to faner, skriv i én, se noden dukke opp i den andre via STDB.