Feat/notes init (#40)

* feat: notes init

* Feat/notes page (#39)

* wip: notes page ui added

* notes page basic ui done

* wip: page UI

* minor cleanup

* todo comments added for easy ref for backend wiring

---------

Co-authored-by: amadeus-x1 <45001978+amadeus-x1@users.noreply.github.com>

* feat: wire up notes

---------

Co-authored-by: amadeus-x1 <45001978+amadeus-x1@users.noreply.github.com>
This commit is contained in:
Haritabh 2025-09-10 16:13:33 +05:30 committed by GitHub
parent 674092f1b7
commit a128ec7972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 2980 additions and 10 deletions

View file

@ -0,0 +1,240 @@
import * as Y from "yjs";
import { Client, createClient } from "@libsql/client";
export interface LibSQLPersistenceOptions {
url?: string;
authToken?: string;
client?: Client;
}
export class LibSQLPersistence {
private doc: Y.Doc;
private docName: string;
private client: Client;
private _synced: boolean = false;
private _destroyed: boolean = false;
private whenSynced: Promise<void>;
private _resolveSynced!: () => void;
private _storeUpdateHandler: (update: Uint8Array, origin: any) => void;
private meta: Map<string, any> = new Map();
constructor(
docName: string,
ydoc: Y.Doc,
options: LibSQLPersistenceOptions = {},
) {
this.doc = ydoc;
this.docName = docName;
// Initialize client
if (options.client) {
this.client = options.client;
} else if (options.url) {
this.client = createClient({
url: options.url,
authToken: options.authToken,
});
} else {
// Default to local file
this.client = createClient({
url: "file:local.db",
});
}
// Create promise for sync status
this.whenSynced = new Promise((resolve) => {
this._resolveSynced = resolve;
});
// Bind the update handler
this._storeUpdateHandler = this._storeUpdate.bind(this);
// Initialize the database and load existing data
this._initialize();
}
private async _initialize() {
try {
// Create tables if they don't exist
await this._createTables();
// Load existing updates
await this._loadUpdates();
// Listen for document updates
this.doc.on("update", this._storeUpdateHandler);
// Mark as synced
this._synced = true;
this._resolveSynced();
} catch (error) {
console.error("Failed to initialize LibSQLPersistence:", error);
throw error;
}
}
private async _createTables() {
// Create updates table
await this.client.execute(`
CREATE TABLE IF NOT EXISTS yjs_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_name TEXT NOT NULL,
update_data BLOB NOT NULL,
created_at INTEGER DEFAULT (unixepoch()),
INDEX idx_doc_name (doc_name)
)
`);
// Create metadata table
await this.client.execute(`
CREATE TABLE IF NOT EXISTS yjs_metadata (
doc_name TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT,
PRIMARY KEY (doc_name, key)
)
`);
}
private async _loadUpdates() {
// Fetch all updates for this document
const result = await this.client.execute({
sql: "SELECT update_data FROM yjs_updates WHERE doc_name = ? ORDER BY id",
args: [this.docName],
});
if (result.rows.length > 0) {
// Apply updates to the document
Y.transact(
this.doc,
() => {
for (const row of result.rows) {
const updateData = row.update_data;
if (updateData instanceof ArrayBuffer) {
Y.applyUpdate(this.doc, new Uint8Array(updateData), this);
} else if (typeof updateData === "string") {
// Handle base64 encoded data
const binaryString = atob(updateData);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
Y.applyUpdate(this.doc, bytes, this);
}
}
},
this,
);
}
// Load metadata
const metaResult = await this.client.execute({
sql: "SELECT key, value FROM yjs_metadata WHERE doc_name = ?",
args: [this.docName],
});
for (const row of metaResult.rows) {
const key = row.key as string;
const value = row.value as string;
try {
this.meta.set(key, JSON.parse(value));
} catch {
this.meta.set(key, value);
}
}
}
private async _storeUpdate(update: Uint8Array, origin: any) {
// Don't store updates that originated from this provider
if (origin === this || this._destroyed) {
return;
}
try {
// Convert Uint8Array to base64 for storage
const base64Update = btoa(String.fromCharCode(...update));
await this.client.execute({
sql: "INSERT INTO yjs_updates (doc_name, update_data) VALUES (?, ?)",
args: [this.docName, base64Update],
});
} catch (error) {
console.error("Failed to store update:", error);
}
}
async set(key: string, value: any): Promise<void> {
this.meta.set(key, value);
const jsonValue = JSON.stringify(value);
await this.client.execute({
sql: `
INSERT INTO yjs_metadata (doc_name, key, value)
VALUES (?, ?, ?)
ON CONFLICT(doc_name, key)
DO UPDATE SET value = excluded.value
`,
args: [this.docName, key, jsonValue],
});
}
get(key: string): any {
return this.meta.get(key);
}
async del(key: string): Promise<void> {
this.meta.delete(key);
await this.client.execute({
sql: "DELETE FROM yjs_metadata WHERE doc_name = ? AND key = ?",
args: [this.docName, key],
});
}
async clearData(): Promise<void> {
// Clear all data for this document
await this.client.execute({
sql: "DELETE FROM yjs_updates WHERE doc_name = ?",
args: [this.docName],
});
await this.client.execute({
sql: "DELETE FROM yjs_metadata WHERE doc_name = ?",
args: [this.docName],
});
this.meta.clear();
}
async compactUpdates(): Promise<void> {
// Get the current state as a single update
const stateUpdate = Y.encodeStateAsUpdate(this.doc);
// Clear old updates
await this.client.execute({
sql: "DELETE FROM yjs_updates WHERE doc_name = ?",
args: [this.docName],
});
// Store the compacted update
const base64Update = btoa(String.fromCharCode(...stateUpdate));
await this.client.execute({
sql: "INSERT INTO yjs_updates (doc_name, update_data) VALUES (?, ?)",
args: [this.docName, base64Update],
});
}
destroy(): void {
if (this._destroyed) return;
this._destroyed = true;
this.doc.off("update", this._storeUpdateHandler);
}
get synced(): boolean {
return this._synced;
}
}
// Export for convenience
export { Y };