Skip to content

Commit eafa545

Browse files
authored
Use novel refactor (lnreader#1838)
* init * feat: add zustand dependency and persistence key contract * refactor: extract bootstrap data loading into reusable service * refactor: move chapter mutations into store-ready action helpers * feat: add zustand novel store with cache and core actions * refactor: bridge novel persistence contracts for migration safety * refactor: migrate NovelScreen domain flows to zustand selectors * refactor: migrate NovelScreenList to selector-based store access * refactor: move reader chapter flows onto store boundaries * refactor: decouple useNovelSettings from broad context domain state * refactor: align migrateNovel with stable persistence contracts * refactor: cut novel-reader consumers to store-only context boundary * refactor: retire legacy useNovel and route cache cleanup export * test: update suites for store-only context boundary cutover * test: modernize store-era mocks and add contract coverage * test: finalize Task-15 sweep—remove dead useNovelData and lint clear mocksContract Final validation confirms mock-contract test suite clean and target file deletion verified with zero stale references in src/ scope. * remove imports from NovelScreen * reworked ai output * improvements * implemented synchronus novel and chapter fetch * refactor tests * fix db tests * Update remaining tests. * Harden chapter actions and bootstrap flows * Only count filtered chapters * improved chapter insert speed by optimizing triggers * Improved the batching function * Added drizzle support to dbManager.batch * removed better-sqlite3 for testing * fix tests * Update updateNovelChapters fucntion * reverse read filter * fix snackbar * fixed page bottomsheet * resolved paged novels showing wrong chapter number on opening * use openPage instead of setPageIndex in chapterDrawer * fix lint & tests * fix type issues * fix various smaller issues * updated novel restore * Delete tsconfig.tsbuildinfo
1 parent b680a07 commit eafa545

75 files changed

Lines changed: 6232 additions & 2014 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

__mocks__/database.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
jest.mock('@database/queries/NovelQueries', () => ({
2+
getNovelById: jest.fn(),
23
getNovelByPath: jest.fn(),
34
deleteCachedNovels: jest.fn(),
45
getCachedNovels: jest.fn(),
@@ -30,7 +31,9 @@ jest.mock('@database/queries/ChapterQueries', () => ({
3031
insertChapters: jest.fn(),
3132
getCustomPages: jest.fn(),
3233
getChapterCount: jest.fn(),
34+
getChapterCountSync: jest.fn(),
3335
getPageChaptersBatched: jest.fn(),
36+
getNovelChaptersSync: jest.fn(),
3437
getFirstUnreadChapter: jest.fn(),
3538
updateChapterProgress: jest.fn(),
3639
}));

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@
118118
"react-native-worklets": "^0.8.1",
119119
"react-native-zip-archive": "^7.0.2",
120120
"sanitize-html": "^2.17.2",
121-
"urlencode": "^2.0.0"
121+
"urlencode": "^2.0.0",
122+
"zustand": "^5.0.12"
122123
},
123124
"devDependencies": {
124125
"@babel/core": "^7.29.0",
@@ -145,7 +146,7 @@
145146
"@typescript-eslint/parser": "^8.58.0",
146147
"babel-plugin-module-resolver": "^5.0.3",
147148
"babel-plugin-react-compiler": "^1.0.0",
148-
"better-sqlite3": "^12.8.0",
149+
"better-sqlite3": "^12.9.0",
149150
"drizzle-kit": "1.0.0-beta.20",
150151
"eslint": "^8.57.1",
151152
"eslint-plugin-eslint-comments": "^3.2.0",

pnpm-lock.yaml

Lines changed: 35 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/database/__tests__/db.test.ts

Lines changed: 36 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,9 @@
1-
import Database from 'better-sqlite3';
1+
import { open, type DB } from '@op-engineering/op-sqlite';
22
import { drizzle } from 'drizzle-orm/op-sqlite';
33
import { migrate } from 'drizzle-orm/op-sqlite/migrator';
44
import migrations from '../../../drizzle/migrations';
55
import { schema } from '@database/schema';
66

7-
jest.mock('@op-engineering/op-sqlite', () => ({
8-
__esModule: true,
9-
open: jest.fn(() => ({
10-
execute: jest.fn().mockResolvedValue({ rows: [] }),
11-
executeAsync: jest.fn().mockResolvedValue({ rows: [] }),
12-
executeSync: jest.fn().mockReturnValue({ rows: [] }),
13-
executeRawAsync: jest.fn().mockResolvedValue([]),
14-
executeBatch: jest.fn().mockResolvedValue(undefined),
15-
flushPendingReactiveQueries: jest.fn(),
16-
reactiveExecute: jest.fn(() => () => undefined),
17-
})),
18-
}));
19-
207
import { runDatabaseBootstrap } from '@database/db';
218

229
const MIGRATION_STATEMENTS = [
@@ -80,84 +67,26 @@ const MIGRATION_STATEMENTS = [
8067
`CREATE UNIQUE INDEX IF NOT EXISTS repository_url_unique ON Repository (url)`,
8168
];
8269

83-
const createExecutor = (sqlite: Database.Database) => ({
70+
const createExecutor = (sqlite: DB) => ({
8471
executeSync: (sql: string, params?: unknown[]) => {
85-
if (params && params.length) {
86-
const stmt = sqlite.prepare(sql);
87-
stmt.run(params as any[]);
88-
return;
89-
}
90-
sqlite.exec(sql);
72+
sqlite.executeSync(sql, params as any[]);
9173
},
9274
});
9375

94-
const createOpSqliteAdapter = (sqlite: Database.Database) => {
95-
return {
96-
execute: async (sql: string, params?: unknown[]) => {
97-
const stmt = sqlite.prepare(sql);
98-
const rows =
99-
params && params.length ? stmt.all(params as any[]) : stmt.all();
100-
return {
101-
rows: {
102-
_array: rows.map(row =>
103-
Object.values(row as Record<string, unknown>),
104-
),
105-
},
106-
};
107-
},
108-
executeSync: (sql: string, params?: unknown[]) => {
109-
const stmt = sqlite.prepare(sql);
110-
const result =
111-
params && params.length ? stmt.run(params as any[]) : stmt.run();
112-
return { rows: [], rowsAffected: result.changes ?? 0 };
113-
},
114-
executeAsync: async (sql: string, params?: unknown[]) => {
115-
const stmt = sqlite.prepare(sql);
116-
const result =
117-
params && params.length ? stmt.run(params as any[]) : stmt.run();
118-
return { rows: [], rowsAffected: result.changes ?? 0 };
119-
},
120-
executeRawAsync: async (sql: string, params?: unknown[]) => {
121-
const stmt = sqlite.prepare(sql).raw();
122-
const rows =
123-
params && params.length ? stmt.all(params as any[]) : stmt.all();
124-
return rows as unknown[][];
125-
},
126-
executeBatch: async (
127-
commands: Array<[string, unknown[] | unknown[][]]>,
128-
) => {
129-
const transaction = sqlite.transaction((cmds: typeof commands) => {
130-
for (const cmd of cmds) {
131-
const stmt = sqlite.prepare(cmd[0]);
132-
if (Array.isArray(cmd[1])) {
133-
for (const arg of cmd[1]) {
134-
stmt.run(arg as any[]);
135-
}
136-
} else {
137-
stmt.run(cmd[1] as any[]);
138-
}
139-
}
140-
});
141-
transaction(commands);
142-
},
143-
flushPendingReactiveQueries: () => undefined,
144-
reactiveExecute: () => () => undefined,
145-
};
146-
};
147-
14876
describe('new database initialization', () => {
14977
it('creates schema, triggers, and default data', async () => {
150-
const sqlite = new Database(':memory:');
78+
const sqlite = open({ name: ':memory:' });
79+
(sqlite as any).executeAsync ??= sqlite.execute;
80+
(sqlite as any).executeRawAsync ??= sqlite.executeRaw;
15181
try {
152-
const adapter = createOpSqliteAdapter(sqlite);
153-
const drizzleDb = drizzle(adapter, { schema });
82+
const drizzleDb = drizzle(sqlite, { schema });
15483

15584
await migrate(drizzleDb, migrations);
15685
runDatabaseBootstrap(createExecutor(sqlite));
15786

158-
const tables = sqlite
159-
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
160-
.all() as Array<{ name: string }>;
87+
const tables = sqlite.executeSync(
88+
"SELECT name FROM sqlite_master WHERE type='table'",
89+
).rows as Array<{ name: string }>;
16190
const tableNames = tables.map(table => table.name);
16291
expect(tableNames).toEqual(
16392
expect.arrayContaining([
@@ -169,9 +98,9 @@ describe('new database initialization', () => {
16998
]),
17099
);
171100

172-
const triggers = sqlite
173-
.prepare("SELECT name FROM sqlite_master WHERE type='trigger'")
174-
.all() as Array<{ name: string }>;
101+
const triggers = sqlite.executeSync(
102+
"SELECT name FROM sqlite_master WHERE type='trigger'",
103+
).rows as Array<{ name: string }>;
175104
const triggerNames = triggers.map(trigger => trigger.name);
176105
expect(triggerNames).toEqual(
177106
expect.arrayContaining([
@@ -182,9 +111,9 @@ describe('new database initialization', () => {
182111
]),
183112
);
184113

185-
const categories = sqlite
186-
.prepare('SELECT id, name FROM Category ORDER BY id')
187-
.all() as Array<{ id: number; name: string }>;
114+
const categories = sqlite.executeSync(
115+
'SELECT id, name FROM Category ORDER BY id',
116+
).rows as Array<{ id: number; name: string }>;
188117
expect(categories.map(category => category.id)).toEqual([1, 2]);
189118
} finally {
190119
sqlite.close();
@@ -194,20 +123,23 @@ describe('new database initialization', () => {
194123

195124
describe('runDatabaseBootstrap', () => {
196125
it('applies pragmas, triggers, and default categories', () => {
197-
const sqlite = new Database(':memory:');
126+
const sqlite = open({ name: ':memory:' });
127+
(sqlite as any).executeAsync ??= sqlite.execute;
128+
(sqlite as any).executeRawAsync ??= sqlite.executeRaw;
198129
try {
199130
for (const statement of MIGRATION_STATEMENTS) {
200-
sqlite.exec(statement.trim());
131+
sqlite.executeSync(statement.trim());
201132
}
202133

134+
sqlite.executeSync('PRAGMA journal_mode = WAL');
203135
runDatabaseBootstrap(createExecutor(sqlite));
204136

205-
const journalMode = sqlite.pragma('journal_mode', { simple: true });
137+
const journalMode = sqlite.executeRawSync('PRAGMA journal_mode')[0]?.[0];
206138
expect(['wal', 'memory']).toContain(String(journalMode).toLowerCase());
207139

208-
const triggers = sqlite
209-
.prepare("SELECT name FROM sqlite_master WHERE type='trigger'")
210-
.all() as Array<{ name: string }>;
140+
const triggers = sqlite.executeSync(
141+
"SELECT name FROM sqlite_master WHERE type='trigger'",
142+
).rows as Array<{ name: string }>;
211143
const triggerNames = triggers.map(trigger => trigger.name);
212144
expect(triggerNames).toEqual(
213145
expect.arrayContaining([
@@ -218,9 +150,9 @@ describe('runDatabaseBootstrap', () => {
218150
]),
219151
);
220152

221-
const categories = sqlite
222-
.prepare('SELECT id, name FROM Category ORDER BY id')
223-
.all() as Array<{ id: number; name: string }>;
153+
const categories = sqlite.executeSync(
154+
'SELECT id, name FROM Category ORDER BY id',
155+
).rows as Array<{ id: number; name: string }>;
224156
expect(categories.map(category => category.id)).toEqual([1, 2]);
225157
expect(categories.map(category => category.name)).toEqual([
226158
'categories.default',
@@ -234,19 +166,20 @@ describe('runDatabaseBootstrap', () => {
234166

235167
describe('production migrations', () => {
236168
it('can run after test schema exists', async () => {
237-
const sqlite = new Database(':memory:');
169+
const sqlite = open({ name: ':memory:' });
170+
(sqlite as any).executeAsync ??= sqlite.execute;
171+
(sqlite as any).executeRawAsync ??= sqlite.executeRaw;
238172
try {
239173
for (const statement of MIGRATION_STATEMENTS) {
240-
sqlite.exec(statement.trim());
174+
sqlite.executeSync(statement.trim());
241175
}
242176

243-
const adapter = createOpSqliteAdapter(sqlite);
244-
const drizzleDb = drizzle(adapter, { schema });
177+
const drizzleDb = drizzle(sqlite, { schema });
245178
await migrate(drizzleDb, migrations);
246179

247-
const tables = sqlite
248-
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
249-
.all() as Array<{ name: string }>;
180+
const tables = sqlite.executeSync(
181+
"SELECT name FROM sqlite_master WHERE type='table'",
182+
).rows as Array<{ name: string }>;
250183
const tableNames = tables.map(table => table.name);
251184
expect(tableNames).toEqual(
252185
expect.arrayContaining([

src/database/db.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ const populateDatabase = (executor: SqlExecutor) => {
6969

7070
const createDbTriggers = (executor: SqlExecutor) => {
7171
console.log('Creating database triggers');
72+
executor.executeSync('DROP TRIGGER IF EXISTS update_novel_stats');
73+
executor.executeSync('DROP TRIGGER IF EXISTS update_novel_stats_on_update');
74+
executor.executeSync('DROP TRIGGER IF EXISTS update_novel_stats_on_delete');
75+
executor.executeSync('DROP TRIGGER IF EXISTS add_category');
7276
executor.executeSync(createCategoryTriggerQuery);
7377
executor.executeSync(createNovelTriggerQueryDelete);
7478
executor.executeSync(createNovelTriggerQueryInsert);

src/database/manager/manager.d.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
// db-manager.types.ts
2-
import type { SQLiteTransaction, TablesRelationalConfig } from 'drizzle-orm';
2+
import type {
3+
SQLiteTransaction,
4+
TablesRelationalConfig,
5+
Placeholder,
6+
} from 'drizzle-orm';
7+
import { SQLitePreparedQuery } from 'drizzle-orm/sqlite-core';
38

49
// Define the TransactionParameter type based on your DrizzleDb
510
export type TransactionParameter = SQLiteTransaction<
@@ -14,6 +19,18 @@ export type TransactionParameter = SQLiteTransaction<
1419
* This contract ensures consistent documentation and type safety across the application.
1520
*/
1621
export interface IDbManager {
22+
/**
23+
* Efficiently executes a Drizzle query for multiple data rows using
24+
* op-sqlite executeBatch under the hood.
25+
*/
26+
batch<T extends Record<string, unknown>>(
27+
data: T[],
28+
fn: (
29+
tx: TransactionParameter,
30+
ph: (arg: Extract<keyof T, string>) => Placeholder,
31+
) => SQLitePreparedQuery<any>,
32+
): Promise<void>;
33+
1734
/**
1835
* Creates a subquery that defines a temporary named result set as a CTE.
1936
*

0 commit comments

Comments
 (0)