@optimystic/db-p2p-storage-ns 0.13.4

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.
@@ -0,0 +1,163 @@
1
+ import type { ActionId, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
2
+ import type { BlockMetadata, IRawStorage } from '@optimystic/db-p2p';
3
+ import type { SqliteDb, SqliteStatement } from './db.js';
4
+ import { createLogger } from './logger.js';
5
+
6
+ const log = createLogger('storage:sqlite');
7
+
8
+ /**
9
+ * SQLite-backed `IRawStorage` implementation for NativeScript peers.
10
+ *
11
+ * Every CRUD operation goes through a prepared statement bound once in the
12
+ * constructor. `listRevisions` and `listPendingTransactions` issue
13
+ * `SELECT … ORDER BY` queries bounded by `block_id` (and `rev` for revisions)
14
+ * and drain the results into an array before yielding — matching the
15
+ * `IndexedDBRawStorage` pattern, where holding a cursor live across consumer
16
+ * awaits would invalidate the underlying transaction.
17
+ *
18
+ * `promotePendingTransaction` runs as a single `BEGIN; INSERT…; DELETE…; COMMIT;`
19
+ * via `SqliteDb.transaction(fn)`, so the move is atomic across crashes —
20
+ * unlike the MMKV adapter, which has to maintain a separate pending-index row.
21
+ */
22
+ export class SqliteRawStorage implements IRawStorage {
23
+ private readonly stmts: {
24
+ getMetadata: SqliteStatement;
25
+ saveMetadata: SqliteStatement;
26
+ getRevision: SqliteStatement;
27
+ saveRevision: SqliteStatement;
28
+ listRevisionsAsc: SqliteStatement;
29
+ listRevisionsDesc: SqliteStatement;
30
+ getPending: SqliteStatement;
31
+ savePending: SqliteStatement;
32
+ deletePending: SqliteStatement;
33
+ listPending: SqliteStatement;
34
+ getTransaction: SqliteStatement;
35
+ saveTransaction: SqliteStatement;
36
+ getMaterialized: SqliteStatement;
37
+ saveMaterialized: SqliteStatement;
38
+ deleteMaterialized: SqliteStatement;
39
+ pageCount: SqliteStatement;
40
+ pageSize: SqliteStatement;
41
+ };
42
+
43
+ constructor(private readonly db: SqliteDb) {
44
+ this.stmts = {
45
+ getMetadata: db.prepare('SELECT value FROM metadata WHERE block_id = ?'),
46
+ saveMetadata: db.prepare('INSERT OR REPLACE INTO metadata (block_id, value) VALUES (?, ?)'),
47
+ getRevision: db.prepare('SELECT action_id FROM revisions WHERE block_id = ? AND rev = ?'),
48
+ saveRevision: db.prepare('INSERT OR REPLACE INTO revisions (block_id, rev, action_id) VALUES (?, ?, ?)'),
49
+ listRevisionsAsc: db.prepare('SELECT rev, action_id FROM revisions WHERE block_id = ? AND rev BETWEEN ? AND ? ORDER BY rev ASC'),
50
+ listRevisionsDesc: db.prepare('SELECT rev, action_id FROM revisions WHERE block_id = ? AND rev BETWEEN ? AND ? ORDER BY rev DESC'),
51
+ getPending: db.prepare('SELECT value FROM pending WHERE block_id = ? AND action_id = ?'),
52
+ savePending: db.prepare('INSERT OR REPLACE INTO pending (block_id, action_id, value) VALUES (?, ?, ?)'),
53
+ deletePending: db.prepare('DELETE FROM pending WHERE block_id = ? AND action_id = ?'),
54
+ listPending: db.prepare('SELECT action_id FROM pending WHERE block_id = ? ORDER BY action_id ASC'),
55
+ getTransaction: db.prepare('SELECT value FROM transactions WHERE block_id = ? AND action_id = ?'),
56
+ saveTransaction: db.prepare('INSERT OR REPLACE INTO transactions (block_id, action_id, value) VALUES (?, ?, ?)'),
57
+ getMaterialized: db.prepare('SELECT value FROM materialized WHERE block_id = ? AND action_id = ?'),
58
+ saveMaterialized: db.prepare('INSERT OR REPLACE INTO materialized (block_id, action_id, value) VALUES (?, ?, ?)'),
59
+ deleteMaterialized: db.prepare('DELETE FROM materialized WHERE block_id = ? AND action_id = ?'),
60
+ pageCount: db.prepare('PRAGMA page_count'),
61
+ pageSize: db.prepare('PRAGMA page_size'),
62
+ };
63
+ }
64
+
65
+ async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
66
+ const row = await this.stmts.getMetadata.get(blockId);
67
+ if (!row) return undefined;
68
+ return JSON.parse(row.value as string) as BlockMetadata;
69
+ }
70
+
71
+ async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
72
+ await this.stmts.saveMetadata.run(blockId, JSON.stringify(metadata));
73
+ }
74
+
75
+ async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
76
+ const row = await this.stmts.getRevision.get(blockId, rev);
77
+ return row ? (row.action_id as ActionId) : undefined;
78
+ }
79
+
80
+ async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
81
+ await this.stmts.saveRevision.run(blockId, rev, actionId);
82
+ }
83
+
84
+ async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
85
+ const ascending = startRev <= endRev;
86
+ const lo = ascending ? startRev : endRev;
87
+ const hi = ascending ? endRev : startRev;
88
+ const stmt = ascending ? this.stmts.listRevisionsAsc : this.stmts.listRevisionsDesc;
89
+ const rows = await stmt.all(blockId, lo, hi);
90
+ for (const row of rows) {
91
+ yield { rev: row.rev as number, actionId: row.action_id as ActionId };
92
+ }
93
+ }
94
+
95
+ async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
96
+ const row = await this.stmts.getPending.get(blockId, actionId);
97
+ if (!row) return undefined;
98
+ return JSON.parse(row.value as string) as Transform;
99
+ }
100
+
101
+ async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
102
+ await this.stmts.savePending.run(blockId, actionId, JSON.stringify(transform));
103
+ }
104
+
105
+ async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
106
+ await this.stmts.deletePending.run(blockId, actionId);
107
+ }
108
+
109
+ async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
110
+ const rows = await this.stmts.listPending.all(blockId);
111
+ for (const row of rows) {
112
+ yield row.action_id as ActionId;
113
+ }
114
+ }
115
+
116
+ async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
117
+ const row = await this.stmts.getTransaction.get(blockId, actionId);
118
+ if (!row) return undefined;
119
+ return JSON.parse(row.value as string) as Transform;
120
+ }
121
+
122
+ async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
123
+ await this.stmts.saveTransaction.run(blockId, actionId, JSON.stringify(transform));
124
+ }
125
+
126
+ async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
127
+ const row = await this.stmts.getMaterialized.get(blockId, actionId);
128
+ if (!row) return undefined;
129
+ return JSON.parse(row.value as string) as IBlock;
130
+ }
131
+
132
+ async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
133
+ if (block) {
134
+ await this.stmts.saveMaterialized.run(blockId, actionId, JSON.stringify(block));
135
+ } else {
136
+ await this.stmts.deleteMaterialized.run(blockId, actionId);
137
+ }
138
+ }
139
+
140
+ async getApproximateBytesUsed(): Promise<number> {
141
+ try {
142
+ const pageCountRow = await this.stmts.pageCount.get();
143
+ const pageSizeRow = await this.stmts.pageSize.get();
144
+ const pages = (pageCountRow?.page_count as number | undefined) ?? 0;
145
+ const size = (pageSizeRow?.page_size as number | undefined) ?? 0;
146
+ return pages * size;
147
+ } catch (err) {
148
+ log('PRAGMA page_count/page_size failed: %o', err);
149
+ return 0;
150
+ }
151
+ }
152
+
153
+ async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
154
+ await this.db.transaction(async () => {
155
+ const row = await this.stmts.getPending.get(blockId, actionId);
156
+ if (!row) {
157
+ throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
158
+ }
159
+ await this.stmts.saveTransaction.run(blockId, actionId, row.value as string);
160
+ await this.stmts.deletePending.run(blockId, actionId);
161
+ });
162
+ }
163
+ }