@massalabs/gossip-sdk 0.0.2-dev.20260220052333 → 0.0.2-dev.20260220053835
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.
- package/README.md +2 -2
- package/dist/config/sdk.js +1 -1
- package/dist/contacts.d.ts +11 -17
- package/dist/contacts.js +20 -26
- package/dist/core/SdkEventEmitter.d.ts +2 -0
- package/dist/core/SdkEventEmitter.js +2 -0
- package/dist/core/SdkPolling.d.ts +2 -5
- package/dist/core/SdkPolling.js +1 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -0
- package/dist/db.d.ts +34 -56
- package/dist/db.js +39 -229
- package/dist/gossip.d.ts +54 -23
- package/dist/gossip.js +149 -120
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -4
- package/dist/queries/activeSeekers.d.ts +2 -0
- package/dist/queries/activeSeekers.js +18 -0
- package/dist/queries/announcementCursors.d.ts +2 -0
- package/dist/queries/announcementCursors.js +20 -0
- package/dist/queries/contacts.d.ts +13 -0
- package/dist/queries/contacts.js +43 -0
- package/dist/queries/discussions.d.ts +21 -0
- package/dist/queries/discussions.js +73 -0
- package/dist/queries/index.d.ts +7 -0
- package/dist/queries/index.js +7 -0
- package/dist/queries/messages.d.ts +27 -0
- package/dist/queries/messages.js +120 -0
- package/dist/queries/pendingAnnouncements.d.ts +4 -0
- package/dist/queries/pendingAnnouncements.js +11 -0
- package/dist/queries/userProfile.d.ts +22 -0
- package/dist/queries/userProfile.js +116 -0
- package/dist/schema.d.ts +1280 -0
- package/dist/schema.js +164 -0
- package/dist/services/announcement.d.ts +8 -7
- package/dist/services/announcement.js +95 -121
- package/dist/services/auth.d.ts +1 -3
- package/dist/services/auth.js +4 -11
- package/dist/services/discussion.d.ts +8 -15
- package/dist/services/discussion.js +62 -62
- package/dist/services/message.d.ts +15 -26
- package/dist/services/message.js +282 -270
- package/dist/services/refresh.d.ts +3 -5
- package/dist/services/refresh.js +10 -13
- package/dist/sqlite-worker.d.ts +12 -0
- package/dist/sqlite-worker.js +106 -0
- package/dist/sqlite.d.ts +79 -0
- package/dist/sqlite.js +448 -0
- package/dist/utils/contacts.d.ts +2 -5
- package/dist/utils/contacts.js +23 -39
- package/dist/utils/discussions.d.ts +7 -2
- package/dist/utils/discussions.js +24 -5
- package/dist/utils/logs.js +1 -3
- package/dist/utils/validation.d.ts +2 -4
- package/dist/utils/validation.js +5 -10
- package/package.json +5 -7
- package/dist/api/messageProtocol/mock.d.ts +0 -12
- package/dist/api/messageProtocol/mock.js +0 -12
- package/dist/types/events.d.ts +0 -80
- package/dist/types/events.js +0 -7
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Class-based service to periodically refresh sessions and handle
|
|
5
5
|
* keep-alive messages and broken sessions.
|
|
6
6
|
*/
|
|
7
|
-
import { type GossipDatabase } from '../db';
|
|
8
7
|
import type { SessionModule } from '../wasm/session';
|
|
9
8
|
import { MessageService } from './message';
|
|
10
9
|
import { AnnouncementService } from './announcement';
|
|
@@ -16,11 +15,11 @@ import { SdkEventEmitter } from '../core/SdkEventEmitter';
|
|
|
16
15
|
* @example
|
|
17
16
|
* ```typescript
|
|
18
17
|
* const refreshService = new RefreshService(
|
|
19
|
-
* db,
|
|
20
18
|
* messageService,
|
|
21
19
|
* discussionService,
|
|
22
20
|
* announcementService,
|
|
23
|
-
* session
|
|
21
|
+
* session,
|
|
22
|
+
* eventEmitter
|
|
24
23
|
* );
|
|
25
24
|
*
|
|
26
25
|
* // Handle session refresh for active discussions
|
|
@@ -28,14 +27,13 @@ import { SdkEventEmitter } from '../core/SdkEventEmitter';
|
|
|
28
27
|
* ```
|
|
29
28
|
*/
|
|
30
29
|
export declare class RefreshService {
|
|
31
|
-
private db;
|
|
32
30
|
private messageService;
|
|
33
31
|
private announcementService;
|
|
34
32
|
private discussionService;
|
|
35
33
|
private session;
|
|
36
34
|
private isUpdating;
|
|
37
35
|
private eventEmitter;
|
|
38
|
-
constructor(
|
|
36
|
+
constructor(messageService: MessageService, discussionService: DiscussionService, announcementService: AnnouncementService, session: SessionModule, eventEmitter: SdkEventEmitter);
|
|
39
37
|
/**
|
|
40
38
|
* Update state for all discussions:
|
|
41
39
|
* - Cleanup orphaned peers
|
package/dist/services/refresh.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Class-based service to periodically refresh sessions and handle
|
|
5
5
|
* keep-alive messages and broken sessions.
|
|
6
6
|
*/
|
|
7
|
-
import { MessageType, MessageDirection, MessageStatus
|
|
7
|
+
import { MessageType, MessageDirection, MessageStatus } from '../db';
|
|
8
|
+
import { getDiscussionsByOwner } from '../queries';
|
|
9
|
+
import { toSortedDiscussions } from '../utils/discussions';
|
|
8
10
|
import { SessionStatus } from '../wasm/bindings';
|
|
9
11
|
import { decodeUserId, encodeUserId } from '../utils/userId';
|
|
10
12
|
import { Logger } from '../utils/logs';
|
|
@@ -16,11 +18,11 @@ const logger = new Logger('RefreshService');
|
|
|
16
18
|
* @example
|
|
17
19
|
* ```typescript
|
|
18
20
|
* const refreshService = new RefreshService(
|
|
19
|
-
* db,
|
|
20
21
|
* messageService,
|
|
21
22
|
* discussionService,
|
|
22
23
|
* announcementService,
|
|
23
|
-
* session
|
|
24
|
+
* session,
|
|
25
|
+
* eventEmitter
|
|
24
26
|
* );
|
|
25
27
|
*
|
|
26
28
|
* // Handle session refresh for active discussions
|
|
@@ -28,13 +30,7 @@ const logger = new Logger('RefreshService');
|
|
|
28
30
|
* ```
|
|
29
31
|
*/
|
|
30
32
|
export class RefreshService {
|
|
31
|
-
constructor(
|
|
32
|
-
Object.defineProperty(this, "db", {
|
|
33
|
-
enumerable: true,
|
|
34
|
-
configurable: true,
|
|
35
|
-
writable: true,
|
|
36
|
-
value: void 0
|
|
37
|
-
});
|
|
33
|
+
constructor(messageService, discussionService, announcementService, session, eventEmitter) {
|
|
38
34
|
Object.defineProperty(this, "messageService", {
|
|
39
35
|
enumerable: true,
|
|
40
36
|
configurable: true,
|
|
@@ -71,7 +67,6 @@ export class RefreshService {
|
|
|
71
67
|
writable: true,
|
|
72
68
|
value: void 0
|
|
73
69
|
});
|
|
74
|
-
this.db = db;
|
|
75
70
|
this.messageService = messageService;
|
|
76
71
|
this.discussionService = discussionService;
|
|
77
72
|
this.announcementService = announcementService;
|
|
@@ -100,7 +95,8 @@ export class RefreshService {
|
|
|
100
95
|
log.error('ownerUserId is empty, skipping update_state');
|
|
101
96
|
return;
|
|
102
97
|
}
|
|
103
|
-
const
|
|
98
|
+
const allRows = await getDiscussionsByOwner(ownerUserId);
|
|
99
|
+
const discussions = toSortedDiscussions(allRows);
|
|
104
100
|
if (!discussions.length) {
|
|
105
101
|
return;
|
|
106
102
|
}
|
|
@@ -147,7 +143,8 @@ export class RefreshService {
|
|
|
147
143
|
}
|
|
148
144
|
}
|
|
149
145
|
// Step 2: send announcements
|
|
150
|
-
const
|
|
146
|
+
const refreshRows = await getDiscussionsByOwner(ownerUserId);
|
|
147
|
+
const discussionsAfterRefresh = toSortedDiscussions(refreshRows);
|
|
151
148
|
const activePendingDiscussions = discussionsAfterRefresh.filter(discussion => {
|
|
152
149
|
const status = this.session.peerSessionStatus(decodeUserId(discussion.contactUserId));
|
|
153
150
|
return [SessionStatus.Active, SessionStatus.SelfRequested].includes(status);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Web Worker — owns all WASM/SQLite state.
|
|
3
|
+
*
|
|
4
|
+
* Runs wa-sqlite (sync build) with AccessHandlePoolVFS for OPFS persistence.
|
|
5
|
+
* The main thread communicates via postMessage with { id, type, ... } messages.
|
|
6
|
+
*
|
|
7
|
+
* Messages:
|
|
8
|
+
* init → load WASM + VFS, open DB, run init SQL (PRAGMAs + DDL)
|
|
9
|
+
* exec → prepare/bind/step/finalize, return { rows, lastInsertRowId }
|
|
10
|
+
* close → close DB
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Web Worker — owns all WASM/SQLite state.
|
|
3
|
+
*
|
|
4
|
+
* Runs wa-sqlite (sync build) with AccessHandlePoolVFS for OPFS persistence.
|
|
5
|
+
* The main thread communicates via postMessage with { id, type, ... } messages.
|
|
6
|
+
*
|
|
7
|
+
* Messages:
|
|
8
|
+
* init → load WASM + VFS, open DB, run init SQL (PRAGMAs + DDL)
|
|
9
|
+
* exec → prepare/bind/step/finalize, return { rows, lastInsertRowId }
|
|
10
|
+
* close → close DB
|
|
11
|
+
*/
|
|
12
|
+
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs';
|
|
13
|
+
import * as SQLite from 'wa-sqlite';
|
|
14
|
+
import { AccessHandlePoolVFS } from 'wa-sqlite/src/examples/AccessHandlePoolVFS.js';
|
|
15
|
+
let sqlite3 = null;
|
|
16
|
+
let dbHandle = null;
|
|
17
|
+
// Bind Worker's postMessage (avoids DOM lib signature mismatch at compile time)
|
|
18
|
+
const post = globalThis.postMessage.bind(globalThis);
|
|
19
|
+
function copyRow(row) {
|
|
20
|
+
return row.map(v => (v instanceof Uint8Array ? new Uint8Array(v) : v));
|
|
21
|
+
}
|
|
22
|
+
async function execSql(sql, params) {
|
|
23
|
+
if (!sqlite3 || dbHandle === null)
|
|
24
|
+
throw new Error('SQLite not initialized');
|
|
25
|
+
const rows = [];
|
|
26
|
+
if (params.length === 0) {
|
|
27
|
+
await sqlite3.exec(dbHandle, sql, (row) => {
|
|
28
|
+
rows.push(copyRow(row));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const str = sqlite3.str_new(dbHandle, sql);
|
|
33
|
+
try {
|
|
34
|
+
const prepared = await sqlite3.prepare_v2(dbHandle, sqlite3.str_value(str));
|
|
35
|
+
if (prepared) {
|
|
36
|
+
try {
|
|
37
|
+
sqlite3.bind_collection(prepared.stmt, params);
|
|
38
|
+
while ((await sqlite3.step(prepared.stmt)) === SQLite.SQLITE_ROW) {
|
|
39
|
+
rows.push(copyRow(sqlite3.row(prepared.stmt)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await sqlite3.finalize(prepared.stmt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
sqlite3.str_finish(str);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Only capture lastInsertRowId for INSERT statements (avoid unnecessary query on SELECT/UPDATE/DELETE)
|
|
52
|
+
let lastInsertRowId = 0;
|
|
53
|
+
if (sql.trimStart().toUpperCase().startsWith('INSERT')) {
|
|
54
|
+
await sqlite3.exec(dbHandle, 'SELECT last_insert_rowid()', (row) => {
|
|
55
|
+
lastInsertRowId = row[0];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return { rows, lastInsertRowId };
|
|
59
|
+
}
|
|
60
|
+
addEventListener('message', async (e) => {
|
|
61
|
+
const { id, type } = e.data;
|
|
62
|
+
try {
|
|
63
|
+
switch (type) {
|
|
64
|
+
case 'init': {
|
|
65
|
+
const { opfsPath, wasmUrl, initSql } = e.data;
|
|
66
|
+
const moduleArg = {};
|
|
67
|
+
if (wasmUrl)
|
|
68
|
+
moduleArg.locateFile = () => wasmUrl;
|
|
69
|
+
const module = await SQLiteESMFactory(moduleArg);
|
|
70
|
+
sqlite3 = SQLite.Factory(module);
|
|
71
|
+
const vfs = new AccessHandlePoolVFS(opfsPath);
|
|
72
|
+
await vfs.isReady;
|
|
73
|
+
sqlite3.vfs_register(vfs, true);
|
|
74
|
+
dbHandle = await sqlite3.open_v2('gossip.db');
|
|
75
|
+
if (initSql) {
|
|
76
|
+
await sqlite3.exec(dbHandle, initSql);
|
|
77
|
+
}
|
|
78
|
+
post({ id, type: 'init-result', success: true });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'exec': {
|
|
82
|
+
const { sql, params } = e.data;
|
|
83
|
+
const result = await execSql(sql, params);
|
|
84
|
+
post({
|
|
85
|
+
id,
|
|
86
|
+
type: 'exec-result',
|
|
87
|
+
rows: result.rows,
|
|
88
|
+
lastInsertRowId: result.lastInsertRowId,
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case 'close': {
|
|
93
|
+
if (dbHandle !== null && sqlite3) {
|
|
94
|
+
await sqlite3.close(dbHandle);
|
|
95
|
+
dbHandle = null;
|
|
96
|
+
}
|
|
97
|
+
sqlite3 = null;
|
|
98
|
+
post({ id, type: 'close-result' });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
post({ id, type: 'error', message: err.message });
|
|
105
|
+
}
|
|
106
|
+
});
|
package/dist/sqlite.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite initialization module for the Gossip SDK.
|
|
3
|
+
*
|
|
4
|
+
* Uses wa-sqlite (WASM) with Drizzle ORM's sqlite-proxy driver.
|
|
5
|
+
* Two execution paths:
|
|
6
|
+
* - Browser (opfsPath set): Web Worker + AccessHandlePoolVFS — OPFS persistence,
|
|
7
|
+
* off the main thread. Uses the sync WASM build (wa-sqlite).
|
|
8
|
+
* - In-memory (tests): :memory: in-process — no persistence, fast, isolated.
|
|
9
|
+
* Uses the sync WASM build with wasmBinary passed directly.
|
|
10
|
+
*
|
|
11
|
+
* In Phase C the VFS will be swapped for the encrypted PlausibleDeniableVFS.
|
|
12
|
+
*/
|
|
13
|
+
import { type SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
|
|
14
|
+
import * as schema from './schema.js';
|
|
15
|
+
export type GossipDatabase = SqliteRemoteDatabase<typeof schema>;
|
|
16
|
+
export interface InitDbOptions {
|
|
17
|
+
/**
|
|
18
|
+
* OPFS directory path for persistent storage.
|
|
19
|
+
* When set, spawns a Web Worker with AccessHandlePoolVFS for OPFS persistence.
|
|
20
|
+
* When omitted, uses an in-memory database (for tests).
|
|
21
|
+
*/
|
|
22
|
+
opfsPath?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Pre-loaded WASM binary for environments where fetch() is unavailable
|
|
25
|
+
* (e.g. Node.js tests). When omitted, the factory uses fetch() to load
|
|
26
|
+
* the .wasm file (browser default).
|
|
27
|
+
*/
|
|
28
|
+
wasmBinary?: ArrayBuffer;
|
|
29
|
+
/**
|
|
30
|
+
* URL to the wa-sqlite WASM file. Used in browser to tell the Emscripten
|
|
31
|
+
* factory where to fetch the WASM binary (needed when bundlers like Vite
|
|
32
|
+
* rewrite asset paths). When omitted, the factory uses its default path.
|
|
33
|
+
*/
|
|
34
|
+
wasmUrl?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initialize wa-sqlite and create the Drizzle ORM instance.
|
|
38
|
+
* Idempotent — subsequent calls are no-ops.
|
|
39
|
+
*
|
|
40
|
+
* @param options.opfsPath - Set to persist via OPFS Worker (production).
|
|
41
|
+
* Omit for in-memory database (tests).
|
|
42
|
+
*/
|
|
43
|
+
export declare function initDb(options?: InitDbOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Get the Drizzle ORM database instance.
|
|
46
|
+
* Throws if initDb() has not been called.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getSqliteDb(): GossipDatabase;
|
|
49
|
+
export declare function isSqliteOpen(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Run a callback inside a SQLite transaction (BEGIN / COMMIT / ROLLBACK).
|
|
52
|
+
* All Drizzle operations inside the callback share the same transaction
|
|
53
|
+
* and the same dbLock hold, so they cannot interleave with outside queries.
|
|
54
|
+
*/
|
|
55
|
+
export declare function withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
56
|
+
export declare function clearAllTables(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Clear only conversation-related tables (contacts, discussions, messages).
|
|
59
|
+
* Preserves user profiles and other data.
|
|
60
|
+
*/
|
|
61
|
+
export declare function clearConversationTables(): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Get the last auto-increment row ID inserted via this connection.
|
|
64
|
+
* Used after INSERT into tables with INTEGER PRIMARY KEY AUTOINCREMENT.
|
|
65
|
+
*
|
|
66
|
+
* Browser path: returns the cached value from the last Worker exec response
|
|
67
|
+
* (returned atomically with every exec — no race condition).
|
|
68
|
+
* Test path: queries directly (same connection, serialized by dbLock).
|
|
69
|
+
*/
|
|
70
|
+
export declare function getLastInsertRowId(): Promise<number>;
|
|
71
|
+
/**
|
|
72
|
+
* Close the database and release all resources.
|
|
73
|
+
* Browser path: sends close to Worker, then terminates it.
|
|
74
|
+
* Test path: closes in-process database handle.
|
|
75
|
+
* Atomically resets all state by replacing with a fresh default.
|
|
76
|
+
*/
|
|
77
|
+
export declare function closeSqlite(): Promise<void>;
|
|
78
|
+
/** Exported for testing — the raw DDL string used to create tables. */
|
|
79
|
+
export declare const _DDL_FOR_TESTING = "\n CREATE TABLE IF NOT EXISTS contacts (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ownerUserId TEXT NOT NULL,\n userId TEXT NOT NULL,\n name TEXT NOT NULL,\n avatar TEXT,\n publicKeys BLOB NOT NULL,\n isOnline INTEGER NOT NULL DEFAULT 0,\n lastSeen INTEGER NOT NULL,\n createdAt INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ownerUserId TEXT NOT NULL,\n contactUserId TEXT NOT NULL,\n messageId BLOB,\n content TEXT NOT NULL,\n serializedContent BLOB,\n type TEXT NOT NULL,\n direction TEXT NOT NULL,\n status TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n metadata TEXT,\n seeker BLOB,\n replyTo TEXT,\n forwardOf TEXT,\n encryptedMessage BLOB,\n whenToSend INTEGER\n );\n\n CREATE TABLE IF NOT EXISTS userProfile (\n userId TEXT PRIMARY KEY,\n username TEXT NOT NULL,\n avatar TEXT,\n bio TEXT,\n status TEXT NOT NULL,\n lastSeen INTEGER NOT NULL,\n createdAt INTEGER NOT NULL,\n updatedAt INTEGER NOT NULL,\n lastPublicKeyPush INTEGER,\n security TEXT NOT NULL,\n session BLOB NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS discussions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ownerUserId TEXT NOT NULL,\n contactUserId TEXT NOT NULL,\n direction TEXT NOT NULL,\n status TEXT NOT NULL,\n weAccepted INTEGER NOT NULL DEFAULT 0,\n sendAnnouncement TEXT,\n nextSeeker BLOB,\n initiationAnnouncement BLOB,\n announcementMessage TEXT,\n lastSyncTimestamp INTEGER,\n customName TEXT,\n lastMessageId INTEGER,\n lastMessageContent TEXT,\n lastMessageTimestamp INTEGER,\n unreadCount INTEGER NOT NULL DEFAULT 0,\n createdAt INTEGER NOT NULL,\n updatedAt INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS pendingEncryptedMessages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n seeker BLOB NOT NULL,\n ciphertext BLOB NOT NULL,\n fetchedAt INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS pendingAnnouncements (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n announcement BLOB NOT NULL,\n fetchedAt INTEGER NOT NULL,\n counter TEXT\n );\n\n CREATE TABLE IF NOT EXISTS activeSeekers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n seeker BLOB NOT NULL\n );\n\n -- Indexes: contacts\n CREATE INDEX IF NOT EXISTS contacts_owner_user_idx ON contacts(ownerUserId, userId);\n CREATE INDEX IF NOT EXISTS contacts_owner_name_idx ON contacts(ownerUserId, name);\n\n -- Indexes: messages\n CREATE INDEX IF NOT EXISTS messages_owner_contact_idx ON messages(ownerUserId, contactUserId);\n CREATE INDEX IF NOT EXISTS messages_owner_status_idx ON messages(ownerUserId, status);\n CREATE INDEX IF NOT EXISTS messages_owner_contact_status_idx ON messages(ownerUserId, contactUserId, status);\n CREATE INDEX IF NOT EXISTS messages_owner_seeker_idx ON messages(ownerUserId, seeker);\n CREATE INDEX IF NOT EXISTS messages_owner_contact_dir_idx ON messages(ownerUserId, contactUserId, direction);\n CREATE INDEX IF NOT EXISTS messages_owner_dir_status_idx ON messages(ownerUserId, direction, status);\n CREATE INDEX IF NOT EXISTS messages_timestamp_idx ON messages(timestamp);\n\n -- Indexes: userProfile\n CREATE INDEX IF NOT EXISTS userProfile_username_idx ON userProfile(username);\n CREATE INDEX IF NOT EXISTS userProfile_status_idx ON userProfile(status);\n\n -- Indexes: discussions\n CREATE UNIQUE INDEX IF NOT EXISTS discussions_owner_contact_idx ON discussions(ownerUserId, contactUserId);\n CREATE INDEX IF NOT EXISTS discussions_owner_status_idx ON discussions(ownerUserId, status);\n\n -- Indexes: pendingEncryptedMessages\n CREATE INDEX IF NOT EXISTS pending_encrypted_seeker_idx ON pendingEncryptedMessages(seeker);\n CREATE INDEX IF NOT EXISTS pending_encrypted_fetchedAt_idx ON pendingEncryptedMessages(fetchedAt);\n\n -- Indexes: pendingAnnouncements\n CREATE UNIQUE INDEX IF NOT EXISTS pending_announcements_announcement_idx ON pendingAnnouncements(announcement);\n CREATE INDEX IF NOT EXISTS pending_announcements_fetchedAt_idx ON pendingAnnouncements(fetchedAt);\n\n -- Indexes: activeSeekers\n CREATE INDEX IF NOT EXISTS active_seekers_seeker_idx ON activeSeekers(seeker);\n\n CREATE TABLE IF NOT EXISTS announcementCursors (\n userId TEXT PRIMARY KEY,\n counter TEXT NOT NULL\n );\n";
|