@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.
- package/README.md +118 -0
- package/dist/src/db.d.ts +75 -0
- package/dist/src/db.d.ts.map +1 -0
- package/dist/src/db.js +82 -0
- package/dist/src/db.js.map +1 -0
- package/dist/src/identity.d.ts +17 -0
- package/dist/src/identity.d.ts.map +1 -0
- package/dist/src/identity.js +28 -0
- package/dist/src/identity.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +3 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +6 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/ns-opener.d.ts +37 -0
- package/dist/src/ns-opener.d.ts.map +1 -0
- package/dist/src/ns-opener.js +93 -0
- package/dist/src/ns-opener.js.map +1 -0
- package/dist/src/sqlite-kv-store.d.ts +24 -0
- package/dist/src/sqlite-kv-store.d.ts.map +1 -0
- package/dist/src/sqlite-kv-store.js +50 -0
- package/dist/src/sqlite-kv-store.js.map +1 -0
- package/dist/src/sqlite-storage.d.ts +38 -0
- package/dist/src/sqlite-storage.d.ts.map +1 -0
- package/dist/src/sqlite-storage.js +133 -0
- package/dist/src/sqlite-storage.js.map +1 -0
- package/package.json +70 -0
- package/src/db.ts +131 -0
- package/src/identity.ts +34 -0
- package/src/index.ts +6 -0
- package/src/logger.ts +7 -0
- package/src/ns-opener.ts +112 -0
- package/src/sqlite-kv-store.ts +61 -0
- package/src/sqlite-storage.ts +163 -0
|
@@ -0,0 +1,38 @@
|
|
|
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 } from './db.js';
|
|
4
|
+
/**
|
|
5
|
+
* SQLite-backed `IRawStorage` implementation for NativeScript peers.
|
|
6
|
+
*
|
|
7
|
+
* Every CRUD operation goes through a prepared statement bound once in the
|
|
8
|
+
* constructor. `listRevisions` and `listPendingTransactions` issue
|
|
9
|
+
* `SELECT … ORDER BY` queries bounded by `block_id` (and `rev` for revisions)
|
|
10
|
+
* and drain the results into an array before yielding — matching the
|
|
11
|
+
* `IndexedDBRawStorage` pattern, where holding a cursor live across consumer
|
|
12
|
+
* awaits would invalidate the underlying transaction.
|
|
13
|
+
*
|
|
14
|
+
* `promotePendingTransaction` runs as a single `BEGIN; INSERT…; DELETE…; COMMIT;`
|
|
15
|
+
* via `SqliteDb.transaction(fn)`, so the move is atomic across crashes —
|
|
16
|
+
* unlike the MMKV adapter, which has to maintain a separate pending-index row.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SqliteRawStorage implements IRawStorage {
|
|
19
|
+
private readonly db;
|
|
20
|
+
private readonly stmts;
|
|
21
|
+
constructor(db: SqliteDb);
|
|
22
|
+
getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined>;
|
|
23
|
+
saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void>;
|
|
24
|
+
getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined>;
|
|
25
|
+
saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void>;
|
|
26
|
+
listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev>;
|
|
27
|
+
getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
|
|
28
|
+
savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
|
|
29
|
+
deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
30
|
+
listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId>;
|
|
31
|
+
getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
|
|
32
|
+
saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
|
|
33
|
+
getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined>;
|
|
34
|
+
saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void>;
|
|
35
|
+
getApproximateBytesUsed(): Promise<number>;
|
|
36
|
+
promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=sqlite-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-storage.d.ts","sourceRoot":"","sources":["../../src/sqlite-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC3F,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAKzD;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAiB,YAAW,WAAW;IAqBvC,OAAO,CAAC,QAAQ,CAAC,EAAE;IApB/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAkBpB;gBAE2B,EAAE,EAAE,QAAQ;IAsBnC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAMjE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAKzE,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7E,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC;IAW5F,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAM3F,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjG,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5E,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC;IAOnE,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAMpF,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAMvF,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1F,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAa1C,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAUpF"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
const log = createLogger('storage:sqlite');
|
|
3
|
+
/**
|
|
4
|
+
* SQLite-backed `IRawStorage` implementation for NativeScript peers.
|
|
5
|
+
*
|
|
6
|
+
* Every CRUD operation goes through a prepared statement bound once in the
|
|
7
|
+
* constructor. `listRevisions` and `listPendingTransactions` issue
|
|
8
|
+
* `SELECT … ORDER BY` queries bounded by `block_id` (and `rev` for revisions)
|
|
9
|
+
* and drain the results into an array before yielding — matching the
|
|
10
|
+
* `IndexedDBRawStorage` pattern, where holding a cursor live across consumer
|
|
11
|
+
* awaits would invalidate the underlying transaction.
|
|
12
|
+
*
|
|
13
|
+
* `promotePendingTransaction` runs as a single `BEGIN; INSERT…; DELETE…; COMMIT;`
|
|
14
|
+
* via `SqliteDb.transaction(fn)`, so the move is atomic across crashes —
|
|
15
|
+
* unlike the MMKV adapter, which has to maintain a separate pending-index row.
|
|
16
|
+
*/
|
|
17
|
+
export class SqliteRawStorage {
|
|
18
|
+
db;
|
|
19
|
+
stmts;
|
|
20
|
+
constructor(db) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.stmts = {
|
|
23
|
+
getMetadata: db.prepare('SELECT value FROM metadata WHERE block_id = ?'),
|
|
24
|
+
saveMetadata: db.prepare('INSERT OR REPLACE INTO metadata (block_id, value) VALUES (?, ?)'),
|
|
25
|
+
getRevision: db.prepare('SELECT action_id FROM revisions WHERE block_id = ? AND rev = ?'),
|
|
26
|
+
saveRevision: db.prepare('INSERT OR REPLACE INTO revisions (block_id, rev, action_id) VALUES (?, ?, ?)'),
|
|
27
|
+
listRevisionsAsc: db.prepare('SELECT rev, action_id FROM revisions WHERE block_id = ? AND rev BETWEEN ? AND ? ORDER BY rev ASC'),
|
|
28
|
+
listRevisionsDesc: db.prepare('SELECT rev, action_id FROM revisions WHERE block_id = ? AND rev BETWEEN ? AND ? ORDER BY rev DESC'),
|
|
29
|
+
getPending: db.prepare('SELECT value FROM pending WHERE block_id = ? AND action_id = ?'),
|
|
30
|
+
savePending: db.prepare('INSERT OR REPLACE INTO pending (block_id, action_id, value) VALUES (?, ?, ?)'),
|
|
31
|
+
deletePending: db.prepare('DELETE FROM pending WHERE block_id = ? AND action_id = ?'),
|
|
32
|
+
listPending: db.prepare('SELECT action_id FROM pending WHERE block_id = ? ORDER BY action_id ASC'),
|
|
33
|
+
getTransaction: db.prepare('SELECT value FROM transactions WHERE block_id = ? AND action_id = ?'),
|
|
34
|
+
saveTransaction: db.prepare('INSERT OR REPLACE INTO transactions (block_id, action_id, value) VALUES (?, ?, ?)'),
|
|
35
|
+
getMaterialized: db.prepare('SELECT value FROM materialized WHERE block_id = ? AND action_id = ?'),
|
|
36
|
+
saveMaterialized: db.prepare('INSERT OR REPLACE INTO materialized (block_id, action_id, value) VALUES (?, ?, ?)'),
|
|
37
|
+
deleteMaterialized: db.prepare('DELETE FROM materialized WHERE block_id = ? AND action_id = ?'),
|
|
38
|
+
pageCount: db.prepare('PRAGMA page_count'),
|
|
39
|
+
pageSize: db.prepare('PRAGMA page_size'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async getMetadata(blockId) {
|
|
43
|
+
const row = await this.stmts.getMetadata.get(blockId);
|
|
44
|
+
if (!row)
|
|
45
|
+
return undefined;
|
|
46
|
+
return JSON.parse(row.value);
|
|
47
|
+
}
|
|
48
|
+
async saveMetadata(blockId, metadata) {
|
|
49
|
+
await this.stmts.saveMetadata.run(blockId, JSON.stringify(metadata));
|
|
50
|
+
}
|
|
51
|
+
async getRevision(blockId, rev) {
|
|
52
|
+
const row = await this.stmts.getRevision.get(blockId, rev);
|
|
53
|
+
return row ? row.action_id : undefined;
|
|
54
|
+
}
|
|
55
|
+
async saveRevision(blockId, rev, actionId) {
|
|
56
|
+
await this.stmts.saveRevision.run(blockId, rev, actionId);
|
|
57
|
+
}
|
|
58
|
+
async *listRevisions(blockId, startRev, endRev) {
|
|
59
|
+
const ascending = startRev <= endRev;
|
|
60
|
+
const lo = ascending ? startRev : endRev;
|
|
61
|
+
const hi = ascending ? endRev : startRev;
|
|
62
|
+
const stmt = ascending ? this.stmts.listRevisionsAsc : this.stmts.listRevisionsDesc;
|
|
63
|
+
const rows = await stmt.all(blockId, lo, hi);
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
yield { rev: row.rev, actionId: row.action_id };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async getPendingTransaction(blockId, actionId) {
|
|
69
|
+
const row = await this.stmts.getPending.get(blockId, actionId);
|
|
70
|
+
if (!row)
|
|
71
|
+
return undefined;
|
|
72
|
+
return JSON.parse(row.value);
|
|
73
|
+
}
|
|
74
|
+
async savePendingTransaction(blockId, actionId, transform) {
|
|
75
|
+
await this.stmts.savePending.run(blockId, actionId, JSON.stringify(transform));
|
|
76
|
+
}
|
|
77
|
+
async deletePendingTransaction(blockId, actionId) {
|
|
78
|
+
await this.stmts.deletePending.run(blockId, actionId);
|
|
79
|
+
}
|
|
80
|
+
async *listPendingTransactions(blockId) {
|
|
81
|
+
const rows = await this.stmts.listPending.all(blockId);
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
yield row.action_id;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async getTransaction(blockId, actionId) {
|
|
87
|
+
const row = await this.stmts.getTransaction.get(blockId, actionId);
|
|
88
|
+
if (!row)
|
|
89
|
+
return undefined;
|
|
90
|
+
return JSON.parse(row.value);
|
|
91
|
+
}
|
|
92
|
+
async saveTransaction(blockId, actionId, transform) {
|
|
93
|
+
await this.stmts.saveTransaction.run(blockId, actionId, JSON.stringify(transform));
|
|
94
|
+
}
|
|
95
|
+
async getMaterializedBlock(blockId, actionId) {
|
|
96
|
+
const row = await this.stmts.getMaterialized.get(blockId, actionId);
|
|
97
|
+
if (!row)
|
|
98
|
+
return undefined;
|
|
99
|
+
return JSON.parse(row.value);
|
|
100
|
+
}
|
|
101
|
+
async saveMaterializedBlock(blockId, actionId, block) {
|
|
102
|
+
if (block) {
|
|
103
|
+
await this.stmts.saveMaterialized.run(blockId, actionId, JSON.stringify(block));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await this.stmts.deleteMaterialized.run(blockId, actionId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async getApproximateBytesUsed() {
|
|
110
|
+
try {
|
|
111
|
+
const pageCountRow = await this.stmts.pageCount.get();
|
|
112
|
+
const pageSizeRow = await this.stmts.pageSize.get();
|
|
113
|
+
const pages = pageCountRow?.page_count ?? 0;
|
|
114
|
+
const size = pageSizeRow?.page_size ?? 0;
|
|
115
|
+
return pages * size;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
log('PRAGMA page_count/page_size failed: %o', err);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async promotePendingTransaction(blockId, actionId) {
|
|
123
|
+
await this.db.transaction(async () => {
|
|
124
|
+
const row = await this.stmts.getPending.get(blockId, actionId);
|
|
125
|
+
if (!row) {
|
|
126
|
+
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
127
|
+
}
|
|
128
|
+
await this.stmts.saveTransaction.run(blockId, actionId, row.value);
|
|
129
|
+
await this.stmts.deletePending.run(blockId, actionId);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=sqlite-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-storage.js","sourceRoot":"","sources":["../../src/sqlite-storage.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,GAAG,GAAG,YAAY,CAAC,gBAAgB,CAAC,CAAC;AAE3C;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,gBAAgB;IAqBC;IApBZ,KAAK,CAkBpB;IAEF,YAA6B,EAAY;QAAZ,OAAE,GAAF,EAAE,CAAU;QACxC,IAAI,CAAC,KAAK,GAAG;YACZ,WAAW,EAAE,EAAE,CAAC,OAAO,CAAC,+CAA+C,CAAC;YACxE,YAAY,EAAE,EAAE,CAAC,OAAO,CAAC,iEAAiE,CAAC;YAC3F,WAAW,EAAE,EAAE,CAAC,OAAO,CAAC,gEAAgE,CAAC;YACzF,YAAY,EAAE,EAAE,CAAC,OAAO,CAAC,8EAA8E,CAAC;YACxG,gBAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,kGAAkG,CAAC;YAChI,iBAAiB,EAAE,EAAE,CAAC,OAAO,CAAC,mGAAmG,CAAC;YAClI,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,gEAAgE,CAAC;YACxF,WAAW,EAAE,EAAE,CAAC,OAAO,CAAC,8EAA8E,CAAC;YACvG,aAAa,EAAE,EAAE,CAAC,OAAO,CAAC,0DAA0D,CAAC;YACrF,WAAW,EAAE,EAAE,CAAC,OAAO,CAAC,yEAAyE,CAAC;YAClG,cAAc,EAAE,EAAE,CAAC,OAAO,CAAC,qEAAqE,CAAC;YACjG,eAAe,EAAE,EAAE,CAAC,OAAO,CAAC,mFAAmF,CAAC;YAChH,eAAe,EAAE,EAAE,CAAC,OAAO,CAAC,qEAAqE,CAAC;YAClG,gBAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,mFAAmF,CAAC;YACjH,kBAAkB,EAAE,EAAE,CAAC,OAAO,CAAC,+DAA+D,CAAC;YAC/F,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAC;YAC1C,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC;SACxC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAgB;QACjC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACtD,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAe,CAAkB,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAgB,EAAE,QAAuB;QAC3D,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAgB,EAAE,GAAW;QAC9C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC3D,OAAO,GAAG,CAAC,CAAC,CAAE,GAAG,CAAC,SAAsB,CAAC,CAAC,CAAC,SAAS,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAgB,EAAE,GAAW,EAAE,QAAkB;QACnE,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,CAAC,aAAa,CAAC,OAAgB,EAAE,QAAgB,EAAE,MAAc;QACtE,MAAM,SAAS,GAAG,QAAQ,IAAI,MAAM,CAAC;QACrC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;QACzC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;QACzC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC;QACpF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAC7C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GAAa,EAAE,QAAQ,EAAE,GAAG,CAAC,SAAqB,EAAE,CAAC;QACvE,CAAC;IACF,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,OAAgB,EAAE,QAAkB;QAC/D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC/D,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAe,CAAc,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,OAAgB,EAAE,QAAkB,EAAE,SAAoB;QACtF,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;IAChF,CAAC;IAED,KAAK,CAAC,wBAAwB,CAAC,OAAgB,EAAE,QAAkB;QAClE,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,CAAC,uBAAuB,CAAC,OAAgB;QAC9C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,GAAG,CAAC,SAAqB,CAAC;QACjC,CAAC;IACF,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,OAAgB,EAAE,QAAkB;QACxD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACnE,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAe,CAAc,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,OAAgB,EAAE,QAAkB,EAAE,SAAoB;QAC/E,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,OAAgB,EAAE,QAAkB;QAC9D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACpE,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAe,CAAW,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,OAAgB,EAAE,QAAkB,EAAE,KAAc;QAC/E,IAAI,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACjF,CAAC;aAAM,CAAC;YACP,MAAM,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC5D,CAAC;IACF,CAAC;IAED,KAAK,CAAC,uBAAuB;QAC5B,IAAI,CAAC;YACJ,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;YACtD,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACpD,MAAM,KAAK,GAAI,YAAY,EAAE,UAAiC,IAAI,CAAC,CAAC;YACpE,MAAM,IAAI,GAAI,WAAW,EAAE,SAAgC,IAAI,CAAC,CAAC;YACjE,OAAO,KAAK,GAAG,IAAI,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,GAAG,CAAC,wCAAwC,EAAE,GAAG,CAAC,CAAC;YACnD,OAAO,CAAC,CAAC;QACV,CAAC;IACF,CAAC;IAED,KAAK,CAAC,yBAAyB,CAAC,OAAgB,EAAE,QAAkB;QACnE,MAAM,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE;YACpC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC/D,IAAI,CAAC,GAAG,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,wBAAwB,OAAO,EAAE,CAAC,CAAC;YAC9E,CAAC;YACD,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAe,CAAC,CAAC;YAC7E,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;IACJ,CAAC;CACD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optimystic/db-p2p-storage-ns",
|
|
3
|
+
"version": "0.13.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "NativeScript SQLite storage backend for @optimystic/db-p2p",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"dist",
|
|
11
|
+
"!dist/test",
|
|
12
|
+
"!**/*.tsbuildinfo"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/src/index.d.ts",
|
|
17
|
+
"import": "./dist/src/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/gotchoices/optimystic.git",
|
|
23
|
+
"directory": "packages/db-p2p-storage-ns"
|
|
24
|
+
},
|
|
25
|
+
"author": "Got Choices Foundation",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"optimystic",
|
|
29
|
+
"database",
|
|
30
|
+
"p2p",
|
|
31
|
+
"storage",
|
|
32
|
+
"nativescript",
|
|
33
|
+
"sqlite"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"clean": "rimraf dist",
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"test": "node --no-warnings=ExperimentalWarning --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter min",
|
|
39
|
+
"test:verbose": "node --no-warnings=ExperimentalWarning --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter spec"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@libp2p/crypto": "^5.1.13",
|
|
43
|
+
"@libp2p/interface": "^3.1.0",
|
|
44
|
+
"@libp2p/peer-id": "^6.0.4",
|
|
45
|
+
"@types/chai": "^5.2.3",
|
|
46
|
+
"@types/debug": "^4.1.12",
|
|
47
|
+
"@types/mocha": "^10.0.10",
|
|
48
|
+
"@types/node": "^25.1.0",
|
|
49
|
+
"chai": "^6.2.2",
|
|
50
|
+
"mocha": "^11.7.5",
|
|
51
|
+
"rimraf": "^6.1.2",
|
|
52
|
+
"ts-node": "^10.9.2",
|
|
53
|
+
"typescript": "^5.9.3"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@optimystic/db-core": "^0.13.4",
|
|
57
|
+
"@optimystic/db-p2p": "^0.13.4",
|
|
58
|
+
"debug": "^4.4.3"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@libp2p/crypto": "^5.1.13",
|
|
62
|
+
"@libp2p/interface": "^3.1.0",
|
|
63
|
+
"@nativescript-community/sqlite": "^3.5.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@nativescript-community/sqlite": {
|
|
67
|
+
"optional": false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal SQLite shim and Optimystic schema for the NativeScript storage backend.
|
|
3
|
+
*
|
|
4
|
+
* The storage classes (`SqliteRawStorage`, `SqliteKVStore`) and the identity
|
|
5
|
+
* helper (`loadOrCreateNSPeerKey`) only depend on the `SqliteDb` interface
|
|
6
|
+
* defined here — never on `@nativescript-community/sqlite` directly. That lets
|
|
7
|
+
* the suite run under Node mocha against a `node:sqlite` or `better-sqlite3`
|
|
8
|
+
* driver, matching the pattern `db-p2p-storage-web` uses with `fake-indexeddb`.
|
|
9
|
+
*
|
|
10
|
+
* The wrapper is **package-private**: only `openOptimysticNSDb` is exported to
|
|
11
|
+
* consumers; the `SqliteDb` interface is an internal seam for tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** SQL parameter value — covers everything `IRawStorage` and `IKVStore` write. */
|
|
15
|
+
export type SqliteParam = string | number | Uint8Array | null;
|
|
16
|
+
|
|
17
|
+
/** A single row returned from a SQL query, keyed by column name. */
|
|
18
|
+
export type SqliteRow = Record<string, SqliteParam>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A prepared statement bound to a single SQL string.
|
|
22
|
+
*
|
|
23
|
+
* Both `node:sqlite` and `@nativescript-community/sqlite` cache parsed SQL
|
|
24
|
+
* internally, so re-binding a prepared statement is cheaper than re-parsing a
|
|
25
|
+
* raw `execSQL`. The storage classes prepare each query once per instance.
|
|
26
|
+
*/
|
|
27
|
+
export interface SqliteStatement {
|
|
28
|
+
/** Execute with the given bind parameters, ignoring any returned rows. */
|
|
29
|
+
run(...params: SqliteParam[]): Promise<void>;
|
|
30
|
+
/** Execute and return the first row, or `undefined` if none. */
|
|
31
|
+
get(...params: SqliteParam[]): Promise<SqliteRow | undefined>;
|
|
32
|
+
/** Execute and return all rows (already drained — safe to await between yields). */
|
|
33
|
+
all(...params: SqliteParam[]): Promise<SqliteRow[]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal SQLite driver surface used by this package.
|
|
38
|
+
*
|
|
39
|
+
* Wraps either the NativeScript plugin (in production) or a Node SQLite
|
|
40
|
+
* driver (in tests). Async on every method so the NS plugin's I/O can be
|
|
41
|
+
* Promised — even where the underlying call is synchronous.
|
|
42
|
+
*/
|
|
43
|
+
export interface SqliteDb {
|
|
44
|
+
/** Execute one or more semicolon-separated statements; no result rows. */
|
|
45
|
+
exec(sql: string): Promise<void>;
|
|
46
|
+
/** Prepare a parameterized statement for repeated execution. */
|
|
47
|
+
prepare(sql: string): SqliteStatement;
|
|
48
|
+
/** Run `fn` inside `BEGIN ... COMMIT` (rolls back on throw). */
|
|
49
|
+
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
50
|
+
/** Release the underlying handle. */
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_DB_NAME = 'optimystic.sqlite';
|
|
55
|
+
export const DEFAULT_DB_VERSION = 1;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schema for the NativeScript storage backend.
|
|
59
|
+
*
|
|
60
|
+
* Mirrors the IndexedDB object stores 1:1:
|
|
61
|
+
* - `metadata` → per-block `BlockMetadata` (JSON).
|
|
62
|
+
* - `revisions` → revision lookup keyed by `(block_id, rev)`.
|
|
63
|
+
* - `pending` → uncommitted transforms keyed by `(block_id, action_id)`.
|
|
64
|
+
* - `transactions` → committed transforms keyed by `(block_id, action_id)`.
|
|
65
|
+
* - `materialized` → materialized blocks keyed by `(block_id, action_id)`.
|
|
66
|
+
* - `kv` → generic string keyspace shared by `IKVStore` (`s_val`) and the
|
|
67
|
+
* identity helper (`b_val`). The two columns avoid a base64 round-trip
|
|
68
|
+
* for the libp2p private key.
|
|
69
|
+
*/
|
|
70
|
+
export const SCHEMA_SQL = `
|
|
71
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
72
|
+
block_id TEXT PRIMARY KEY,
|
|
73
|
+
value TEXT NOT NULL
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS revisions (
|
|
77
|
+
block_id TEXT NOT NULL,
|
|
78
|
+
rev INTEGER NOT NULL,
|
|
79
|
+
action_id TEXT NOT NULL,
|
|
80
|
+
PRIMARY KEY (block_id, rev)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS pending (
|
|
84
|
+
block_id TEXT NOT NULL,
|
|
85
|
+
action_id TEXT NOT NULL,
|
|
86
|
+
value TEXT NOT NULL,
|
|
87
|
+
PRIMARY KEY (block_id, action_id)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS transactions (
|
|
91
|
+
block_id TEXT NOT NULL,
|
|
92
|
+
action_id TEXT NOT NULL,
|
|
93
|
+
value TEXT NOT NULL,
|
|
94
|
+
PRIMARY KEY (block_id, action_id)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS materialized (
|
|
98
|
+
block_id TEXT NOT NULL,
|
|
99
|
+
action_id TEXT NOT NULL,
|
|
100
|
+
value TEXT NOT NULL,
|
|
101
|
+
PRIMARY KEY (block_id, action_id)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
105
|
+
key TEXT PRIMARY KEY,
|
|
106
|
+
s_val TEXT,
|
|
107
|
+
b_val BLOB
|
|
108
|
+
);
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
const PRAGMAS_SQL = `
|
|
112
|
+
PRAGMA journal_mode = WAL;
|
|
113
|
+
PRAGMA synchronous = NORMAL;
|
|
114
|
+
PRAGMA foreign_keys = OFF;
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Apply pragmas, run the schema DDL, and stamp `user_version` so future
|
|
119
|
+
* migrations can branch on it. Idempotent — safe to call on every open.
|
|
120
|
+
*/
|
|
121
|
+
export async function applySchema(db: SqliteDb, version: number = DEFAULT_DB_VERSION): Promise<void> {
|
|
122
|
+
await db.exec(PRAGMAS_SQL);
|
|
123
|
+
await db.exec(SCHEMA_SQL);
|
|
124
|
+
await db.exec(`PRAGMA user_version = ${version}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Public handle returned by `openOptimysticNSDb`. Consumers pass it to
|
|
129
|
+
* `SqliteRawStorage`, `SqliteKVStore`, and `loadOrCreateNSPeerKey`.
|
|
130
|
+
*/
|
|
131
|
+
export type OptimysticNSDBHandle = SqliteDb;
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
2
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
3
|
+
import type { SqliteDb } from './db.js';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PEER_KEY_NAME = 'peer-private-key';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loads the persisted NativeScript peer's libp2p private key, or generates a
|
|
9
|
+
* fresh Ed25519 key on first call and persists it.
|
|
10
|
+
*
|
|
11
|
+
* The key is stored as raw protobuf bytes in the `kv` table's `b_val` column
|
|
12
|
+
* under `keyName`. SQLite stores BLOB natively, so there is no base64
|
|
13
|
+
* round-trip — unlike text-only key/value stores.
|
|
14
|
+
*
|
|
15
|
+
* The returned `PrivateKey` can be passed directly to
|
|
16
|
+
* `createLibp2pNode({ privateKey })`, giving a NativeScript peer a stable,
|
|
17
|
+
* restart-surviving identity.
|
|
18
|
+
*/
|
|
19
|
+
export async function loadOrCreateNSPeerKey(
|
|
20
|
+
db: SqliteDb,
|
|
21
|
+
keyName: string = DEFAULT_PEER_KEY_NAME,
|
|
22
|
+
): Promise<PrivateKey> {
|
|
23
|
+
const row = await db.prepare('SELECT b_val FROM kv WHERE key = ?').get(keyName);
|
|
24
|
+
const stored = row?.b_val;
|
|
25
|
+
if (stored instanceof Uint8Array && stored.length > 0) {
|
|
26
|
+
return privateKeyFromProtobuf(stored);
|
|
27
|
+
}
|
|
28
|
+
const key = await generateKeyPair('Ed25519');
|
|
29
|
+
const bytes = privateKeyToProtobuf(key);
|
|
30
|
+
await db
|
|
31
|
+
.prepare('INSERT INTO kv (key, s_val, b_val) VALUES (?, NULL, ?) ON CONFLICT(key) DO UPDATE SET b_val = excluded.b_val')
|
|
32
|
+
.run(keyName, bytes);
|
|
33
|
+
return key;
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { OptimysticNSDBHandle, SqliteDb, SqliteStatement, SqliteParam, SqliteRow } from './db.js';
|
|
2
|
+
export { DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
|
|
3
|
+
export { openOptimysticNSDb, wrapNSPluginDb } from './ns-opener.js';
|
|
4
|
+
export { SqliteRawStorage } from './sqlite-storage.js';
|
|
5
|
+
export { SqliteKVStore } from './sqlite-kv-store.js';
|
|
6
|
+
export { loadOrCreateNSPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
|
package/src/logger.ts
ADDED
package/src/ns-opener.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { applySchema, DEFAULT_DB_NAME, DEFAULT_DB_VERSION, type OptimysticNSDBHandle, type SqliteDb, type SqliteParam, type SqliteRow, type SqliteStatement } from './db.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal subset of `@nativescript-community/sqlite`'s `Db` we depend on.
|
|
5
|
+
* Re-declared locally so this module's *types* don't pull in the plugin's
|
|
6
|
+
* declarations (the plugin is a peer dependency that may not be installed
|
|
7
|
+
* at typecheck time on non-NativeScript consumers).
|
|
8
|
+
*/
|
|
9
|
+
interface NSPluginDb {
|
|
10
|
+
execSQL(sql: string, params?: ReadonlyArray<unknown>): unknown;
|
|
11
|
+
get(sql: string, params?: ReadonlyArray<unknown>): Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;
|
|
12
|
+
select(sql: string, params?: ReadonlyArray<unknown>): Promise<Array<Record<string, unknown>>> | Array<Record<string, unknown>>;
|
|
13
|
+
close(): void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface NSPluginModule {
|
|
17
|
+
openOrCreate(path: string): NSPluginDb | Promise<NSPluginDb>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Opens (creating if needed) the Optimystic SQLite database at `name` under
|
|
22
|
+
* the NativeScript app's documents directory, applies WAL pragmas, and runs
|
|
23
|
+
* the migration to `version`.
|
|
24
|
+
*
|
|
25
|
+
* The returned `SqliteDb` handle is safe to share across `SqliteRawStorage`,
|
|
26
|
+
* `SqliteKVStore`, and `loadOrCreateNSPeerKey` — SQLite serializes writes
|
|
27
|
+
* inside the connection, and our reads/writes are short-lived enough that
|
|
28
|
+
* single-connection contention is a non-issue for a client peer.
|
|
29
|
+
*
|
|
30
|
+
* `path` may be passed in as the full filesystem path if the caller wants
|
|
31
|
+
* to control file placement; otherwise the plugin's documents-directory
|
|
32
|
+
* default is used.
|
|
33
|
+
*/
|
|
34
|
+
export async function openOptimysticNSDb(
|
|
35
|
+
name: string = DEFAULT_DB_NAME,
|
|
36
|
+
version: number = DEFAULT_DB_VERSION,
|
|
37
|
+
): Promise<OptimysticNSDBHandle> {
|
|
38
|
+
const plugin = (await import('@nativescript-community/sqlite' as string)) as unknown as NSPluginModule;
|
|
39
|
+
const raw = await plugin.openOrCreate(name);
|
|
40
|
+
const wrapped = wrapNSPluginDb(raw);
|
|
41
|
+
await applySchema(wrapped, version);
|
|
42
|
+
return wrapped;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wraps a `@nativescript-community/sqlite` `Db` instance to satisfy the
|
|
47
|
+
* internal `SqliteDb` interface used by the storage classes. Exported only
|
|
48
|
+
* for callers that already hold an open NS-plugin handle and want to skip
|
|
49
|
+
* the opener (rare — typically users just call `openOptimysticNSDb`).
|
|
50
|
+
*/
|
|
51
|
+
export function wrapNSPluginDb(raw: NSPluginDb): SqliteDb {
|
|
52
|
+
return new NSPluginDbWrapper(raw);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class NSPluginDbWrapper implements SqliteDb {
|
|
56
|
+
constructor(private readonly raw: NSPluginDb) {}
|
|
57
|
+
|
|
58
|
+
async exec(sql: string): Promise<void> {
|
|
59
|
+
// The plugin's execSQL accepts a single statement; split semicolon-
|
|
60
|
+
// separated DDL so callers can pass the full schema in one go.
|
|
61
|
+
const statements = sql
|
|
62
|
+
.split(';')
|
|
63
|
+
.map(s => s.trim())
|
|
64
|
+
.filter(s => s.length > 0);
|
|
65
|
+
for (const statement of statements) {
|
|
66
|
+
await this.raw.execSQL(statement);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
prepare(sql: string): SqliteStatement {
|
|
71
|
+
return new NSPluginStatement(this.raw, sql);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async transaction<T>(fn: () => Promise<T>): Promise<T> {
|
|
75
|
+
await this.raw.execSQL('BEGIN');
|
|
76
|
+
try {
|
|
77
|
+
const result = await fn();
|
|
78
|
+
await this.raw.execSQL('COMMIT');
|
|
79
|
+
return result;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
try {
|
|
82
|
+
await this.raw.execSQL('ROLLBACK');
|
|
83
|
+
} catch {
|
|
84
|
+
// Swallow rollback failures so we surface the original error.
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async close(): Promise<void> {
|
|
91
|
+
await this.raw.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class NSPluginStatement implements SqliteStatement {
|
|
96
|
+
constructor(private readonly raw: NSPluginDb, private readonly sql: string) {}
|
|
97
|
+
|
|
98
|
+
async run(...params: SqliteParam[]): Promise<void> {
|
|
99
|
+
await this.raw.execSQL(this.sql, params);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async get(...params: SqliteParam[]): Promise<SqliteRow | undefined> {
|
|
103
|
+
const row = await this.raw.get(this.sql, params);
|
|
104
|
+
if (row === null || row === undefined) return undefined;
|
|
105
|
+
return row as SqliteRow;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async all(...params: SqliteParam[]): Promise<SqliteRow[]> {
|
|
109
|
+
const rows = await this.raw.select(this.sql, params);
|
|
110
|
+
return rows as SqliteRow[];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { IKVStore } from '@optimystic/db-p2p';
|
|
2
|
+
import type { SqliteDb, SqliteStatement } from './db.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLite-backed `IKVStore` adapter for NativeScript peers.
|
|
6
|
+
*
|
|
7
|
+
* Stored keys are namespaced with `prefix` so the `kv` table can be shared
|
|
8
|
+
* with the identity helper (which uses the `b_val` column) without collisions.
|
|
9
|
+
* `list(prefix)` issues a bounded `SELECT key FROM kv WHERE key >= ? AND key < ?`
|
|
10
|
+
* — never `SELECT * FROM kv` + JS-side filter — so listing latency stays
|
|
11
|
+
* bounded as the table grows.
|
|
12
|
+
*
|
|
13
|
+
* Only the `s_val` column is read or written here; the `b_val` column is
|
|
14
|
+
* reserved for binary identity material.
|
|
15
|
+
*/
|
|
16
|
+
export class SqliteKVStore implements IKVStore {
|
|
17
|
+
private readonly stmts: {
|
|
18
|
+
get: SqliteStatement;
|
|
19
|
+
set: SqliteStatement;
|
|
20
|
+
delete: SqliteStatement;
|
|
21
|
+
list: SqliteStatement;
|
|
22
|
+
};
|
|
23
|
+
private readonly prefix: string;
|
|
24
|
+
|
|
25
|
+
constructor(db: SqliteDb, prefix: string = 'optimystic:txn:') {
|
|
26
|
+
this.prefix = prefix;
|
|
27
|
+
this.stmts = {
|
|
28
|
+
get: db.prepare('SELECT s_val FROM kv WHERE key = ?'),
|
|
29
|
+
set: db.prepare('INSERT INTO kv (key, s_val, b_val) VALUES (?, ?, NULL) ON CONFLICT(key) DO UPDATE SET s_val = excluded.s_val'),
|
|
30
|
+
delete: db.prepare('DELETE FROM kv WHERE key = ?'),
|
|
31
|
+
list: db.prepare('SELECT key FROM kv WHERE key >= ? AND key < ? ORDER BY key ASC'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(key: string): Promise<string | undefined> {
|
|
36
|
+
const row = await this.stmts.get.get(this.prefix + key);
|
|
37
|
+
if (!row) return undefined;
|
|
38
|
+
const value = row.s_val;
|
|
39
|
+
return typeof value === 'string' ? value : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async set(key: string, value: string): Promise<void> {
|
|
43
|
+
await this.stmts.set.run(this.prefix + key, value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async delete(key: string): Promise<void> {
|
|
47
|
+
await this.stmts.delete.run(this.prefix + key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async list(prefix: string): Promise<string[]> {
|
|
51
|
+
const fullPrefix = this.prefix + prefix;
|
|
52
|
+
// SQLite TEXT columns compare with BINARY collation by default — byte-by-byte
|
|
53
|
+
// over the UTF-8 encoding. U+10FFFF (`\u{10FFFF}`) is the highest Unicode
|
|
54
|
+
// code point, encoded as `F4 8F BF BF`; since the only valid UTF-8 leading
|
|
55
|
+
// bytes go up to 0xF4, any string starting with `fullPrefix` sorts strictly
|
|
56
|
+
// below `fullPrefix + '\u{10FFFF}'`, so the bound is exact for all valid keys.
|
|
57
|
+
const upper = fullPrefix + '\u{10FFFF}';
|
|
58
|
+
const rows = await this.stmts.list.all(fullPrefix, upper);
|
|
59
|
+
return rows.map(row => (row.key as string).slice(this.prefix.length));
|
|
60
|
+
}
|
|
61
|
+
}
|