cojson-storage-sqlite 0.8.34 → 0.8.36
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/CHANGELOG.md +21 -0
- package/dist/index.js +1 -351
- package/dist/index.js.map +1 -1
- package/dist/sqliteClient.js +76 -0
- package/dist/sqliteClient.js.map +1 -0
- package/dist/sqliteNode.js +91 -0
- package/dist/sqliteNode.js.map +1 -0
- package/package.json +4 -3
- package/src/index.ts +1 -612
- package/src/sqliteClient.ts +153 -0
- package/src/sqliteNode.ts +181 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Database as DatabaseT } from "better-sqlite3";
|
|
2
|
+
import { CojsonInternalTypes, OutgoingSyncQueue, SessionID } from "cojson";
|
|
3
|
+
import RawCoID = CojsonInternalTypes.RawCoID;
|
|
4
|
+
import Signature = CojsonInternalTypes.Signature;
|
|
5
|
+
import Transaction = CojsonInternalTypes.Transaction;
|
|
6
|
+
import {
|
|
7
|
+
DBClientInterface,
|
|
8
|
+
SessionRow,
|
|
9
|
+
SignatureAfterRow,
|
|
10
|
+
StoredCoValueRow,
|
|
11
|
+
StoredSessionRow,
|
|
12
|
+
TransactionRow,
|
|
13
|
+
} from "cojson-storage";
|
|
14
|
+
|
|
15
|
+
export type RawCoValueRow = {
|
|
16
|
+
id: CojsonInternalTypes.RawCoID;
|
|
17
|
+
header: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RawTransactionRow = {
|
|
21
|
+
ses: number;
|
|
22
|
+
idx: number;
|
|
23
|
+
tx: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class SQLiteClient implements DBClientInterface {
|
|
27
|
+
private readonly db: DatabaseT;
|
|
28
|
+
private readonly toLocalNode: OutgoingSyncQueue;
|
|
29
|
+
|
|
30
|
+
constructor(db: DatabaseT, toLocalNode: OutgoingSyncQueue) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
this.toLocalNode = toLocalNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getCoValue(coValueId: RawCoID): StoredCoValueRow | undefined {
|
|
36
|
+
const coValueRow = this.db
|
|
37
|
+
.prepare(`SELECT * FROM coValues WHERE id = ?`)
|
|
38
|
+
.get(coValueId) as RawCoValueRow & { rowID: number };
|
|
39
|
+
|
|
40
|
+
if (!coValueRow) return;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsedHeader = (coValueRow?.header &&
|
|
44
|
+
JSON.parse(coValueRow.header)) as CojsonInternalTypes.CoValueHeader;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...coValueRow,
|
|
48
|
+
header: parsedHeader,
|
|
49
|
+
};
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn(coValueId, "Invalid JSON in header", e, coValueRow?.header);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getCoValueSessions(coValueRowId: number): StoredSessionRow[] {
|
|
57
|
+
return this.db
|
|
58
|
+
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
|
|
59
|
+
.all(coValueRowId) as StoredSessionRow[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getNewTransactionInSession(
|
|
63
|
+
sessionRowId: number,
|
|
64
|
+
firstNewTxIdx: number,
|
|
65
|
+
): TransactionRow[] {
|
|
66
|
+
const txs = this.db
|
|
67
|
+
.prepare<[number, number]>(
|
|
68
|
+
`SELECT * FROM transactions WHERE ses = ? AND idx >= ?`,
|
|
69
|
+
)
|
|
70
|
+
.all(sessionRowId, firstNewTxIdx) as RawTransactionRow[];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return txs.map((transactionRow) => ({
|
|
74
|
+
...transactionRow,
|
|
75
|
+
tx: JSON.parse(transactionRow.tx) as Transaction,
|
|
76
|
+
}));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn("Invalid JSON in transaction", e);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getSignatures(
|
|
84
|
+
sessionRowId: number,
|
|
85
|
+
firstNewTxIdx: number,
|
|
86
|
+
): SignatureAfterRow[] {
|
|
87
|
+
return this.db
|
|
88
|
+
.prepare<[number, number]>(
|
|
89
|
+
`SELECT * FROM signatureAfter WHERE ses = ? AND idx >= ?`,
|
|
90
|
+
)
|
|
91
|
+
.all(sessionRowId, firstNewTxIdx) as SignatureAfterRow[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
addCoValue(msg: CojsonInternalTypes.NewContentMessage): number {
|
|
95
|
+
return this.db
|
|
96
|
+
.prepare<[CojsonInternalTypes.RawCoID, string]>(
|
|
97
|
+
`INSERT INTO coValues (id, header) VALUES (?, ?)`,
|
|
98
|
+
)
|
|
99
|
+
.run(msg.id, JSON.stringify(msg.header)).lastInsertRowid as number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
addSessionUpdate({
|
|
103
|
+
sessionUpdate,
|
|
104
|
+
sessionRow,
|
|
105
|
+
}: {
|
|
106
|
+
sessionUpdate: SessionRow;
|
|
107
|
+
sessionRow?: StoredSessionRow;
|
|
108
|
+
}): number {
|
|
109
|
+
return (
|
|
110
|
+
this.db
|
|
111
|
+
.prepare<[number, string, number, string, number | undefined]>(
|
|
112
|
+
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
|
|
113
|
+
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
|
|
114
|
+
RETURNING rowID`,
|
|
115
|
+
)
|
|
116
|
+
.get(
|
|
117
|
+
sessionUpdate.coValue,
|
|
118
|
+
sessionUpdate.sessionID,
|
|
119
|
+
sessionUpdate.lastIdx,
|
|
120
|
+
sessionUpdate.lastSignature,
|
|
121
|
+
sessionUpdate.bytesSinceLastSignature,
|
|
122
|
+
) as { rowID: number }
|
|
123
|
+
).rowID;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
addTransaction(
|
|
127
|
+
sessionRowID: number,
|
|
128
|
+
nextIdx: number,
|
|
129
|
+
newTransaction: Transaction,
|
|
130
|
+
) {
|
|
131
|
+
this.db
|
|
132
|
+
.prepare<[number, number, string]>(
|
|
133
|
+
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`,
|
|
134
|
+
)
|
|
135
|
+
.run(sessionRowID, nextIdx, JSON.stringify(newTransaction));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
addSignatureAfter({
|
|
139
|
+
sessionRowID,
|
|
140
|
+
idx,
|
|
141
|
+
signature,
|
|
142
|
+
}: { sessionRowID: number; idx: number; signature: Signature }) {
|
|
143
|
+
this.db
|
|
144
|
+
.prepare<[number, number, string]>(
|
|
145
|
+
`INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)`,
|
|
146
|
+
)
|
|
147
|
+
.run(sessionRowID, idx, signature);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
unitOfWork(operationsCallback: () => any[]) {
|
|
151
|
+
this.db.transaction(operationsCallback)();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import Database, { Database as DatabaseT } from "better-sqlite3";
|
|
2
|
+
import {
|
|
3
|
+
IncomingSyncStream,
|
|
4
|
+
OutgoingSyncQueue,
|
|
5
|
+
Peer,
|
|
6
|
+
cojsonInternals,
|
|
7
|
+
} from "cojson";
|
|
8
|
+
import { SyncManager, TransactionRow } from "cojson-storage";
|
|
9
|
+
import { SQLiteClient } from "./sqliteClient.js";
|
|
10
|
+
|
|
11
|
+
export class SQLiteNode {
|
|
12
|
+
private readonly syncManager: SyncManager;
|
|
13
|
+
private readonly dbClient: SQLiteClient;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
db: DatabaseT,
|
|
17
|
+
fromLocalNode: IncomingSyncStream,
|
|
18
|
+
toLocalNode: OutgoingSyncQueue,
|
|
19
|
+
) {
|
|
20
|
+
this.dbClient = new SQLiteClient(db, toLocalNode);
|
|
21
|
+
this.syncManager = new SyncManager(this.dbClient, toLocalNode);
|
|
22
|
+
|
|
23
|
+
const processMessages = async () => {
|
|
24
|
+
for await (const msg of fromLocalNode) {
|
|
25
|
+
try {
|
|
26
|
+
if (msg === "Disconnected" || msg === "PingTimeout") {
|
|
27
|
+
throw new Error("Unexpected Disconnected message");
|
|
28
|
+
}
|
|
29
|
+
await this.syncManager.handleSyncMessage(msg);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(
|
|
32
|
+
new Error(
|
|
33
|
+
`Error reading from localNode, handling msg\n\n${JSON.stringify(
|
|
34
|
+
msg,
|
|
35
|
+
(k, v) =>
|
|
36
|
+
k === "changes" || k === "encryptedChanges"
|
|
37
|
+
? v.slice(0, 20) + "..."
|
|
38
|
+
: v,
|
|
39
|
+
)}`,
|
|
40
|
+
{ cause: e },
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
processMessages().catch((e) =>
|
|
48
|
+
console.error("Error in processMessages in sqlite", e),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static async asPeer({
|
|
53
|
+
filename,
|
|
54
|
+
trace,
|
|
55
|
+
localNodeName = "local",
|
|
56
|
+
}: {
|
|
57
|
+
filename: string;
|
|
58
|
+
trace?: boolean;
|
|
59
|
+
localNodeName?: string;
|
|
60
|
+
}): Promise<Peer> {
|
|
61
|
+
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
|
62
|
+
localNodeName,
|
|
63
|
+
"storage",
|
|
64
|
+
{ peer1role: "client", peer2role: "storage", trace, crashOnClose: true },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await SQLiteNode.open(
|
|
68
|
+
filename,
|
|
69
|
+
localNodeAsPeer.incoming,
|
|
70
|
+
localNodeAsPeer.outgoing,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return { ...storageAsPeer, priority: 100 };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async open(
|
|
77
|
+
filename: string,
|
|
78
|
+
fromLocalNode: IncomingSyncStream,
|
|
79
|
+
toLocalNode: OutgoingSyncQueue,
|
|
80
|
+
) {
|
|
81
|
+
const db = Database(filename);
|
|
82
|
+
db.pragma("journal_mode = WAL");
|
|
83
|
+
|
|
84
|
+
const oldVersion = (
|
|
85
|
+
db.pragma("user_version") as [{ user_version: number }]
|
|
86
|
+
)[0].user_version as number;
|
|
87
|
+
|
|
88
|
+
console.log("DB version", oldVersion);
|
|
89
|
+
|
|
90
|
+
if (oldVersion === 0) {
|
|
91
|
+
console.log("Migration 0 -> 1: Basic schema");
|
|
92
|
+
db.prepare(
|
|
93
|
+
`CREATE TABLE IF NOT EXISTS transactions (
|
|
94
|
+
ses INTEGER,
|
|
95
|
+
idx INTEGER,
|
|
96
|
+
tx TEXT NOT NULL,
|
|
97
|
+
PRIMARY KEY (ses, idx)
|
|
98
|
+
) WITHOUT ROWID;`,
|
|
99
|
+
).run();
|
|
100
|
+
|
|
101
|
+
db.prepare(
|
|
102
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
103
|
+
rowID INTEGER PRIMARY KEY,
|
|
104
|
+
coValue INTEGER NOT NULL,
|
|
105
|
+
sessionID TEXT NOT NULL,
|
|
106
|
+
lastIdx INTEGER,
|
|
107
|
+
lastSignature TEXT,
|
|
108
|
+
UNIQUE (sessionID, coValue)
|
|
109
|
+
);`,
|
|
110
|
+
).run();
|
|
111
|
+
|
|
112
|
+
db.prepare(
|
|
113
|
+
`CREATE INDEX IF NOT EXISTS sessionsByCoValue ON sessions (coValue);`,
|
|
114
|
+
).run();
|
|
115
|
+
|
|
116
|
+
db.prepare(
|
|
117
|
+
`CREATE TABLE IF NOT EXISTS coValues (
|
|
118
|
+
rowID INTEGER PRIMARY KEY,
|
|
119
|
+
id TEXT NOT NULL UNIQUE,
|
|
120
|
+
header TEXT NOT NULL UNIQUE
|
|
121
|
+
);`,
|
|
122
|
+
).run();
|
|
123
|
+
|
|
124
|
+
db.prepare(
|
|
125
|
+
`CREATE INDEX IF NOT EXISTS coValuesByID ON coValues (id);`,
|
|
126
|
+
).run();
|
|
127
|
+
|
|
128
|
+
db.pragma("user_version = 1");
|
|
129
|
+
console.log("Migration 0 -> 1: Basic schema - done");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (oldVersion <= 1) {
|
|
133
|
+
// fix embarrassing off-by-one error for transaction indices
|
|
134
|
+
console.log(
|
|
135
|
+
"Migration 1 -> 2: Fix off-by-one error for transaction indices",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const txs = db
|
|
139
|
+
.prepare(`SELECT * FROM transactions`)
|
|
140
|
+
.all() as TransactionRow[];
|
|
141
|
+
|
|
142
|
+
for (const tx of txs) {
|
|
143
|
+
db.prepare(`DELETE FROM transactions WHERE ses = ? AND idx = ?`).run(
|
|
144
|
+
tx.ses,
|
|
145
|
+
tx.idx,
|
|
146
|
+
);
|
|
147
|
+
tx.idx -= 1;
|
|
148
|
+
db.prepare(
|
|
149
|
+
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`,
|
|
150
|
+
).run(tx.ses, tx.idx, tx.tx);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
db.pragma("user_version = 2");
|
|
154
|
+
console.log(
|
|
155
|
+
"Migration 1 -> 2: Fix off-by-one error for transaction indices - done",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (oldVersion <= 2) {
|
|
160
|
+
console.log("Migration 2 -> 3: Add signatureAfter");
|
|
161
|
+
|
|
162
|
+
db.prepare(
|
|
163
|
+
`CREATE TABLE IF NOT EXISTS signatureAfter (
|
|
164
|
+
ses INTEGER,
|
|
165
|
+
idx INTEGER,
|
|
166
|
+
signature TEXT NOT NULL,
|
|
167
|
+
PRIMARY KEY (ses, idx)
|
|
168
|
+
) WITHOUT ROWID;`,
|
|
169
|
+
).run();
|
|
170
|
+
|
|
171
|
+
db.prepare(
|
|
172
|
+
`ALTER TABLE sessions ADD COLUMN bytesSinceLastSignature INTEGER;`,
|
|
173
|
+
).run();
|
|
174
|
+
|
|
175
|
+
db.pragma("user_version = 3");
|
|
176
|
+
console.log("Migration 2 -> 3: Add signatureAfter - done!!");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return new SQLiteNode(db, fromLocalNode, toLocalNode);
|
|
180
|
+
}
|
|
181
|
+
}
|