cojson-storage-indexeddb 0.6.0 → 0.6.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/CHANGELOG.md +8 -0
- package/dist/index.js +328 -229
- package/dist/index.js.map +1 -1
- package/dist/syncPromises.js +161 -0
- package/dist/syncPromises.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +558 -374
- package/src/syncPromises.ts +228 -0
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ReadableStreamDefaultReader,
|
|
14
14
|
WritableStreamDefaultWriter,
|
|
15
15
|
} from "isomorphic-streams";
|
|
16
|
+
import { SyncPromise } from "./syncPromises";
|
|
16
17
|
|
|
17
18
|
type CoValueRow = {
|
|
18
19
|
id: CojsonInternalTypes.RawCoID;
|
|
@@ -64,12 +65,17 @@ export class IDBStorage {
|
|
|
64
65
|
done = result.done;
|
|
65
66
|
|
|
66
67
|
if (result.value) {
|
|
67
|
-
await this.handleSyncMessage(result.value);
|
|
68
68
|
// console.log(
|
|
69
69
|
// "IDB: handling msg",
|
|
70
70
|
// result.value.id,
|
|
71
71
|
// result.value.action
|
|
72
72
|
// );
|
|
73
|
+
await this.handleSyncMessage(result.value);
|
|
74
|
+
// console.log(
|
|
75
|
+
// "IDB: handled msg",
|
|
76
|
+
// result.value.id,
|
|
77
|
+
// result.value.action
|
|
78
|
+
// );
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
})();
|
|
@@ -144,23 +150,23 @@ export class IDBStorage {
|
|
|
144
150
|
keyPath: ["ses", "idx"],
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
|
-
if (ev.oldVersion !== 0 && ev.oldVersion <= 3) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
153
|
+
// if (ev.oldVersion !== 0 && ev.oldVersion <= 3) {
|
|
154
|
+
// // fix embarrassing off-by-one error for transaction indices
|
|
155
|
+
// console.log("Migration: fixing off-by-one error");
|
|
156
|
+
// const transaction = (
|
|
157
|
+
// ev.target as unknown as { transaction: IDBTransaction }
|
|
158
|
+
// ).transaction;
|
|
159
|
+
|
|
160
|
+
// const txsStore = transaction.objectStore("transactions");
|
|
161
|
+
// const txs = await promised(txsStore.getAll());
|
|
162
|
+
|
|
163
|
+
// for (const tx of txs) {
|
|
164
|
+
// await promised(txsStore.delete([tx.ses, tx.idx]));
|
|
165
|
+
// tx.idx -= 1;
|
|
166
|
+
// await promised(txsStore.add(tx));
|
|
167
|
+
// }
|
|
168
|
+
// console.log("Migration: fixing off-by-one error - done");
|
|
169
|
+
// }
|
|
164
170
|
};
|
|
165
171
|
});
|
|
166
172
|
|
|
@@ -170,418 +176,596 @@ export class IDBStorage {
|
|
|
170
176
|
async handleSyncMessage(msg: SyncMessage) {
|
|
171
177
|
switch (msg.action) {
|
|
172
178
|
case "load":
|
|
173
|
-
await this.handleLoad(msg);
|
|
179
|
+
/*await*/ this.handleLoad(msg);
|
|
174
180
|
break;
|
|
175
181
|
case "content":
|
|
176
182
|
await this.handleContent(msg);
|
|
177
183
|
break;
|
|
178
184
|
case "known":
|
|
179
|
-
await this.handleKnown(msg);
|
|
185
|
+
/*await*/ this.handleKnown(msg);
|
|
180
186
|
break;
|
|
181
187
|
case "done":
|
|
182
|
-
await this.handleDone(msg);
|
|
188
|
+
/*await*/ this.handleDone(msg);
|
|
183
189
|
break;
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
currentTx:
|
|
194
|
+
| {
|
|
195
|
+
id: number;
|
|
196
|
+
tx: IDBTransaction;
|
|
197
|
+
stores: {
|
|
198
|
+
coValues: IDBObjectStore;
|
|
199
|
+
sessions: IDBObjectStore;
|
|
200
|
+
transactions: IDBObjectStore;
|
|
201
|
+
signatureAfter: IDBObjectStore;
|
|
202
|
+
};
|
|
203
|
+
startedAt: number;
|
|
204
|
+
pendingRequests: ((txEntry: {
|
|
205
|
+
stores: {
|
|
206
|
+
coValues: IDBObjectStore;
|
|
207
|
+
sessions: IDBObjectStore;
|
|
208
|
+
transactions: IDBObjectStore;
|
|
209
|
+
signatureAfter: IDBObjectStore;
|
|
210
|
+
};
|
|
211
|
+
}) => void)[];
|
|
212
|
+
}
|
|
213
|
+
| undefined;
|
|
214
|
+
currentTxID = 0;
|
|
215
|
+
|
|
216
|
+
makeRequest<T>(
|
|
217
|
+
handler: (stores: {
|
|
195
218
|
coValues: IDBObjectStore;
|
|
196
219
|
sessions: IDBObjectStore;
|
|
197
220
|
transactions: IDBObjectStore;
|
|
198
221
|
signatureAfter: IDBObjectStore;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
}) => IDBRequest
|
|
223
|
+
): SyncPromise<T> {
|
|
224
|
+
return new SyncPromise((resolve, reject) => {
|
|
225
|
+
let txEntry = this.currentTx;
|
|
226
|
+
|
|
227
|
+
const requestEntry = ({
|
|
228
|
+
stores,
|
|
229
|
+
}: {
|
|
230
|
+
stores: {
|
|
231
|
+
coValues: IDBObjectStore;
|
|
232
|
+
sessions: IDBObjectStore;
|
|
233
|
+
transactions: IDBObjectStore;
|
|
234
|
+
signatureAfter: IDBObjectStore;
|
|
235
|
+
};
|
|
236
|
+
}) => {
|
|
237
|
+
const request = handler(stores);
|
|
238
|
+
request.onerror = () => {
|
|
239
|
+
console.error("Error in request", request.error);
|
|
240
|
+
this.currentTx = undefined;
|
|
241
|
+
reject(request.error);
|
|
242
|
+
// TODO: recover pending requests in new tx
|
|
243
|
+
};
|
|
244
|
+
request.onsuccess = () => {
|
|
245
|
+
const value = request.result as T;
|
|
246
|
+
resolve(value);
|
|
205
247
|
|
|
206
|
-
|
|
207
|
-
? await promised<StoredSessionRow[]>(
|
|
208
|
-
sessions.index("sessionsByCoValue").getAll(coValueRow.rowID)
|
|
209
|
-
)
|
|
210
|
-
: [];
|
|
248
|
+
const next = txEntry!.pendingRequests.shift();
|
|
211
249
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
if (next) {
|
|
251
|
+
next({ stores });
|
|
252
|
+
} else {
|
|
253
|
+
if (this.currentTx === txEntry) {
|
|
254
|
+
this.currentTx = undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
};
|
|
217
259
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
header: theirKnown.header ? undefined : coValueRow?.header,
|
|
223
|
-
new: {},
|
|
224
|
-
},
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
for (const sessionRow of allOurSessions) {
|
|
228
|
-
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
sessionRow.lastIdx >
|
|
232
|
-
(theirKnown.sessions[sessionRow.sessionID] || 0)
|
|
233
|
-
) {
|
|
234
|
-
const firstNewTxIdx =
|
|
235
|
-
theirKnown.sessions[sessionRow.sessionID] || 0;
|
|
236
|
-
|
|
237
|
-
const signaturesAndIdxs = await promised<SignatureAfterRow[]>(
|
|
238
|
-
signatureAfter.getAll(
|
|
239
|
-
IDBKeyRange.bound(
|
|
240
|
-
[sessionRow.rowID, firstNewTxIdx],
|
|
241
|
-
[sessionRow.rowID, Infinity]
|
|
242
|
-
)
|
|
243
|
-
)
|
|
260
|
+
if (!txEntry || performance.now() - txEntry.startedAt > 20) {
|
|
261
|
+
const tx = this.db.transaction(
|
|
262
|
+
["coValues", "sessions", "transactions", "signatureAfter"],
|
|
263
|
+
"readwrite"
|
|
244
264
|
);
|
|
265
|
+
txEntry = {
|
|
266
|
+
id: this.currentTxID++,
|
|
267
|
+
tx,
|
|
268
|
+
stores: {
|
|
269
|
+
coValues: tx.objectStore("coValues"),
|
|
270
|
+
sessions: tx.objectStore("sessions"),
|
|
271
|
+
transactions: tx.objectStore("transactions"),
|
|
272
|
+
signatureAfter: tx.objectStore("signatureAfter"),
|
|
273
|
+
},
|
|
274
|
+
startedAt: performance.now(),
|
|
275
|
+
pendingRequests: [],
|
|
276
|
+
};
|
|
245
277
|
|
|
246
|
-
|
|
247
|
-
// theirKnown.id,
|
|
248
|
-
// "signaturesAndIdxs",
|
|
249
|
-
// JSON.stringify(signaturesAndIdxs)
|
|
250
|
-
// );
|
|
278
|
+
console.time("IndexedDB TX" + txEntry.id);
|
|
251
279
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
[sessionRow.rowID, firstNewTxIdx],
|
|
256
|
-
[sessionRow.rowID, Infinity]
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
);
|
|
280
|
+
txEntry.tx.oncomplete = () => {
|
|
281
|
+
console.timeEnd("IndexedDB TX" + txEntry!.id);
|
|
282
|
+
};
|
|
260
283
|
|
|
261
|
-
|
|
284
|
+
this.currentTx = txEntry;
|
|
262
285
|
|
|
286
|
+
requestEntry(txEntry);
|
|
287
|
+
} else {
|
|
288
|
+
txEntry.pendingRequests.push(requestEntry);
|
|
263
289
|
// console.log(
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// newTxInSession.length
|
|
290
|
+
// "Queued request in TX " + txEntry.id,
|
|
291
|
+
// txEntry.pendingRequests.length
|
|
267
292
|
// );
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
268
296
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
297
|
+
sendNewContentAfter(
|
|
298
|
+
theirKnown: CojsonInternalTypes.CoValueKnownState,
|
|
299
|
+
asDependencyOf?: CojsonInternalTypes.RawCoID
|
|
300
|
+
): SyncPromise<void> {
|
|
301
|
+
return this.makeRequest<StoredCoValueRow | undefined>(({ coValues }) =>
|
|
302
|
+
coValues.index("coValuesById").get(theirKnown.id)
|
|
303
|
+
)
|
|
304
|
+
.then((coValueRow) => {
|
|
305
|
+
return (
|
|
306
|
+
coValueRow
|
|
307
|
+
? this.makeRequest<StoredSessionRow[]>(({ sessions }) =>
|
|
308
|
+
sessions
|
|
309
|
+
.index("sessionsByCoValue")
|
|
310
|
+
.getAll(coValueRow.rowID)
|
|
311
|
+
)
|
|
312
|
+
: SyncPromise.resolve([])
|
|
313
|
+
).then((allOurSessions) => {
|
|
314
|
+
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
|
|
315
|
+
id: theirKnown.id,
|
|
316
|
+
header: !!coValueRow,
|
|
317
|
+
sessions: {},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const newContentPieces: CojsonInternalTypes.NewContentMessage[] =
|
|
321
|
+
[
|
|
322
|
+
{
|
|
323
|
+
action: "content",
|
|
324
|
+
id: theirKnown.id,
|
|
325
|
+
header: theirKnown.header
|
|
326
|
+
? undefined
|
|
327
|
+
: coValueRow?.header,
|
|
328
|
+
new: {},
|
|
329
|
+
},
|
|
273
330
|
];
|
|
274
|
-
if (!sessionEntry) {
|
|
275
|
-
sessionEntry = {
|
|
276
|
-
after: idx,
|
|
277
|
-
lastSignature:
|
|
278
|
-
"WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
|
|
279
|
-
newTransactions: [],
|
|
280
|
-
};
|
|
281
|
-
newContentPieces[newContentPieces.length - 1]!.new[
|
|
282
|
-
sessionRow.sessionID
|
|
283
|
-
] = sessionEntry;
|
|
284
|
-
}
|
|
285
331
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
332
|
+
return SyncPromise.all(
|
|
333
|
+
allOurSessions.map((sessionRow) => {
|
|
334
|
+
ourKnown.sessions[sessionRow.sessionID] =
|
|
335
|
+
sessionRow.lastIdx;
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
sessionRow.lastIdx >
|
|
339
|
+
(theirKnown.sessions[sessionRow.sessionID] || 0)
|
|
340
|
+
) {
|
|
341
|
+
const firstNewTxIdx =
|
|
342
|
+
theirKnown.sessions[sessionRow.sessionID] ||
|
|
343
|
+
0;
|
|
344
|
+
|
|
345
|
+
return this.makeRequest<SignatureAfterRow[]>(
|
|
346
|
+
({ signatureAfter }) =>
|
|
347
|
+
signatureAfter.getAll(
|
|
348
|
+
IDBKeyRange.bound(
|
|
349
|
+
[
|
|
350
|
+
sessionRow.rowID,
|
|
351
|
+
firstNewTxIdx,
|
|
352
|
+
],
|
|
353
|
+
[sessionRow.rowID, Infinity]
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
).then((signaturesAndIdxs) => {
|
|
357
|
+
// console.log(
|
|
358
|
+
// theirKnown.id,
|
|
359
|
+
// "signaturesAndIdxs",
|
|
360
|
+
// JSON.stringify(signaturesAndIdxs)
|
|
361
|
+
// );
|
|
362
|
+
|
|
363
|
+
return this.makeRequest<TransactionRow[]>(
|
|
364
|
+
({ transactions }) =>
|
|
365
|
+
transactions.getAll(
|
|
366
|
+
IDBKeyRange.bound(
|
|
367
|
+
[
|
|
368
|
+
sessionRow.rowID,
|
|
369
|
+
firstNewTxIdx,
|
|
370
|
+
],
|
|
371
|
+
[sessionRow.rowID, Infinity]
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
).then((newTxsInSession) => {
|
|
375
|
+
collectNewTxs(
|
|
376
|
+
newTxsInSession,
|
|
377
|
+
newContentPieces,
|
|
378
|
+
sessionRow,
|
|
379
|
+
signaturesAndIdxs,
|
|
380
|
+
theirKnown,
|
|
381
|
+
firstNewTxIdx
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
return SyncPromise.resolve();
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
).then(() => {
|
|
390
|
+
const dependedOnCoValues = getDependedOnCoValues(
|
|
391
|
+
coValueRow,
|
|
392
|
+
newContentPieces,
|
|
393
|
+
theirKnown
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return SyncPromise.all(
|
|
397
|
+
dependedOnCoValues.map((dependedOnCoValue) =>
|
|
398
|
+
this.sendNewContentAfter(
|
|
399
|
+
{
|
|
400
|
+
id: dependedOnCoValue,
|
|
401
|
+
header: false,
|
|
402
|
+
sessions: {},
|
|
403
|
+
},
|
|
404
|
+
asDependencyOf || theirKnown.id
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
).then(() => {
|
|
408
|
+
// we're done with IndexedDB stuff here so can use native Promises again
|
|
409
|
+
setTimeout(async () => {
|
|
410
|
+
await this.toLocalNode.write({
|
|
411
|
+
action: "known",
|
|
412
|
+
...ourKnown,
|
|
413
|
+
asDependencyOf,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const nonEmptyNewContentPieces =
|
|
417
|
+
newContentPieces.filter(
|
|
418
|
+
(piece) =>
|
|
419
|
+
piece.header ||
|
|
420
|
+
Object.keys(piece.new).length > 0
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// console.log(theirKnown.id, nonEmptyNewContentPieces);
|
|
424
|
+
|
|
425
|
+
for (const piece of nonEmptyNewContentPieces) {
|
|
426
|
+
await this.toLocalNode.write(piece);
|
|
427
|
+
await new Promise((resolve) =>
|
|
428
|
+
setTimeout(resolve, 0)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}, 0);
|
|
432
|
+
|
|
433
|
+
return Promise.resolve();
|
|
299
434
|
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
idx += 1;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const dependedOnCoValues =
|
|
312
|
-
coValueRow?.header.ruleset.type === "group"
|
|
313
|
-
? newContentPieces
|
|
314
|
-
.flatMap((piece) => Object.values(piece.new))
|
|
315
|
-
.flatMap((sessionEntry) =>
|
|
316
|
-
sessionEntry.newTransactions.flatMap((tx) => {
|
|
317
|
-
if (tx.privacy !== "trusting") return [];
|
|
318
|
-
// TODO: avoid parse here?
|
|
319
|
-
return cojsonInternals
|
|
320
|
-
.parseJSON(tx.changes)
|
|
321
|
-
.map(
|
|
322
|
-
(change) =>
|
|
323
|
-
change &&
|
|
324
|
-
typeof change === "object" &&
|
|
325
|
-
"op" in change &&
|
|
326
|
-
change.op === "set" &&
|
|
327
|
-
"key" in change &&
|
|
328
|
-
change.key
|
|
329
|
-
)
|
|
330
|
-
.filter(
|
|
331
|
-
(
|
|
332
|
-
key
|
|
333
|
-
): key is CojsonInternalTypes.RawCoID =>
|
|
334
|
-
typeof key === "string" &&
|
|
335
|
-
key.startsWith("co_")
|
|
336
|
-
);
|
|
337
|
-
})
|
|
338
|
-
)
|
|
339
|
-
: coValueRow?.header.ruleset.type === "ownedByGroup"
|
|
340
|
-
? [
|
|
341
|
-
coValueRow?.header.ruleset.group,
|
|
342
|
-
...new Set(
|
|
343
|
-
newContentPieces.flatMap((piece) =>
|
|
344
|
-
Object.keys(piece)
|
|
345
|
-
.map((sessionID) =>
|
|
346
|
-
cojsonInternals.accountOrAgentIDfromSessionID(
|
|
347
|
-
sessionID as SessionID
|
|
348
|
-
)
|
|
349
|
-
)
|
|
350
|
-
.filter(
|
|
351
|
-
(accountID): accountID is AccountID =>
|
|
352
|
-
cojsonInternals.isAccountID(
|
|
353
|
-
accountID
|
|
354
|
-
) && accountID !== theirKnown.id
|
|
355
|
-
)
|
|
356
|
-
)
|
|
357
|
-
),
|
|
358
|
-
]
|
|
359
|
-
: [];
|
|
360
|
-
|
|
361
|
-
for (const dependedOnCoValue of dependedOnCoValues) {
|
|
362
|
-
await this.sendNewContentAfter(
|
|
363
|
-
{ id: dependedOnCoValue, header: false, sessions: {} },
|
|
364
|
-
{ coValues, sessions, transactions, signatureAfter },
|
|
365
|
-
asDependencyOf || theirKnown.id
|
|
366
|
-
);
|
|
367
|
-
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
})
|
|
438
|
+
.then(() => {});
|
|
439
|
+
}
|
|
368
440
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
asDependencyOf,
|
|
373
|
-
});
|
|
441
|
+
handleLoad(msg: CojsonInternalTypes.LoadMessage) {
|
|
442
|
+
return this.sendNewContentAfter(msg);
|
|
443
|
+
}
|
|
374
444
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
445
|
+
handleContent(
|
|
446
|
+
msg: CojsonInternalTypes.NewContentMessage
|
|
447
|
+
): SyncPromise<void> {
|
|
448
|
+
return this.makeRequest<StoredCoValueRow | undefined>(({ coValues }) =>
|
|
449
|
+
coValues.index("coValuesById").get(msg.id)
|
|
450
|
+
)
|
|
451
|
+
.then((coValueRow) => {
|
|
452
|
+
if (coValueRow?.rowID === undefined) {
|
|
453
|
+
const header = msg.header;
|
|
454
|
+
if (!header) {
|
|
455
|
+
console.error("Expected to be sent header first");
|
|
456
|
+
this.toLocalNode.write({
|
|
457
|
+
action: "known",
|
|
458
|
+
id: msg.id,
|
|
459
|
+
header: false,
|
|
460
|
+
sessions: {},
|
|
461
|
+
isCorrection: true,
|
|
462
|
+
});
|
|
463
|
+
throw new Error("Expected to be sent header first");
|
|
464
|
+
}
|
|
378
465
|
|
|
379
|
-
|
|
466
|
+
return this.makeRequest<IDBValidKey>(({ coValues }) =>
|
|
467
|
+
coValues.put({
|
|
468
|
+
id: msg.id,
|
|
469
|
+
header: header,
|
|
470
|
+
} satisfies CoValueRow)
|
|
471
|
+
) as SyncPromise<number>;
|
|
472
|
+
} else {
|
|
473
|
+
return SyncPromise.resolve(coValueRow.rowID);
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
.then((storedCoValueRowID: number) => {
|
|
477
|
+
this.makeRequest<StoredSessionRow[]>(({ sessions }) =>
|
|
478
|
+
sessions
|
|
479
|
+
.index("sessionsByCoValue")
|
|
480
|
+
.getAll(storedCoValueRowID)
|
|
481
|
+
).then((allOurSessionsEntries) => {
|
|
482
|
+
const allOurSessions: {
|
|
483
|
+
[sessionID: SessionID]: StoredSessionRow;
|
|
484
|
+
} = Object.fromEntries(
|
|
485
|
+
allOurSessionsEntries.map((row) => [row.sessionID, row])
|
|
486
|
+
);
|
|
380
487
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
488
|
+
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
|
|
489
|
+
id: msg.id,
|
|
490
|
+
header: true,
|
|
491
|
+
sessions: {},
|
|
492
|
+
};
|
|
493
|
+
let invalidAssumptions = false;
|
|
494
|
+
|
|
495
|
+
return Promise.all(
|
|
496
|
+
(Object.keys(msg.new) as SessionID[]).map(
|
|
497
|
+
(sessionID) => {
|
|
498
|
+
const sessionRow = allOurSessions[sessionID];
|
|
499
|
+
if (sessionRow) {
|
|
500
|
+
ourKnown.sessions[sessionRow.sessionID] =
|
|
501
|
+
sessionRow.lastIdx;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
(sessionRow?.lastIdx || 0) <
|
|
506
|
+
(msg.new[sessionID]?.after || 0)
|
|
507
|
+
) {
|
|
508
|
+
invalidAssumptions = true;
|
|
509
|
+
} else {
|
|
510
|
+
return this.putNewTxs(
|
|
511
|
+
msg,
|
|
512
|
+
sessionID,
|
|
513
|
+
sessionRow,
|
|
514
|
+
storedCoValueRowID
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
).then(() => {
|
|
520
|
+
if (invalidAssumptions) {
|
|
521
|
+
this.toLocalNode.write({
|
|
522
|
+
action: "known",
|
|
523
|
+
...ourKnown,
|
|
524
|
+
isCorrection: invalidAssumptions,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
});
|
|
385
530
|
}
|
|
386
531
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
532
|
+
private putNewTxs(
|
|
533
|
+
msg: CojsonInternalTypes.NewContentMessage,
|
|
534
|
+
sessionID: SessionID,
|
|
535
|
+
sessionRow: StoredSessionRow | undefined,
|
|
536
|
+
storedCoValueRowID: number
|
|
537
|
+
) {
|
|
538
|
+
const newTransactions = msg.new[sessionID]?.newTransactions || [];
|
|
539
|
+
|
|
540
|
+
const actuallyNewOffset =
|
|
541
|
+
(sessionRow?.lastIdx || 0) - (msg.new[sessionID]?.after || 0);
|
|
542
|
+
|
|
543
|
+
const actuallyNewTransactions =
|
|
544
|
+
newTransactions.slice(actuallyNewOffset);
|
|
545
|
+
|
|
546
|
+
let newBytesSinceLastSignature =
|
|
547
|
+
(sessionRow?.bytesSinceLastSignature || 0) +
|
|
548
|
+
actuallyNewTransactions.reduce(
|
|
549
|
+
(sum, tx) =>
|
|
550
|
+
sum +
|
|
551
|
+
(tx.privacy === "private"
|
|
552
|
+
? tx.encryptedChanges.length
|
|
553
|
+
: tx.changes.length),
|
|
554
|
+
0
|
|
555
|
+
);
|
|
390
556
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.inTransaction("readwrite");
|
|
557
|
+
const newLastIdx =
|
|
558
|
+
(sessionRow?.lastIdx || 0) + actuallyNewTransactions.length;
|
|
394
559
|
|
|
395
|
-
let
|
|
396
|
-
await promised<StoredCoValueRow | undefined>(
|
|
397
|
-
coValues.index("coValuesById").get(msg.id)
|
|
398
|
-
)
|
|
399
|
-
)?.rowID;
|
|
400
|
-
|
|
401
|
-
if (storedCoValueRowID === undefined) {
|
|
402
|
-
const header = msg.header;
|
|
403
|
-
if (!header) {
|
|
404
|
-
console.error("Expected to be sent header first");
|
|
405
|
-
await this.toLocalNode.write({
|
|
406
|
-
action: "known",
|
|
407
|
-
id: msg.id,
|
|
408
|
-
header: false,
|
|
409
|
-
sessions: {},
|
|
410
|
-
isCorrection: true,
|
|
411
|
-
});
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
560
|
+
let shouldWriteSignature = false;
|
|
414
561
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
header: header,
|
|
419
|
-
} satisfies CoValueRow)
|
|
420
|
-
)) as number;
|
|
562
|
+
if (newBytesSinceLastSignature > MAX_RECOMMENDED_TX_SIZE) {
|
|
563
|
+
shouldWriteSignature = true;
|
|
564
|
+
newBytesSinceLastSignature = 0;
|
|
421
565
|
}
|
|
422
566
|
|
|
423
|
-
const
|
|
424
|
-
[sessionID: SessionID]: StoredSessionRow;
|
|
425
|
-
}>((resolve) => {
|
|
426
|
-
const allOurSessionsRequest = sessions
|
|
427
|
-
.index("sessionsByCoValue")
|
|
428
|
-
.getAll(storedCoValueRowID);
|
|
429
|
-
|
|
430
|
-
allOurSessionsRequest.onsuccess = () => {
|
|
431
|
-
resolve(
|
|
432
|
-
Object.fromEntries(
|
|
433
|
-
(
|
|
434
|
-
allOurSessionsRequest.result as StoredSessionRow[]
|
|
435
|
-
).map((row) => [row.sessionID, row])
|
|
436
|
-
)
|
|
437
|
-
);
|
|
438
|
-
};
|
|
439
|
-
});
|
|
567
|
+
const nextIdx = sessionRow?.lastIdx || 0;
|
|
440
568
|
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
569
|
+
const sessionUpdate = {
|
|
570
|
+
coValue: storedCoValueRowID,
|
|
571
|
+
sessionID: sessionID,
|
|
572
|
+
lastIdx: newLastIdx,
|
|
573
|
+
lastSignature: msg.new[sessionID]!.lastSignature,
|
|
574
|
+
bytesSinceLastSignature: newBytesSinceLastSignature,
|
|
445
575
|
};
|
|
446
|
-
let invalidAssumptions = false;
|
|
447
|
-
|
|
448
|
-
for (const sessionID of Object.keys(msg.new) as SessionID[]) {
|
|
449
|
-
const sessionRow = allOurSessions[sessionID];
|
|
450
|
-
if (sessionRow) {
|
|
451
|
-
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
|
452
|
-
}
|
|
453
576
|
|
|
454
|
-
|
|
455
|
-
|
|
577
|
+
return this.makeRequest<number>(({ sessions }) =>
|
|
578
|
+
sessions.put(
|
|
579
|
+
sessionRow?.rowID
|
|
580
|
+
? {
|
|
581
|
+
rowID: sessionRow.rowID,
|
|
582
|
+
...sessionUpdate,
|
|
583
|
+
}
|
|
584
|
+
: sessionUpdate
|
|
585
|
+
)
|
|
586
|
+
).then((sessionRowID) => {
|
|
587
|
+
let maybePutRequest;
|
|
588
|
+
if (shouldWriteSignature) {
|
|
589
|
+
maybePutRequest = this.makeRequest(({ signatureAfter }) =>
|
|
590
|
+
signatureAfter.put({
|
|
591
|
+
ses: sessionRowID,
|
|
592
|
+
// TODO: newLastIdx is a misnomer, it's actually more like nextIdx or length
|
|
593
|
+
idx: newLastIdx - 1,
|
|
594
|
+
signature: msg.new[sessionID]!.lastSignature,
|
|
595
|
+
} satisfies SignatureAfterRow)
|
|
596
|
+
);
|
|
456
597
|
} else {
|
|
457
|
-
|
|
458
|
-
msg.new[sessionID]?.newTransactions || [];
|
|
459
|
-
|
|
460
|
-
const actuallyNewOffset =
|
|
461
|
-
(sessionRow?.lastIdx || 0) -
|
|
462
|
-
(msg.new[sessionID]?.after || 0);
|
|
463
|
-
|
|
464
|
-
const actuallyNewTransactions =
|
|
465
|
-
newTransactions.slice(actuallyNewOffset);
|
|
466
|
-
|
|
467
|
-
let newBytesSinceLastSignature =
|
|
468
|
-
(sessionRow?.bytesSinceLastSignature || 0) +
|
|
469
|
-
actuallyNewTransactions.reduce(
|
|
470
|
-
(sum, tx) =>
|
|
471
|
-
sum +
|
|
472
|
-
(tx.privacy === "private"
|
|
473
|
-
? tx.encryptedChanges.length
|
|
474
|
-
: tx.changes.length),
|
|
475
|
-
0
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
const newLastIdx =
|
|
479
|
-
(sessionRow?.lastIdx || 0) + actuallyNewTransactions.length;
|
|
480
|
-
|
|
481
|
-
let shouldWriteSignature = false;
|
|
482
|
-
|
|
483
|
-
if (newBytesSinceLastSignature > MAX_RECOMMENDED_TX_SIZE) {
|
|
484
|
-
shouldWriteSignature = true;
|
|
485
|
-
newBytesSinceLastSignature = 0;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
let nextIdx = sessionRow?.lastIdx || 0;
|
|
489
|
-
|
|
490
|
-
const sessionUpdate = {
|
|
491
|
-
coValue: storedCoValueRowID,
|
|
492
|
-
sessionID: sessionID,
|
|
493
|
-
lastIdx: newLastIdx,
|
|
494
|
-
lastSignature: msg.new[sessionID]!.lastSignature,
|
|
495
|
-
bytesSinceLastSignature: newBytesSinceLastSignature,
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
const sessionRowID = (await promised(
|
|
499
|
-
sessions.put(
|
|
500
|
-
sessionRow?.rowID
|
|
501
|
-
? {
|
|
502
|
-
rowID: sessionRow.rowID,
|
|
503
|
-
...sessionUpdate,
|
|
504
|
-
}
|
|
505
|
-
: sessionUpdate
|
|
506
|
-
)
|
|
507
|
-
)) as number;
|
|
508
|
-
|
|
509
|
-
if (shouldWriteSignature) {
|
|
510
|
-
await promised(
|
|
511
|
-
signatureAfter.put({
|
|
512
|
-
ses: sessionRowID,
|
|
513
|
-
// TODO: newLastIdx is a misnomer, it's actually more like nextIdx or length
|
|
514
|
-
idx: newLastIdx - 1,
|
|
515
|
-
signature: msg.new[sessionID]!.lastSignature,
|
|
516
|
-
} satisfies SignatureAfterRow)
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
for (const newTransaction of actuallyNewTransactions) {
|
|
521
|
-
await promised(
|
|
522
|
-
transactions.add({
|
|
523
|
-
ses: sessionRowID,
|
|
524
|
-
idx: nextIdx,
|
|
525
|
-
tx: newTransaction,
|
|
526
|
-
} satisfies TransactionRow)
|
|
527
|
-
);
|
|
528
|
-
nextIdx++;
|
|
529
|
-
}
|
|
598
|
+
maybePutRequest = SyncPromise.resolve();
|
|
530
599
|
}
|
|
531
|
-
}
|
|
532
600
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
601
|
+
return maybePutRequest.then(() =>
|
|
602
|
+
Promise.all(
|
|
603
|
+
actuallyNewTransactions.map((newTransaction, i) => {
|
|
604
|
+
return this.makeRequest(({ transactions }) =>
|
|
605
|
+
transactions.add({
|
|
606
|
+
ses: sessionRowID,
|
|
607
|
+
idx: nextIdx + i,
|
|
608
|
+
tx: newTransaction,
|
|
609
|
+
} satisfies TransactionRow)
|
|
610
|
+
);
|
|
611
|
+
})
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
});
|
|
540
615
|
}
|
|
541
616
|
|
|
542
617
|
handleKnown(msg: CojsonInternalTypes.KnownStateMessage) {
|
|
543
|
-
return this.sendNewContentAfter(msg
|
|
618
|
+
return this.sendNewContentAfter(msg);
|
|
544
619
|
}
|
|
545
620
|
|
|
546
621
|
handleDone(_msg: CojsonInternalTypes.DoneMessage) {}
|
|
547
622
|
|
|
548
|
-
inTransaction(mode: "readwrite" | "readonly"): {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
} {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
623
|
+
// inTransaction(mode: "readwrite" | "readonly"): {
|
|
624
|
+
// coValues: IDBObjectStore;
|
|
625
|
+
// sessions: IDBObjectStore;
|
|
626
|
+
// transactions: IDBObjectStore;
|
|
627
|
+
// signatureAfter: IDBObjectStore;
|
|
628
|
+
// } {
|
|
629
|
+
// const tx = this.db.transaction(
|
|
630
|
+
// ["coValues", "sessions", "transactions", "signatureAfter"],
|
|
631
|
+
// mode
|
|
632
|
+
// );
|
|
633
|
+
|
|
634
|
+
// const txID = lastTx;
|
|
635
|
+
// lastTx++;
|
|
636
|
+
// console.time("IndexedDB TX" + txID);
|
|
637
|
+
|
|
638
|
+
// tx.onerror = (event) => {
|
|
639
|
+
// const target = event.target as unknown as {
|
|
640
|
+
// error: DOMException;
|
|
641
|
+
// source?: { name: string };
|
|
642
|
+
// } | null;
|
|
643
|
+
// throw new Error(
|
|
644
|
+
// `Error in transaction (${target?.source?.name}): ${target?.error}`,
|
|
645
|
+
// { cause: target?.error }
|
|
646
|
+
// );
|
|
647
|
+
// };
|
|
648
|
+
// tx.oncomplete = () => {
|
|
649
|
+
// console.timeEnd("IndexedDB TX" + txID);
|
|
650
|
+
// }
|
|
651
|
+
// const coValues = tx.objectStore("coValues");
|
|
652
|
+
// const sessions = tx.objectStore("sessions");
|
|
653
|
+
// const transactions = tx.objectStore("transactions");
|
|
654
|
+
// const signatureAfter = tx.objectStore("signatureAfter");
|
|
655
|
+
|
|
656
|
+
// return { coValues, sessions, transactions, signatureAfter };
|
|
657
|
+
// }
|
|
658
|
+
}
|
|
558
659
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
660
|
+
function collectNewTxs(
|
|
661
|
+
newTxsInSession: TransactionRow[],
|
|
662
|
+
newContentPieces: CojsonInternalTypes.NewContentMessage[],
|
|
663
|
+
sessionRow: StoredSessionRow,
|
|
664
|
+
signaturesAndIdxs: SignatureAfterRow[],
|
|
665
|
+
theirKnown: CojsonInternalTypes.CoValueKnownState,
|
|
666
|
+
firstNewTxIdx: number
|
|
667
|
+
) {
|
|
668
|
+
let idx = firstNewTxIdx;
|
|
669
|
+
|
|
670
|
+
// console.log(
|
|
671
|
+
// theirKnown.id,
|
|
672
|
+
// "newTxInSession",
|
|
673
|
+
// newTxInSession.length
|
|
674
|
+
// );
|
|
675
|
+
for (const tx of newTxsInSession) {
|
|
676
|
+
let sessionEntry =
|
|
677
|
+
newContentPieces[newContentPieces.length - 1]!.new[
|
|
678
|
+
sessionRow.sessionID
|
|
679
|
+
];
|
|
680
|
+
if (!sessionEntry) {
|
|
681
|
+
sessionEntry = {
|
|
682
|
+
after: idx,
|
|
683
|
+
lastSignature:
|
|
684
|
+
"WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
|
|
685
|
+
newTransactions: [],
|
|
686
|
+
};
|
|
687
|
+
newContentPieces[newContentPieces.length - 1]!.new[
|
|
688
|
+
sessionRow.sessionID
|
|
689
|
+
] = sessionEntry;
|
|
690
|
+
}
|
|
573
691
|
|
|
574
|
-
|
|
692
|
+
sessionEntry.newTransactions.push(tx.tx);
|
|
693
|
+
|
|
694
|
+
if (signaturesAndIdxs[0] && idx === signaturesAndIdxs[0].idx) {
|
|
695
|
+
sessionEntry.lastSignature = signaturesAndIdxs[0].signature;
|
|
696
|
+
signaturesAndIdxs.shift();
|
|
697
|
+
newContentPieces.push({
|
|
698
|
+
action: "content",
|
|
699
|
+
id: theirKnown.id,
|
|
700
|
+
new: {},
|
|
701
|
+
});
|
|
702
|
+
} else if (idx === firstNewTxIdx + newTxsInSession.length - 1) {
|
|
703
|
+
sessionEntry.lastSignature = sessionRow.lastSignature;
|
|
704
|
+
}
|
|
705
|
+
idx += 1;
|
|
575
706
|
}
|
|
576
707
|
}
|
|
577
708
|
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
709
|
+
function getDependedOnCoValues(
|
|
710
|
+
coValueRow: StoredCoValueRow | undefined,
|
|
711
|
+
newContentPieces: CojsonInternalTypes.NewContentMessage[],
|
|
712
|
+
theirKnown: CojsonInternalTypes.CoValueKnownState
|
|
713
|
+
) {
|
|
714
|
+
return coValueRow?.header.ruleset.type === "group"
|
|
715
|
+
? newContentPieces
|
|
716
|
+
.flatMap((piece) => Object.values(piece.new))
|
|
717
|
+
.flatMap((sessionEntry) =>
|
|
718
|
+
sessionEntry.newTransactions.flatMap((tx) => {
|
|
719
|
+
if (tx.privacy !== "trusting") return [];
|
|
720
|
+
// TODO: avoid parse here?
|
|
721
|
+
return cojsonInternals
|
|
722
|
+
.parseJSON(tx.changes)
|
|
723
|
+
.map(
|
|
724
|
+
(change) =>
|
|
725
|
+
change &&
|
|
726
|
+
typeof change === "object" &&
|
|
727
|
+
"op" in change &&
|
|
728
|
+
change.op === "set" &&
|
|
729
|
+
"key" in change &&
|
|
730
|
+
change.key
|
|
731
|
+
)
|
|
732
|
+
.filter(
|
|
733
|
+
(key): key is CojsonInternalTypes.RawCoID =>
|
|
734
|
+
typeof key === "string" &&
|
|
735
|
+
key.startsWith("co_")
|
|
736
|
+
);
|
|
737
|
+
})
|
|
738
|
+
)
|
|
739
|
+
: coValueRow?.header.ruleset.type === "ownedByGroup"
|
|
740
|
+
? [
|
|
741
|
+
coValueRow?.header.ruleset.group,
|
|
742
|
+
...new Set(
|
|
743
|
+
newContentPieces.flatMap((piece) =>
|
|
744
|
+
Object.keys(piece)
|
|
745
|
+
.map((sessionID) =>
|
|
746
|
+
cojsonInternals.accountOrAgentIDfromSessionID(
|
|
747
|
+
sessionID as SessionID
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
.filter(
|
|
751
|
+
(accountID): accountID is AccountID =>
|
|
752
|
+
cojsonInternals.isAccountID(accountID) &&
|
|
753
|
+
accountID !== theirKnown.id
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
),
|
|
757
|
+
]
|
|
758
|
+
: [];
|
|
587
759
|
}
|
|
760
|
+
// let lastTx = 0;
|
|
761
|
+
|
|
762
|
+
// function promised<T>(request: IDBRequest<T>): Promise<T> {
|
|
763
|
+
// return new Promise<T>((resolve, reject) => {
|
|
764
|
+
// request.onsuccess = () => {
|
|
765
|
+
// resolve(request.result);
|
|
766
|
+
// };
|
|
767
|
+
// request.onerror = () => {
|
|
768
|
+
// reject(request.error);
|
|
769
|
+
// };
|
|
770
|
+
// });
|
|
771
|
+
// }
|