applesauce-sqlite 0.0.0-next-20250915145415

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/better-sqlite3/event-database.d.ts +52 -0
  4. package/dist/better-sqlite3/event-database.js +104 -0
  5. package/dist/better-sqlite3/index.d.ts +2 -0
  6. package/dist/better-sqlite3/index.js +2 -0
  7. package/dist/better-sqlite3/methods.d.ts +31 -0
  8. package/dist/better-sqlite3/methods.js +149 -0
  9. package/dist/bun/event-database.d.ts +52 -0
  10. package/dist/bun/event-database.js +105 -0
  11. package/dist/bun/index.d.ts +2 -0
  12. package/dist/bun/index.js +2 -0
  13. package/dist/bun/methods.d.ts +31 -0
  14. package/dist/bun/methods.js +152 -0
  15. package/dist/helpers/index.d.ts +3 -0
  16. package/dist/helpers/index.js +3 -0
  17. package/dist/helpers/search.d.ts +16 -0
  18. package/dist/helpers/search.js +60 -0
  19. package/dist/helpers/sql.d.ts +15 -0
  20. package/dist/helpers/sql.js +122 -0
  21. package/dist/helpers/sqlite.d.ts +66 -0
  22. package/dist/helpers/sqlite.js +367 -0
  23. package/dist/helpers/statements.d.ts +47 -0
  24. package/dist/helpers/statements.js +65 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/libsql/event-database.d.ts +54 -0
  28. package/dist/libsql/event-database.js +106 -0
  29. package/dist/libsql/index.d.ts +2 -0
  30. package/dist/libsql/index.js +2 -0
  31. package/dist/libsql/methods.d.ts +31 -0
  32. package/dist/libsql/methods.js +249 -0
  33. package/dist/native/event-database.d.ts +52 -0
  34. package/dist/native/event-database.js +104 -0
  35. package/dist/native/index.d.ts +2 -0
  36. package/dist/native/index.js +2 -0
  37. package/dist/native/methods.d.ts +31 -0
  38. package/dist/native/methods.js +174 -0
  39. package/dist/relay.d.ts +1 -0
  40. package/dist/relay.js +166 -0
  41. package/dist/sqlite-event-database.d.ts +53 -0
  42. package/dist/sqlite-event-database.js +105 -0
  43. package/package.json +113 -0
package/dist/relay.js ADDED
@@ -0,0 +1,166 @@
1
+ import { EventStore } from "applesauce-core";
2
+ import { matchFilter, verifyEvent } from "applesauce-core/helpers";
3
+ import { createServer } from "http";
4
+ import { WebSocket, WebSocketServer } from "ws";
5
+ import { BetterSqlite3EventDatabase } from "./better-sqlite3/event-database.js";
6
+ // Create the event store with SQLite backend
7
+ const database = new BetterSqlite3EventDatabase(process.env.DATABASE_PATH || ":memory:");
8
+ const eventStore = new EventStore(database);
9
+ // Set validation method for event store
10
+ eventStore.verifyEvent = verifyEvent;
11
+ const subscriptions = new Map();
12
+ // Create HTTP server and WebSocket server
13
+ const server = createServer();
14
+ const wss = new WebSocketServer({ server });
15
+ // Handle WebSocket connections
16
+ wss.on("connection", (ws) => {
17
+ console.log("New client connected");
18
+ ws.on("message", async (data) => {
19
+ try {
20
+ const message = JSON.parse(data.toString());
21
+ if (!Array.isArray(message) || message.length < 2) {
22
+ ws.send(JSON.stringify(["NOTICE", "Invalid message format"]));
23
+ return;
24
+ }
25
+ const [type, ...args] = message;
26
+ switch (type) {
27
+ case "EVENT":
28
+ await handleEvent(ws, args[0]);
29
+ break;
30
+ case "REQ":
31
+ await handleReq(ws, args[0], args.slice(1));
32
+ break;
33
+ case "CLOSE":
34
+ handleClose(ws, args[0]);
35
+ break;
36
+ default:
37
+ ws.send(JSON.stringify(["NOTICE", `Unknown message type: ${type}`]));
38
+ }
39
+ }
40
+ catch (error) {
41
+ console.error("Error processing message:", error);
42
+ ws.send(JSON.stringify(["NOTICE", "Error processing message"]));
43
+ }
44
+ });
45
+ ws.on("close", () => {
46
+ console.log("Client disconnected");
47
+ // Clean up subscriptions for this WebSocket
48
+ for (const [subId, sub] of subscriptions.entries()) {
49
+ if (sub.ws === ws) {
50
+ subscriptions.delete(subId);
51
+ }
52
+ }
53
+ });
54
+ ws.on("error", (error) => {
55
+ console.error("WebSocket error:", error);
56
+ });
57
+ });
58
+ // Handle EVENT messages
59
+ async function handleEvent(ws, event) {
60
+ try {
61
+ // Basic event validation
62
+ if (typeof event !== "object" || event === null)
63
+ throw new Error("invalid: event is not valid");
64
+ let added = null;
65
+ try {
66
+ added = eventStore.add(event);
67
+ }
68
+ catch (error) {
69
+ ws.send(JSON.stringify(["OK", event.id, false, "error: failed to validate event"]));
70
+ return;
71
+ }
72
+ if (!added) {
73
+ ws.send(JSON.stringify(["OK", event.id, false, "error: rejected event"]));
74
+ return;
75
+ }
76
+ if (added === event) {
77
+ // Its a new event because the current instance was returned
78
+ ws.send(JSON.stringify(["OK", event.id, true, ""]));
79
+ }
80
+ else {
81
+ // It was a duplicate because the "real" instance was returned
82
+ ws.send(JSON.stringify(["OK", event.id, true, "duplicate: event already exists"]));
83
+ }
84
+ // Broadcast to subscribers
85
+ broadcastToSubscribers(event);
86
+ }
87
+ catch (error) {
88
+ console.error("Error handling event:", error);
89
+ if (error instanceof Error)
90
+ ws.send(JSON.stringify(["OK", event.id, false, error.message]));
91
+ else
92
+ ws.send(JSON.stringify(["OK", event.id, false, "error: failed to process event"]));
93
+ }
94
+ }
95
+ // Handle REQ messages
96
+ async function handleReq(ws, subscriptionId, filters) {
97
+ try {
98
+ // Store subscription
99
+ subscriptions.set(subscriptionId, {
100
+ id: subscriptionId,
101
+ filters,
102
+ ws,
103
+ });
104
+ // Get existing events that match filters
105
+ const events = eventStore.getByFilters(filters);
106
+ // Send matching events
107
+ for (const event of events) {
108
+ ws.send(JSON.stringify(["EVENT", subscriptionId, event]));
109
+ }
110
+ // Send EOSE (End of Stored Events)
111
+ ws.send(JSON.stringify(["EOSE", subscriptionId]));
112
+ }
113
+ catch (error) {
114
+ console.error("Error handling REQ:", error);
115
+ ws.send(JSON.stringify(["NOTICE", "Error processing subscription"]));
116
+ }
117
+ }
118
+ // Handle CLOSE messages
119
+ function handleClose(ws, subscriptionId) {
120
+ const sub = subscriptions.get(subscriptionId);
121
+ if (sub && sub.ws === ws) {
122
+ subscriptions.delete(subscriptionId);
123
+ }
124
+ }
125
+ // Broadcast event to all subscribers with matching filters
126
+ function broadcastToSubscribers(event) {
127
+ for (const [subId, sub] of subscriptions.entries()) {
128
+ if (sub.ws.readyState === WebSocket.OPEN) {
129
+ // Skip this subscription if it has a search filter (cant match search filters)
130
+ if (sub.filters.some((filter) => filter.search))
131
+ continue;
132
+ // Check if event matches any of the subscription filters
133
+ const matches = sub.filters.some((filter) => matchFilter(filter, event));
134
+ if (matches)
135
+ sub.ws.send(JSON.stringify(["EVENT", subId, event]));
136
+ }
137
+ else {
138
+ // Clean up closed connections
139
+ subscriptions.delete(subId);
140
+ }
141
+ }
142
+ }
143
+ // Start the server
144
+ const PORT = process.env.PORT || 8080;
145
+ server.listen(PORT, () => {
146
+ console.log(`Nostr relay server listening on port ${PORT}`);
147
+ console.log(`WebSocket endpoint: ws://localhost:${PORT}`);
148
+ });
149
+ // Subscribe to new events from the event store to broadcast them
150
+ eventStore.insert$.subscribe((event) => {
151
+ broadcastToSubscribers(event);
152
+ });
153
+ // Graceful shutdown
154
+ process.on("SIGINT", () => {
155
+ console.log("\nShutting down relay server...");
156
+ wss.close(() => {
157
+ server.close(() => {
158
+ database.close();
159
+ process.exit(0);
160
+ });
161
+ });
162
+ });
163
+ console.log("🚀 Nostr relay started!");
164
+ console.log("📡 WebSocket endpoint: ws://localhost:" + (process.env.PORT || 8080));
165
+ console.log("💾 Database: " + (process.env.DATABASE_PATH || ":memory:"));
166
+ console.log("🛑 Press Ctrl+C to stop the server");
@@ -0,0 +1,53 @@
1
+ import { IEventDatabase } from "applesauce-core";
2
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
3
+ import { type Database as TDatabase } from "better-sqlite3";
4
+ import { defaultSearchContentFormatter, enhancedSearchContentFormatter, type SearchContentFormatter } from "./helpers/sqlite.js";
5
+ export { defaultSearchContentFormatter, enhancedSearchContentFormatter, type SearchContentFormatter };
6
+ /** Options for the SqliteEventDatabase */
7
+ export type SqliteEventDatabaseOptions = {
8
+ search?: boolean;
9
+ searchContentFormatter?: SearchContentFormatter;
10
+ };
11
+ export declare class SqliteEventDatabase implements IEventDatabase {
12
+ db: TDatabase;
13
+ /** If search is enabled */
14
+ private search;
15
+ /** The search content formatter */
16
+ private searchContentFormatter;
17
+ constructor(database?: string | TDatabase, options?: SqliteEventDatabaseOptions);
18
+ /** Store a Nostr event in the database */
19
+ add(event: NostrEvent): NostrEvent;
20
+ /** Delete an event by ID */
21
+ remove(id: string): boolean;
22
+ /** Checks if an event exists */
23
+ hasEvent(id: string): boolean;
24
+ /** Get an event by its ID */
25
+ getEvent(id: string): NostrEvent | undefined;
26
+ /** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
27
+ getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
28
+ /** Checks if a replaceable event exists */
29
+ hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
30
+ /** Returns all the versions of a replaceable event */
31
+ getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[];
32
+ /** Get all events that match the filters (supports NIP-50 search field) */
33
+ getByFilters(filters: (Filter & {
34
+ search?: string;
35
+ }) | (Filter & {
36
+ search?: string;
37
+ })[]): Set<NostrEvent>;
38
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
39
+ getTimeline(filters: (Filter & {
40
+ search?: string;
41
+ }) | (Filter & {
42
+ search?: string;
43
+ })[]): NostrEvent[];
44
+ /** Set the search content formatter */
45
+ setSearchContentFormatter(formatter: SearchContentFormatter): void;
46
+ /** Get the current search content formatter */
47
+ getSearchContentFormatter(): SearchContentFormatter;
48
+ /** Rebuild the search index for all events */
49
+ rebuildSearchIndex(): void;
50
+ /** Close the database connection */
51
+ close(): void;
52
+ [Symbol.dispose](): void;
53
+ }
@@ -0,0 +1,105 @@
1
+ import { logger } from "applesauce-core";
2
+ import { insertEventIntoDescendingList } from "applesauce-core/helpers";
3
+ import Database from "better-sqlite3";
4
+ import { createTables, defaultSearchContentFormatter, deleteEvent, enhancedSearchContentFormatter, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./helpers/sqlite.js";
5
+ const log = logger.extend("SqliteEventDatabase");
6
+ // Export the search content formatters and types for external use
7
+ export { defaultSearchContentFormatter, enhancedSearchContentFormatter };
8
+ export class SqliteEventDatabase {
9
+ db;
10
+ /** If search is enabled */
11
+ search;
12
+ /** The search content formatter */
13
+ searchContentFormatter;
14
+ constructor(database = ":memory:", options) {
15
+ this.db = typeof database === "string" ? new Database(database) : database;
16
+ this.search = options?.search ?? true;
17
+ this.searchContentFormatter = options?.searchContentFormatter ?? enhancedSearchContentFormatter;
18
+ // Setup the database tables and indexes
19
+ createTables(this.db, this.search);
20
+ }
21
+ /** Store a Nostr event in the database */
22
+ add(event) {
23
+ try {
24
+ insertEvent(this.db, event, this.search ? this.searchContentFormatter : undefined);
25
+ return event;
26
+ }
27
+ catch (error) {
28
+ log("Error inserting event:", error);
29
+ throw error;
30
+ }
31
+ }
32
+ /** Delete an event by ID */
33
+ remove(id) {
34
+ try {
35
+ // Remove event from database
36
+ return deleteEvent(this.db, id);
37
+ }
38
+ catch (error) {
39
+ return false;
40
+ }
41
+ }
42
+ /** Checks if an event exists */
43
+ hasEvent(id) {
44
+ return hasEvent(this.db, id);
45
+ }
46
+ /** Get an event by its ID */
47
+ getEvent(id) {
48
+ return getEvent(this.db, id);
49
+ }
50
+ /** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
51
+ getReplaceable(kind, pubkey, identifier = "") {
52
+ return getReplaceable(this.db, kind, pubkey, identifier);
53
+ }
54
+ /** Checks if a replaceable event exists */
55
+ hasReplaceable(kind, pubkey, identifier = "") {
56
+ return hasReplaceable(this.db, kind, pubkey, identifier);
57
+ }
58
+ /** Returns all the versions of a replaceable event */
59
+ getReplaceableHistory(kind, pubkey, identifier = "") {
60
+ return getReplaceableHistory(this.db, kind, pubkey, identifier);
61
+ }
62
+ /** Get all events that match the filters (supports NIP-50 search field) */
63
+ getByFilters(filters) {
64
+ try {
65
+ // If search is disabled, remove the search field from the filters
66
+ if (!this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
67
+ throw new Error("Search is disabled");
68
+ return getEventsByFilters(this.db, filters);
69
+ }
70
+ catch (error) {
71
+ return new Set();
72
+ }
73
+ }
74
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
75
+ getTimeline(filters) {
76
+ const events = this.getByFilters(filters);
77
+ const timeline = [];
78
+ for (const event of events)
79
+ insertEventIntoDescendingList(timeline, event);
80
+ return timeline;
81
+ }
82
+ /** Set the search content formatter */
83
+ setSearchContentFormatter(formatter) {
84
+ this.searchContentFormatter = formatter;
85
+ }
86
+ /** Get the current search content formatter */
87
+ getSearchContentFormatter() {
88
+ return this.searchContentFormatter;
89
+ }
90
+ /** Rebuild the search index for all events */
91
+ rebuildSearchIndex() {
92
+ if (!this.search)
93
+ throw new Error("Search is disabled");
94
+ rebuildSearchIndex(this.db, this.searchContentFormatter);
95
+ log("Search index rebuilt successfully");
96
+ }
97
+ /** Close the database connection */
98
+ close() {
99
+ log("Closing database connection");
100
+ this.db.close();
101
+ }
102
+ [Symbol.dispose]() {
103
+ this.close();
104
+ }
105
+ }
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "applesauce-sqlite",
3
+ "version": "0.0.0-next-20250915145415",
4
+ "description": "sqlite event databases for applesauce",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "keywords": [
9
+ "nostr",
10
+ "applesauce"
11
+ ],
12
+ "author": "hzrd149",
13
+ "license": "MIT",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.js",
21
+ "types": "./dist/index.d.ts"
22
+ },
23
+ "./helpers": {
24
+ "import": "./dist/helpers/index.js",
25
+ "require": "./dist/helpers/index.js",
26
+ "types": "./dist/helpers/index.d.ts"
27
+ },
28
+ "./helpers/*": {
29
+ "import": "./dist/helpers/*.js",
30
+ "require": "./dist/helpers/*.js",
31
+ "types": "./dist/helpers/*.d.ts"
32
+ },
33
+ "./better-sqlite3": {
34
+ "import": "./dist/better-sqlite3/index.js",
35
+ "require": "./dist/better-sqlite3/index.js",
36
+ "types": "./dist/better-sqlite3/index.d.ts"
37
+ },
38
+ "./better-sqlite3/*": {
39
+ "import": "./dist/better-sqlite3/*.js",
40
+ "require": "./dist/better-sqlite3/*.js",
41
+ "types": "./dist/better-sqlite3/*.d.ts"
42
+ },
43
+ "./native": {
44
+ "import": "./dist/native/index.js",
45
+ "require": "./dist/native/index.js",
46
+ "types": "./dist/native/index.d.ts"
47
+ },
48
+ "./native/*": {
49
+ "import": "./dist/native/*.js",
50
+ "require": "./dist/native/*.js",
51
+ "types": "./dist/native/*.d.ts"
52
+ },
53
+ "./deno": {
54
+ "import": "./dist/native/index.js",
55
+ "require": "./dist/native/index.js",
56
+ "types": "./dist/native/index.d.ts"
57
+ },
58
+ "./deno/*": {
59
+ "import": "./dist/native/*.js",
60
+ "require": "./dist/native/*.js",
61
+ "types": "./dist/native/*.d.ts"
62
+ },
63
+ "./bun": {
64
+ "import": "./dist/bun/index.js",
65
+ "require": "./dist/bun/index.js",
66
+ "types": "./dist/bun/index.d.ts"
67
+ },
68
+ "./bun/*": {
69
+ "import": "./dist/bun/*.js",
70
+ "require": "./dist/bun/*.js",
71
+ "types": "./dist/bun/*.d.ts"
72
+ },
73
+ "./libsql": {
74
+ "import": "./dist/libsql/index.js",
75
+ "require": "./dist/libsql/index.js",
76
+ "types": "./dist/libsql/index.d.ts"
77
+ },
78
+ "./libsql/*": {
79
+ "import": "./dist/libsql/*.js",
80
+ "require": "./dist/libsql/*.js",
81
+ "types": "./dist/libsql/*.d.ts"
82
+ }
83
+ },
84
+ "dependencies": {
85
+ "applesauce-core": "0.0.0-next-20250915145415"
86
+ },
87
+ "optionalDependencies": {
88
+ "better-sqlite3": "^12.2.0",
89
+ "@libsql/client": "^0.15.15"
90
+ },
91
+ "devDependencies": {
92
+ "@hirez_io/observer-spy": "^2.2.0",
93
+ "@types/better-sqlite3": "^7.6.13",
94
+ "@types/bun": "^1.2.22",
95
+ "@types/ws": "^8.5.13",
96
+ "applesauce-signers": "0.0.0-next-20250915145415",
97
+ "typescript": "^5.7.3",
98
+ "vitest": "^3.2.4",
99
+ "vitest-websocket-mock": "^0.5.0",
100
+ "ws": "^8.18.3"
101
+ },
102
+ "funding": {
103
+ "type": "lightning",
104
+ "url": "lightning:nostrudel@geyser.fund"
105
+ },
106
+ "scripts": {
107
+ "build": "tsc",
108
+ "watch:build": "tsc --watch > /dev/null",
109
+ "test": "vitest run --passWithNoTests",
110
+ "watch:test": "vitest",
111
+ "relay": "node dist/relay.js"
112
+ }
113
+ }