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 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
+ };
@@ -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":"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"}
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
+ }