cojson-storage-sqlite 0.1.1
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/.eslintrc.cjs +17 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -0
- package/package.json +22 -0
- package/src/index.ts +383 -0
- package/tsconfig.json +15 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extends: [
|
|
3
|
+
'eslint:recommended',
|
|
4
|
+
'plugin:@typescript-eslint/recommended',
|
|
5
|
+
],
|
|
6
|
+
parser: '@typescript-eslint/parser',
|
|
7
|
+
plugins: ['@typescript-eslint'],
|
|
8
|
+
parserOptions: {
|
|
9
|
+
project: "./tsconfig.json",
|
|
10
|
+
},
|
|
11
|
+
root: true,
|
|
12
|
+
rules: {
|
|
13
|
+
"no-unused-vars": "off",
|
|
14
|
+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
|
15
|
+
// "@typescript-eslint/no-floating-promises": "error",
|
|
16
|
+
},
|
|
17
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SyncMessage, Peer, CojsonInternalTypes } from "cojson";
|
|
2
|
+
import { ReadableStream, WritableStream, ReadableStreamDefaultReader, WritableStreamDefaultWriter } from "isomorphic-streams";
|
|
3
|
+
import { Database as DatabaseT } from "better-sqlite3";
|
|
4
|
+
export declare class SQLiteStorage {
|
|
5
|
+
fromLocalNode: ReadableStreamDefaultReader<SyncMessage>;
|
|
6
|
+
toLocalNode: WritableStreamDefaultWriter<SyncMessage>;
|
|
7
|
+
db: DatabaseT;
|
|
8
|
+
constructor(db: DatabaseT, fromLocalNode: ReadableStream<SyncMessage>, toLocalNode: WritableStream<SyncMessage>);
|
|
9
|
+
static asPeer({ filename, trace, localNodeName, }: {
|
|
10
|
+
filename: string;
|
|
11
|
+
trace?: boolean;
|
|
12
|
+
localNodeName?: string;
|
|
13
|
+
}): Promise<Peer>;
|
|
14
|
+
static open(filename: string, fromLocalNode: ReadableStream<SyncMessage>, toLocalNode: WritableStream<SyncMessage>): Promise<SQLiteStorage>;
|
|
15
|
+
handleSyncMessage(msg: SyncMessage): Promise<void>;
|
|
16
|
+
sendNewContentAfter(theirKnown: CojsonInternalTypes.CoValueKnownState, asDependencyOf?: CojsonInternalTypes.RawCoID): Promise<void>;
|
|
17
|
+
handleLoad(msg: CojsonInternalTypes.LoadMessage): Promise<void>;
|
|
18
|
+
handleContent(msg: CojsonInternalTypes.NewContentMessage): Promise<void>;
|
|
19
|
+
handleKnown(msg: CojsonInternalTypes.KnownStateMessage): Promise<void>;
|
|
20
|
+
handleDone(_msg: CojsonInternalTypes.DoneMessage): void;
|
|
21
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { cojsonInternals,
|
|
2
|
+
// CojsonInternalTypes,
|
|
3
|
+
// SessionID,
|
|
4
|
+
} from "cojson";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
export class SQLiteStorage {
|
|
7
|
+
constructor(db, fromLocalNode, toLocalNode) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this.fromLocalNode = fromLocalNode.getReader();
|
|
10
|
+
this.toLocalNode = toLocalNode.getWriter();
|
|
11
|
+
(async () => {
|
|
12
|
+
let done = false;
|
|
13
|
+
while (!done) {
|
|
14
|
+
const result = await this.fromLocalNode.read();
|
|
15
|
+
done = result.done;
|
|
16
|
+
if (result.value) {
|
|
17
|
+
this.handleSyncMessage(result.value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
}
|
|
22
|
+
static async asPeer({ filename, trace, localNodeName = "local", }) {
|
|
23
|
+
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(localNodeName, "storage", { peer1role: "client", peer2role: "server", trace });
|
|
24
|
+
await SQLiteStorage.open(filename, localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
|
|
25
|
+
return storageAsPeer;
|
|
26
|
+
}
|
|
27
|
+
static async open(filename, fromLocalNode, toLocalNode) {
|
|
28
|
+
const db = Database(filename);
|
|
29
|
+
db.pragma("journal_mode = WAL");
|
|
30
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS transactions (
|
|
31
|
+
ses INTEGER,
|
|
32
|
+
idx INTEGER,
|
|
33
|
+
tx TEXT NOT NULL ,
|
|
34
|
+
PRIMARY KEY (ses, idx)
|
|
35
|
+
) WITHOUT ROWID;`).run();
|
|
36
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS sessions (
|
|
37
|
+
rowID INTEGER PRIMARY KEY,
|
|
38
|
+
coValue INTEGER NOT NULL,
|
|
39
|
+
sessionID TEXT NOT NULL,
|
|
40
|
+
lastIdx INTEGER,
|
|
41
|
+
lastSignature TEXT,
|
|
42
|
+
UNIQUE (sessionID, coValue)
|
|
43
|
+
);`).run();
|
|
44
|
+
db.prepare(`CREATE INDEX IF NOT EXISTS sessionsByCoValue ON sessions (coValue);`).run();
|
|
45
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS coValues (
|
|
46
|
+
rowID INTEGER PRIMARY KEY,
|
|
47
|
+
id TEXT NOT NULL UNIQUE,
|
|
48
|
+
header TEXT NOT NULL UNIQUE
|
|
49
|
+
);`).run();
|
|
50
|
+
db.prepare(`CREATE INDEX IF NOT EXISTS coValuesByID ON coValues (id);`).run();
|
|
51
|
+
return new SQLiteStorage(db, fromLocalNode, toLocalNode);
|
|
52
|
+
}
|
|
53
|
+
async handleSyncMessage(msg) {
|
|
54
|
+
switch (msg.action) {
|
|
55
|
+
case "load":
|
|
56
|
+
await this.handleLoad(msg);
|
|
57
|
+
break;
|
|
58
|
+
case "content":
|
|
59
|
+
await this.handleContent(msg);
|
|
60
|
+
break;
|
|
61
|
+
case "known":
|
|
62
|
+
await this.handleKnown(msg);
|
|
63
|
+
break;
|
|
64
|
+
case "done":
|
|
65
|
+
await this.handleDone(msg);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async sendNewContentAfter(theirKnown, asDependencyOf) {
|
|
70
|
+
const coValueRow = (await this.db
|
|
71
|
+
.prepare(`SELECT * FROM coValues WHERE id = ?`)
|
|
72
|
+
.get(theirKnown.id));
|
|
73
|
+
const allOurSessions = coValueRow
|
|
74
|
+
? this.db
|
|
75
|
+
.prepare(`SELECT * FROM sessions WHERE coValue = ?`)
|
|
76
|
+
.all(coValueRow.rowID)
|
|
77
|
+
: [];
|
|
78
|
+
const ourKnown = {
|
|
79
|
+
id: theirKnown.id,
|
|
80
|
+
header: !!coValueRow,
|
|
81
|
+
sessions: {},
|
|
82
|
+
};
|
|
83
|
+
const parsedHeader = (coValueRow?.header &&
|
|
84
|
+
JSON.parse(coValueRow.header));
|
|
85
|
+
const newContent = {
|
|
86
|
+
action: "content",
|
|
87
|
+
id: theirKnown.id,
|
|
88
|
+
header: theirKnown.header ? undefined : parsedHeader,
|
|
89
|
+
new: {},
|
|
90
|
+
};
|
|
91
|
+
for (const sessionRow of allOurSessions) {
|
|
92
|
+
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
|
93
|
+
if (sessionRow.lastIdx >
|
|
94
|
+
(theirKnown.sessions[sessionRow.sessionID] || 0)) {
|
|
95
|
+
const firstNewTxIdx = theirKnown.sessions[sessionRow.sessionID] || 0;
|
|
96
|
+
const newTxInSession = this.db
|
|
97
|
+
.prepare(`SELECT * FROM transactions WHERE ses = ? AND idx > ?`)
|
|
98
|
+
.all(sessionRow.rowID, firstNewTxIdx);
|
|
99
|
+
newContent.new[sessionRow.sessionID] = {
|
|
100
|
+
after: firstNewTxIdx,
|
|
101
|
+
lastSignature: sessionRow.lastSignature,
|
|
102
|
+
newTransactions: newTxInSession.map((row) => JSON.parse(row.tx)),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const dependedOnCoValues = parsedHeader?.ruleset.type === "group"
|
|
107
|
+
? Object.values(newContent.new).flatMap((sessionEntry) => sessionEntry.newTransactions.flatMap((tx) => {
|
|
108
|
+
if (tx.privacy !== "trusting")
|
|
109
|
+
return [];
|
|
110
|
+
return tx.changes
|
|
111
|
+
.map((change) => change &&
|
|
112
|
+
typeof change === "object" &&
|
|
113
|
+
"op" in change &&
|
|
114
|
+
change.op === "set" &&
|
|
115
|
+
"key" in change &&
|
|
116
|
+
change.key)
|
|
117
|
+
.filter((key) => typeof key === "string" &&
|
|
118
|
+
key.startsWith("co_"));
|
|
119
|
+
}))
|
|
120
|
+
: parsedHeader?.ruleset.type === "ownedByGroup"
|
|
121
|
+
? [parsedHeader?.ruleset.group]
|
|
122
|
+
: [];
|
|
123
|
+
for (const dependedOnCoValue of dependedOnCoValues) {
|
|
124
|
+
await this.sendNewContentAfter({ id: dependedOnCoValue, header: false, sessions: {} }, asDependencyOf || theirKnown.id);
|
|
125
|
+
}
|
|
126
|
+
await this.toLocalNode.write({
|
|
127
|
+
action: "known",
|
|
128
|
+
...ourKnown,
|
|
129
|
+
asDependencyOf,
|
|
130
|
+
});
|
|
131
|
+
if (newContent.header || Object.keys(newContent.new).length > 0) {
|
|
132
|
+
await this.toLocalNode.write(newContent);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
handleLoad(msg) {
|
|
136
|
+
return this.sendNewContentAfter(msg);
|
|
137
|
+
}
|
|
138
|
+
async handleContent(msg) {
|
|
139
|
+
let storedCoValueRowID = this.db
|
|
140
|
+
.prepare(`SELECT rowID FROM coValues WHERE id = ?`)
|
|
141
|
+
.get(msg.id)?.rowID;
|
|
142
|
+
if (storedCoValueRowID === undefined) {
|
|
143
|
+
const header = msg.header;
|
|
144
|
+
if (!header) {
|
|
145
|
+
console.error("Expected to be sent header first");
|
|
146
|
+
await this.toLocalNode.write({
|
|
147
|
+
action: "known",
|
|
148
|
+
id: msg.id,
|
|
149
|
+
header: false,
|
|
150
|
+
sessions: {},
|
|
151
|
+
isCorrection: true,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
storedCoValueRowID = this.db
|
|
156
|
+
.prepare(`INSERT INTO coValues (id, header) VALUES (?, ?)`)
|
|
157
|
+
.run(msg.id, JSON.stringify(header)).lastInsertRowid;
|
|
158
|
+
}
|
|
159
|
+
const ourKnown = {
|
|
160
|
+
id: msg.id,
|
|
161
|
+
header: true,
|
|
162
|
+
sessions: {},
|
|
163
|
+
};
|
|
164
|
+
let invalidAssumptions = false;
|
|
165
|
+
this.db.transaction(() => {
|
|
166
|
+
const allOurSessions = this.db
|
|
167
|
+
.prepare(`SELECT * FROM sessions WHERE coValue = ?`)
|
|
168
|
+
.all(storedCoValueRowID).reduce((acc, row) => {
|
|
169
|
+
acc[row.sessionID] = row;
|
|
170
|
+
return acc;
|
|
171
|
+
}, {});
|
|
172
|
+
for (const sessionID of Object.keys(msg.new)) {
|
|
173
|
+
const sessionRow = allOurSessions[sessionID];
|
|
174
|
+
if (sessionRow) {
|
|
175
|
+
ourKnown.sessions[sessionRow.sessionID] =
|
|
176
|
+
sessionRow.lastIdx;
|
|
177
|
+
}
|
|
178
|
+
if ((sessionRow?.lastIdx || 0) <
|
|
179
|
+
(msg.new[sessionID]?.after || 0)) {
|
|
180
|
+
invalidAssumptions = true;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const newTransactions = msg.new[sessionID]?.newTransactions || [];
|
|
184
|
+
const actuallyNewOffset = (sessionRow?.lastIdx || 0) -
|
|
185
|
+
(msg.new[sessionID]?.after || 0);
|
|
186
|
+
const actuallyNewTransactions = newTransactions.slice(actuallyNewOffset);
|
|
187
|
+
let nextIdx = sessionRow?.lastIdx || 0;
|
|
188
|
+
const sessionUpdate = {
|
|
189
|
+
coValue: storedCoValueRowID,
|
|
190
|
+
sessionID: sessionID,
|
|
191
|
+
lastIdx: (sessionRow?.lastIdx || 0) +
|
|
192
|
+
actuallyNewTransactions.length,
|
|
193
|
+
lastSignature: msg.new[sessionID].lastSignature,
|
|
194
|
+
};
|
|
195
|
+
const sessionRowID = this.db
|
|
196
|
+
.prepare(`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature) VALUES (?, ?, ?, ?)
|
|
197
|
+
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature`)
|
|
198
|
+
.run(sessionUpdate.coValue, sessionUpdate.sessionID, sessionUpdate.lastIdx, sessionUpdate.lastSignature).lastInsertRowid;
|
|
199
|
+
for (const newTransaction of actuallyNewTransactions) {
|
|
200
|
+
nextIdx++;
|
|
201
|
+
this.db
|
|
202
|
+
.prepare(`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`)
|
|
203
|
+
.run(sessionRowID, nextIdx, JSON.stringify(newTransaction));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
if (invalidAssumptions) {
|
|
209
|
+
await this.toLocalNode.write({
|
|
210
|
+
action: "known",
|
|
211
|
+
...ourKnown,
|
|
212
|
+
isCorrection: invalidAssumptions,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
handleKnown(msg) {
|
|
217
|
+
return this.sendNewContentAfter(msg);
|
|
218
|
+
}
|
|
219
|
+
handleDone(_msg) { }
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,eAAe;AAKf,uBAAuB;AACvB,aAAa;EAChB,MAAM,QAAQ,CAAC;AAQhB,OAAO,QAAmC,MAAM,gBAAgB,CAAC;AAyBjE,MAAM,OAAO,aAAa;IAKtB,YACI,EAAa,EACb,aAA0C,EAC1C,WAAwC;QAExC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;QAC/C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC,SAAS,EAAE,CAAC;QAE3C,CAAC,KAAK,IAAI,EAAE;YACR,IAAI,IAAI,GAAG,KAAK,CAAC;YACjB,OAAO,CAAC,IAAI,EAAE;gBACV,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;gBAC/C,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;gBAEnB,IAAI,MAAM,CAAC,KAAK,EAAE;oBACd,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;iBACxC;aACJ;QACL,CAAC,CAAC,EAAE,CAAC;IACT,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAChB,QAAQ,EACR,KAAK,EACL,aAAa,GAAG,OAAO,GAK1B;QACG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,eAAe,CAAC,cAAc,CACnE,aAAa,EACb,SAAS,EACT,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CACtD,CAAC;QAEF,MAAM,aAAa,CAAC,IAAI,CACpB,QAAQ,EACR,eAAe,CAAC,QAAQ,EACxB,eAAe,CAAC,QAAQ,CAC3B,CAAC;QAEF,OAAO,aAAa,CAAC;IACzB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CACb,QAAgB,EAChB,aAA0C,EAC1C,WAAwC;QAExC,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC9B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QAEhC,EAAE,CAAC,OAAO,CACN;;;;;6BAKiB,CACpB,CAAC,GAAG,EAAE,CAAC;QAER,EAAE,CAAC,OAAO,CACN;;;;;;;eAOG,CACN,CAAC,GAAG,EAAE,CAAC;QAER,EAAE,CAAC,OAAO,CACN,qEAAqE,CACxE,CAAC,GAAG,EAAE,CAAC;QAER,EAAE,CAAC,OAAO,CACN;;;;eAIG,CACN,CAAC,GAAG,EAAE,CAAC;QAER,EAAE,CAAC,OAAO,CACN,2DAA2D,CAC9D,CAAC,GAAG,EAAE,CAAC;QAER,OAAO,IAAI,aAAa,CAAC,EAAE,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,GAAgB;QACpC,QAAQ,GAAG,CAAC,MAAM,EAAE;YAChB,KAAK,MAAM;gBACP,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;gBAC3B,MAAM;YACV,KAAK,SAAS;gBACV,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,OAAO;gBACR,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;gBAC5B,MAAM;YACV,KAAK,MAAM;gBACP,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;gBAC3B,MAAM;SACb;IACL,CAAC;IAED,KAAK,CAAC,mBAAmB,CACrB,UAAiD,EACjD,cAA4C;QAE5C,MAAM,UAAU,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE;aAC5B,OAAO,CAAC,qCAAqC,CAAC;aAC9C,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAiC,CAAC;QAEzD,MAAM,cAAc,GAAG,UAAU;YAC7B,CAAC,CAAE,IAAI,CAAC,EAAE;iBACH,OAAO,CAAS,0CAA0C,CAAC;iBAC3D,GAAG,CAAC,UAAU,CAAC,KAAK,CAAwB;YACnD,CAAC,CAAC,EAAE,CAAC;QAET,MAAM,QAAQ,GAA0C;YACpD,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,MAAM,EAAE,CAAC,CAAC,UAAU;YACpB,QAAQ,EAAE,EAAE;SACf,CAAC;QAEF,MAAM,YAAY,GAAG,CAAC,UAAU,EAAE,MAAM;YACpC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAElB,CAAC;QAEhB,MAAM,UAAU,GAA0C;YACtD,MAAM,EAAE,SAAS;YACjB,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;YACpD,GAAG,EAAE,EAAE;SACV,CAAC;QAEF,KAAK,MAAM,UAAU,IAAI,cAAc,EAAE;YACrC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC;YAE7D,IACI,UAAU,CAAC,OAAO;gBAClB,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAClD;gBACE,MAAM,aAAa,GACf,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;gBAEnD,MAAM,cAAc,GAAG,IAAI,CAAC,EAAE;qBACzB,OAAO,CACJ,sDAAsD,CACzD;qBACA,GAAG,CAAC,UAAU,CAAC,KAAK,EAAE,aAAa,CAAqB,CAAC;gBAE9D,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG;oBACnC,KAAK,EAAE,aAAa;oBACpB,aAAa,EAAE,UAAU,CAAC,aAAa;oBACvC,eAAe,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACxC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CACrB;iBACJ,CAAC;aACL;SACJ;QAED,MAAM,kBAAkB,GACpB,YAAY,EAAE,OAAO,CAAC,IAAI,KAAK,OAAO;YAClC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE,CACnD,YAAY,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;gBACxC,IAAI,EAAE,CAAC,OAAO,KAAK,UAAU;oBAAE,OAAO,EAAE,CAAC;gBACzC,OAAO,EAAE,CAAC,OAAO;qBACZ,GAAG,CACA,CAAC,MAAM,EAAE,EAAE,CACP,MAAM;oBACN,OAAO,MAAM,KAAK,QAAQ;oBAC1B,IAAI,IAAI,MAAM;oBACd,MAAM,CAAC,EAAE,KAAK,KAAK;oBACnB,KAAK,IAAI,MAAM;oBACf,MAAM,CAAC,GAAG,CACjB;qBACA,MAAM,CACH,CAAC,GAAG,EAAsC,EAAE,CACxC,OAAO,GAAG,KAAK,QAAQ;oBACvB,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAC5B,CAAC;YACV,CAAC,CAAC,CACL;YACH,CAAC,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,KAAK,cAAc;gBAC/C,CAAC,CAAC,CAAC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC;gBAC/B,CAAC,CAAC,EAAE,CAAC;QAEb,KAAK,MAAM,iBAAiB,IAAI,kBAAkB,EAAE;YAChD,MAAM,IAAI,CAAC,mBAAmB,CAC1B,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,EACtD,cAAc,IAAI,UAAU,CAAC,EAAE,CAClC,CAAC;SACL;QAED,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;YACzB,MAAM,EAAE,OAAO;YACf,GAAG,QAAQ;YACX,cAAc;SACjB,CAAC,CAAC;QAEH,IAAI,UAAU,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;YAC7D,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;SAC5C;IACL,CAAC;IAED,UAAU,CAAC,GAAoC;QAC3C,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,GAA0C;QAC1D,IAAI,kBAAkB,GAClB,IAAI,CAAC,EAAE;aACF,OAAO,CAAU,yCAAyC,CAAC;aAC3D,GAAG,CAAC,GAAG,CAAC,EAAE,CAClB,EAAE,KAAK,CAAC;QAET,IAAI,kBAAkB,KAAK,SAAS,EAAE;YAClC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;YAC1B,IAAI,CAAC,MAAM,EAAE;gBACT,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;gBAClD,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;oBACzB,MAAM,EAAE,OAAO;oBACf,EAAE,EAAE,GAAG,CAAC,EAAE;oBACV,MAAM,EAAE,KAAK;oBACb,QAAQ,EAAE,EAAE;oBACZ,YAAY,EAAE,IAAI;iBACrB,CAAC,CAAC;gBACH,OAAO;aACV;YAED,kBAAkB,GAAG,IAAI,CAAC,EAAE;iBACvB,OAAO,CACJ,iDAAiD,CACpD;iBACA,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,eAAyB,CAAC;SACtE;QAED,MAAM,QAAQ,GAA0C;YACpD,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,EAAE;SACf,CAAC;QACF,IAAI,kBAAkB,GAAG,KAAK,CAAC;QAE/B,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YACrB,MAAM,cAAc,GAChB,IAAI,CAAC,EAAE;iBACF,OAAO,CAAS,0CAA0C,CAAC;iBAC3D,GAAG,CAAC,kBAAmB,CAC/B,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;gBAClB,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC;gBACzB,OAAO,GAAG,CAAC;YACf,CAAC,EAAE,EAA+C,CAAC,CAAC;YAEpD,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAgB,EAAE;gBACzD,MAAM,UAAU,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;gBAC7C,IAAI,UAAU,EAAE;oBACZ,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC;wBACnC,UAAU,CAAC,OAAO,CAAC;iBAC1B;gBAED,IACI,CAAC,UAAU,EAAE,OAAO,IAAI,CAAC,CAAC;oBAC1B,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,EAClC;oBACE,kBAAkB,GAAG,IAAI,CAAC;iBAC7B;qBAAM;oBACH,MAAM,eAAe,GACjB,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,eAAe,IAAI,EAAE,CAAC;oBAE9C,MAAM,iBAAiB,GACnB,CAAC,UAAU,EAAE,OAAO,IAAI,CAAC,CAAC;wBAC1B,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC;oBACrC,MAAM,uBAAuB,GACzB,eAAe,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;oBAE7C,IAAI,OAAO,GAAG,UAAU,EAAE,OAAO,IAAI,CAAC,CAAC;oBAEvC,MAAM,aAAa,GAAG;wBAClB,OAAO,EAAE,kBAAmB;wBAC5B,SAAS,EAAE,SAAS;wBACpB,OAAO,EACH,CAAC,UAAU,EAAE,OAAO,IAAI,CAAC,CAAC;4BAC1B,uBAAuB,CAAC,MAAM;wBAClC,aAAa,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC,aAAa;qBACnD,CAAC;oBAEF,MAAM,YAAY,GAAG,IAAI,CAAC,EAAE;yBACvB,OAAO,CACJ;yIAC6G,CAChH;yBACA,GAAG,CACA,aAAa,CAAC,OAAO,EACrB,aAAa,CAAC,SAAS,EACvB,aAAa,CAAC,OAAO,EACrB,aAAa,CAAC,aAAa,CAC9B,CAAC,eAAyB,CAAC;oBAEhC,KAAK,MAAM,cAAc,IAAI,uBAAuB,EAAE;wBAClD,OAAO,EAAE,CAAC;wBACV,IAAI,CAAC,EAAE;6BACF,OAAO,CACJ,0DAA0D,CAC7D;6BACA,GAAG,CACA,YAAY,EACZ,OAAO,EACP,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,CACjC,CAAC;qBACT;iBACJ;aACJ;QACL,CAAC,CAAC,EAAE,CAAC;QAEL,IAAI,kBAAkB,EAAE;YACpB,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;gBACzB,MAAM,EAAE,OAAO;gBACf,GAAG,QAAQ;gBACX,YAAY,EAAE,kBAAkB;aACnC,CAAC,CAAC;SACN;IACL,CAAC;IAED,WAAW,CAAC,GAA0C;QAClD,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IAED,UAAU,CAAC,IAAqC,IAAG,CAAC;CACvD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cojson-storage-sqlite",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"better-sqlite3": "^8.5.2",
|
|
10
|
+
"cojson": "^0.1.4",
|
|
11
|
+
"typescript": "^5.1.6"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"lint": "eslint src/**/*.ts",
|
|
15
|
+
"build": "npm run lint && rm -rf ./dist && tsc --declaration --sourceMap --outDir dist",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/better-sqlite3": "^7.6.4"
|
|
20
|
+
},
|
|
21
|
+
"gitHead": "65a7a66c159ae23cfebfa9aa5c3e173194810573"
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cojsonInternals,
|
|
3
|
+
SyncMessage,
|
|
4
|
+
Peer,
|
|
5
|
+
CojsonInternalTypes,
|
|
6
|
+
SessionID,
|
|
7
|
+
// CojsonInternalTypes,
|
|
8
|
+
// SessionID,
|
|
9
|
+
} from "cojson";
|
|
10
|
+
import {
|
|
11
|
+
ReadableStream,
|
|
12
|
+
WritableStream,
|
|
13
|
+
ReadableStreamDefaultReader,
|
|
14
|
+
WritableStreamDefaultWriter,
|
|
15
|
+
} from "isomorphic-streams";
|
|
16
|
+
|
|
17
|
+
import Database, { Database as DatabaseT } from "better-sqlite3";
|
|
18
|
+
import { RawCoID } from "cojson/dist/ids";
|
|
19
|
+
|
|
20
|
+
type CoValueRow = {
|
|
21
|
+
id: CojsonInternalTypes.RawCoID;
|
|
22
|
+
header: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type StoredCoValueRow = CoValueRow & { rowID: number };
|
|
26
|
+
|
|
27
|
+
type SessionRow = {
|
|
28
|
+
coValue: number;
|
|
29
|
+
sessionID: SessionID;
|
|
30
|
+
lastIdx: number;
|
|
31
|
+
lastSignature: CojsonInternalTypes.Signature;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type StoredSessionRow = SessionRow & { rowID: number };
|
|
35
|
+
|
|
36
|
+
type TransactionRow = {
|
|
37
|
+
ses: number;
|
|
38
|
+
idx: number;
|
|
39
|
+
tx: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class SQLiteStorage {
|
|
43
|
+
fromLocalNode!: ReadableStreamDefaultReader<SyncMessage>;
|
|
44
|
+
toLocalNode: WritableStreamDefaultWriter<SyncMessage>;
|
|
45
|
+
db: DatabaseT;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
db: DatabaseT,
|
|
49
|
+
fromLocalNode: ReadableStream<SyncMessage>,
|
|
50
|
+
toLocalNode: WritableStream<SyncMessage>
|
|
51
|
+
) {
|
|
52
|
+
this.db = db;
|
|
53
|
+
this.fromLocalNode = fromLocalNode.getReader();
|
|
54
|
+
this.toLocalNode = toLocalNode.getWriter();
|
|
55
|
+
|
|
56
|
+
(async () => {
|
|
57
|
+
let done = false;
|
|
58
|
+
while (!done) {
|
|
59
|
+
const result = await this.fromLocalNode.read();
|
|
60
|
+
done = result.done;
|
|
61
|
+
|
|
62
|
+
if (result.value) {
|
|
63
|
+
this.handleSyncMessage(result.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async asPeer({
|
|
70
|
+
filename,
|
|
71
|
+
trace,
|
|
72
|
+
localNodeName = "local",
|
|
73
|
+
}: {
|
|
74
|
+
filename: string;
|
|
75
|
+
trace?: boolean;
|
|
76
|
+
localNodeName?: string;
|
|
77
|
+
}): Promise<Peer> {
|
|
78
|
+
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
|
79
|
+
localNodeName,
|
|
80
|
+
"storage",
|
|
81
|
+
{ peer1role: "client", peer2role: "server", trace }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await SQLiteStorage.open(
|
|
85
|
+
filename,
|
|
86
|
+
localNodeAsPeer.incoming,
|
|
87
|
+
localNodeAsPeer.outgoing
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return storageAsPeer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static async open(
|
|
94
|
+
filename: string,
|
|
95
|
+
fromLocalNode: ReadableStream<SyncMessage>,
|
|
96
|
+
toLocalNode: WritableStream<SyncMessage>
|
|
97
|
+
) {
|
|
98
|
+
const db = Database(filename);
|
|
99
|
+
db.pragma("journal_mode = WAL");
|
|
100
|
+
|
|
101
|
+
db.prepare(
|
|
102
|
+
`CREATE TABLE IF NOT EXISTS transactions (
|
|
103
|
+
ses INTEGER,
|
|
104
|
+
idx INTEGER,
|
|
105
|
+
tx TEXT NOT NULL ,
|
|
106
|
+
PRIMARY KEY (ses, idx)
|
|
107
|
+
) WITHOUT ROWID;`
|
|
108
|
+
).run();
|
|
109
|
+
|
|
110
|
+
db.prepare(
|
|
111
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
112
|
+
rowID INTEGER PRIMARY KEY,
|
|
113
|
+
coValue INTEGER NOT NULL,
|
|
114
|
+
sessionID TEXT NOT NULL,
|
|
115
|
+
lastIdx INTEGER,
|
|
116
|
+
lastSignature TEXT,
|
|
117
|
+
UNIQUE (sessionID, coValue)
|
|
118
|
+
);`
|
|
119
|
+
).run();
|
|
120
|
+
|
|
121
|
+
db.prepare(
|
|
122
|
+
`CREATE INDEX IF NOT EXISTS sessionsByCoValue ON sessions (coValue);`
|
|
123
|
+
).run();
|
|
124
|
+
|
|
125
|
+
db.prepare(
|
|
126
|
+
`CREATE TABLE IF NOT EXISTS coValues (
|
|
127
|
+
rowID INTEGER PRIMARY KEY,
|
|
128
|
+
id TEXT NOT NULL UNIQUE,
|
|
129
|
+
header TEXT NOT NULL UNIQUE
|
|
130
|
+
);`
|
|
131
|
+
).run();
|
|
132
|
+
|
|
133
|
+
db.prepare(
|
|
134
|
+
`CREATE INDEX IF NOT EXISTS coValuesByID ON coValues (id);`
|
|
135
|
+
).run();
|
|
136
|
+
|
|
137
|
+
return new SQLiteStorage(db, fromLocalNode, toLocalNode);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async handleSyncMessage(msg: SyncMessage) {
|
|
141
|
+
switch (msg.action) {
|
|
142
|
+
case "load":
|
|
143
|
+
await this.handleLoad(msg);
|
|
144
|
+
break;
|
|
145
|
+
case "content":
|
|
146
|
+
await this.handleContent(msg);
|
|
147
|
+
break;
|
|
148
|
+
case "known":
|
|
149
|
+
await this.handleKnown(msg);
|
|
150
|
+
break;
|
|
151
|
+
case "done":
|
|
152
|
+
await this.handleDone(msg);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async sendNewContentAfter(
|
|
158
|
+
theirKnown: CojsonInternalTypes.CoValueKnownState,
|
|
159
|
+
asDependencyOf?: CojsonInternalTypes.RawCoID
|
|
160
|
+
) {
|
|
161
|
+
const coValueRow = (await this.db
|
|
162
|
+
.prepare(`SELECT * FROM coValues WHERE id = ?`)
|
|
163
|
+
.get(theirKnown.id)) as StoredCoValueRow | undefined;
|
|
164
|
+
|
|
165
|
+
const allOurSessions = coValueRow
|
|
166
|
+
? (this.db
|
|
167
|
+
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
|
|
168
|
+
.all(coValueRow.rowID) as StoredSessionRow[])
|
|
169
|
+
: [];
|
|
170
|
+
|
|
171
|
+
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
|
|
172
|
+
id: theirKnown.id,
|
|
173
|
+
header: !!coValueRow,
|
|
174
|
+
sessions: {},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const parsedHeader = (coValueRow?.header &&
|
|
178
|
+
JSON.parse(coValueRow.header)) as
|
|
179
|
+
| CojsonInternalTypes.CoValueHeader
|
|
180
|
+
| undefined;
|
|
181
|
+
|
|
182
|
+
const newContent: CojsonInternalTypes.NewContentMessage = {
|
|
183
|
+
action: "content",
|
|
184
|
+
id: theirKnown.id,
|
|
185
|
+
header: theirKnown.header ? undefined : parsedHeader,
|
|
186
|
+
new: {},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const sessionRow of allOurSessions) {
|
|
190
|
+
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
sessionRow.lastIdx >
|
|
194
|
+
(theirKnown.sessions[sessionRow.sessionID] || 0)
|
|
195
|
+
) {
|
|
196
|
+
const firstNewTxIdx =
|
|
197
|
+
theirKnown.sessions[sessionRow.sessionID] || 0;
|
|
198
|
+
|
|
199
|
+
const newTxInSession = this.db
|
|
200
|
+
.prepare<[number, number]>(
|
|
201
|
+
`SELECT * FROM transactions WHERE ses = ? AND idx > ?`
|
|
202
|
+
)
|
|
203
|
+
.all(sessionRow.rowID, firstNewTxIdx) as TransactionRow[];
|
|
204
|
+
|
|
205
|
+
newContent.new[sessionRow.sessionID] = {
|
|
206
|
+
after: firstNewTxIdx,
|
|
207
|
+
lastSignature: sessionRow.lastSignature,
|
|
208
|
+
newTransactions: newTxInSession.map((row) =>
|
|
209
|
+
JSON.parse(row.tx)
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const dependedOnCoValues =
|
|
216
|
+
parsedHeader?.ruleset.type === "group"
|
|
217
|
+
? Object.values(newContent.new).flatMap((sessionEntry) =>
|
|
218
|
+
sessionEntry.newTransactions.flatMap((tx) => {
|
|
219
|
+
if (tx.privacy !== "trusting") return [];
|
|
220
|
+
return tx.changes
|
|
221
|
+
.map(
|
|
222
|
+
(change) =>
|
|
223
|
+
change &&
|
|
224
|
+
typeof change === "object" &&
|
|
225
|
+
"op" in change &&
|
|
226
|
+
change.op === "set" &&
|
|
227
|
+
"key" in change &&
|
|
228
|
+
change.key
|
|
229
|
+
)
|
|
230
|
+
.filter(
|
|
231
|
+
(key): key is CojsonInternalTypes.RawCoID =>
|
|
232
|
+
typeof key === "string" &&
|
|
233
|
+
key.startsWith("co_")
|
|
234
|
+
);
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
: parsedHeader?.ruleset.type === "ownedByGroup"
|
|
238
|
+
? [parsedHeader?.ruleset.group]
|
|
239
|
+
: [];
|
|
240
|
+
|
|
241
|
+
for (const dependedOnCoValue of dependedOnCoValues) {
|
|
242
|
+
await this.sendNewContentAfter(
|
|
243
|
+
{ id: dependedOnCoValue, header: false, sessions: {} },
|
|
244
|
+
asDependencyOf || theirKnown.id
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await this.toLocalNode.write({
|
|
249
|
+
action: "known",
|
|
250
|
+
...ourKnown,
|
|
251
|
+
asDependencyOf,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (newContent.header || Object.keys(newContent.new).length > 0) {
|
|
255
|
+
await this.toLocalNode.write(newContent);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
handleLoad(msg: CojsonInternalTypes.LoadMessage) {
|
|
260
|
+
return this.sendNewContentAfter(msg);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async handleContent(msg: CojsonInternalTypes.NewContentMessage) {
|
|
264
|
+
let storedCoValueRowID = (
|
|
265
|
+
this.db
|
|
266
|
+
.prepare<RawCoID>(`SELECT rowID FROM coValues WHERE id = ?`)
|
|
267
|
+
.get(msg.id) as StoredCoValueRow | undefined
|
|
268
|
+
)?.rowID;
|
|
269
|
+
|
|
270
|
+
if (storedCoValueRowID === undefined) {
|
|
271
|
+
const header = msg.header;
|
|
272
|
+
if (!header) {
|
|
273
|
+
console.error("Expected to be sent header first");
|
|
274
|
+
await this.toLocalNode.write({
|
|
275
|
+
action: "known",
|
|
276
|
+
id: msg.id,
|
|
277
|
+
header: false,
|
|
278
|
+
sessions: {},
|
|
279
|
+
isCorrection: true,
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
storedCoValueRowID = this.db
|
|
285
|
+
.prepare<[RawCoID, string]>(
|
|
286
|
+
`INSERT INTO coValues (id, header) VALUES (?, ?)`
|
|
287
|
+
)
|
|
288
|
+
.run(msg.id, JSON.stringify(header)).lastInsertRowid as number;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
|
|
292
|
+
id: msg.id,
|
|
293
|
+
header: true,
|
|
294
|
+
sessions: {},
|
|
295
|
+
};
|
|
296
|
+
let invalidAssumptions = false;
|
|
297
|
+
|
|
298
|
+
this.db.transaction(() => {
|
|
299
|
+
const allOurSessions = (
|
|
300
|
+
this.db
|
|
301
|
+
.prepare<number>(`SELECT * FROM sessions WHERE coValue = ?`)
|
|
302
|
+
.all(storedCoValueRowID!) as StoredSessionRow[]
|
|
303
|
+
).reduce((acc, row) => {
|
|
304
|
+
acc[row.sessionID] = row;
|
|
305
|
+
return acc;
|
|
306
|
+
}, {} as { [sessionID: string]: StoredSessionRow });
|
|
307
|
+
|
|
308
|
+
for (const sessionID of Object.keys(msg.new) as SessionID[]) {
|
|
309
|
+
const sessionRow = allOurSessions[sessionID];
|
|
310
|
+
if (sessionRow) {
|
|
311
|
+
ourKnown.sessions[sessionRow.sessionID] =
|
|
312
|
+
sessionRow.lastIdx;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
(sessionRow?.lastIdx || 0) <
|
|
317
|
+
(msg.new[sessionID]?.after || 0)
|
|
318
|
+
) {
|
|
319
|
+
invalidAssumptions = true;
|
|
320
|
+
} else {
|
|
321
|
+
const newTransactions =
|
|
322
|
+
msg.new[sessionID]?.newTransactions || [];
|
|
323
|
+
|
|
324
|
+
const actuallyNewOffset =
|
|
325
|
+
(sessionRow?.lastIdx || 0) -
|
|
326
|
+
(msg.new[sessionID]?.after || 0);
|
|
327
|
+
const actuallyNewTransactions =
|
|
328
|
+
newTransactions.slice(actuallyNewOffset);
|
|
329
|
+
|
|
330
|
+
let nextIdx = sessionRow?.lastIdx || 0;
|
|
331
|
+
|
|
332
|
+
const sessionUpdate = {
|
|
333
|
+
coValue: storedCoValueRowID!,
|
|
334
|
+
sessionID: sessionID,
|
|
335
|
+
lastIdx:
|
|
336
|
+
(sessionRow?.lastIdx || 0) +
|
|
337
|
+
actuallyNewTransactions.length,
|
|
338
|
+
lastSignature: msg.new[sessionID]!.lastSignature,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const sessionRowID = this.db
|
|
342
|
+
.prepare<[number, string, number, string]>(
|
|
343
|
+
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature) VALUES (?, ?, ?, ?)
|
|
344
|
+
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature`
|
|
345
|
+
)
|
|
346
|
+
.run(
|
|
347
|
+
sessionUpdate.coValue,
|
|
348
|
+
sessionUpdate.sessionID,
|
|
349
|
+
sessionUpdate.lastIdx,
|
|
350
|
+
sessionUpdate.lastSignature
|
|
351
|
+
).lastInsertRowid as number;
|
|
352
|
+
|
|
353
|
+
for (const newTransaction of actuallyNewTransactions) {
|
|
354
|
+
nextIdx++;
|
|
355
|
+
this.db
|
|
356
|
+
.prepare<[number, number, string]>(
|
|
357
|
+
`INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)`
|
|
358
|
+
)
|
|
359
|
+
.run(
|
|
360
|
+
sessionRowID,
|
|
361
|
+
nextIdx,
|
|
362
|
+
JSON.stringify(newTransaction)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})();
|
|
368
|
+
|
|
369
|
+
if (invalidAssumptions) {
|
|
370
|
+
await this.toLocalNode.write({
|
|
371
|
+
action: "known",
|
|
372
|
+
...ourKnown,
|
|
373
|
+
isCorrection: invalidAssumptions,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
handleKnown(msg: CojsonInternalTypes.KnownStateMessage) {
|
|
379
|
+
return this.sendNewContentAfter(msg);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
handleDone(_msg: CojsonInternalTypes.DoneMessage) {}
|
|
383
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"target": "ES2020",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
},
|
|
14
|
+
"include": ["./src/**/*"],
|
|
15
|
+
}
|