cojson-storage-indexeddb 0.6.0 → 0.6.2

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/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
- // fix embarrassing off-by-one error for transaction indices
149
- console.log("Migration: fixing off-by-one error");
150
- const transaction = (
151
- ev.target as unknown as { transaction: IDBTransaction }
152
- ).transaction;
153
-
154
- const txsStore = transaction.objectStore("transactions");
155
- const txs = await promised(txsStore.getAll());
156
-
157
- for (const tx of txs) {
158
- await promised(txsStore.delete([tx.ses, tx.idx]));
159
- tx.idx -= 1;
160
- await promised(txsStore.add(tx));
161
- }
162
- console.log("Migration: fixing off-by-one error - done");
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
- async sendNewContentAfter(
188
- theirKnown: CojsonInternalTypes.CoValueKnownState,
189
- {
190
- coValues,
191
- sessions,
192
- transactions,
193
- signatureAfter,
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
- asDependencyOf?: CojsonInternalTypes.RawCoID
201
- ) {
202
- const coValueRow = await promised<StoredCoValueRow | undefined>(
203
- coValues.index("coValuesById").get(theirKnown.id)
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
- const allOurSessions = coValueRow
207
- ? await promised<StoredSessionRow[]>(
208
- sessions.index("sessionsByCoValue").getAll(coValueRow.rowID)
209
- )
210
- : [];
248
+ const next = txEntry!.pendingRequests.shift();
211
249
 
212
- const ourKnown: CojsonInternalTypes.CoValueKnownState = {
213
- id: theirKnown.id,
214
- header: !!coValueRow,
215
- sessions: {},
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
- const newContentPieces: CojsonInternalTypes.NewContentMessage[] = [
219
- {
220
- action: "content",
221
- id: theirKnown.id,
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
- // console.log(
247
- // theirKnown.id,
248
- // "signaturesAndIdxs",
249
- // JSON.stringify(signaturesAndIdxs)
250
- // );
278
+ console.time("IndexedDB TX" + txEntry.id);
251
279
 
252
- const newTxInSession = await promised<TransactionRow[]>(
253
- transactions.getAll(
254
- IDBKeyRange.bound(
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
- let idx = firstNewTxIdx;
284
+ this.currentTx = txEntry;
262
285
 
286
+ requestEntry(txEntry);
287
+ } else {
288
+ txEntry.pendingRequests.push(requestEntry);
263
289
  // console.log(
264
- // theirKnown.id,
265
- // "newTxInSession",
266
- // newTxInSession.length
290
+ // "Queued request in TX " + txEntry.id,
291
+ // txEntry.pendingRequests.length
267
292
  // );
293
+ }
294
+ });
295
+ }
268
296
 
269
- for (const tx of newTxInSession) {
270
- let sessionEntry =
271
- newContentPieces[newContentPieces.length - 1]!.new[
272
- sessionRow.sessionID
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
- sessionEntry.newTransactions.push(tx.tx);
287
-
288
- if (
289
- signaturesAndIdxs[0] &&
290
- idx === signaturesAndIdxs[0].idx
291
- ) {
292
- sessionEntry.lastSignature =
293
- signaturesAndIdxs[0].signature;
294
- signaturesAndIdxs.shift();
295
- newContentPieces.push({
296
- action: "content",
297
- id: theirKnown.id,
298
- new: {},
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
- } else if (
301
- idx ===
302
- firstNewTxIdx + newTxInSession.length - 1
303
- ) {
304
- sessionEntry.lastSignature = sessionRow.lastSignature;
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
- await this.toLocalNode.write({
370
- action: "known",
371
- ...ourKnown,
372
- asDependencyOf,
373
- });
441
+ handleLoad(msg: CojsonInternalTypes.LoadMessage) {
442
+ return this.sendNewContentAfter(msg);
443
+ }
374
444
 
375
- const nonEmptyNewContentPieces = newContentPieces.filter(
376
- (piece) => piece.header || Object.keys(piece.new).length > 0
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
- // console.log(theirKnown.id, nonEmptyNewContentPieces);
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
- for (const piece of nonEmptyNewContentPieces) {
382
- await this.toLocalNode.write(piece);
383
- await new Promise((resolve) => setTimeout(resolve, 0));
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
- handleLoad(msg: CojsonInternalTypes.LoadMessage) {
388
- return this.sendNewContentAfter(msg, this.inTransaction("readonly"));
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
- async handleContent(msg: CojsonInternalTypes.NewContentMessage) {
392
- const { coValues, sessions, transactions, signatureAfter } =
393
- this.inTransaction("readwrite");
557
+ const newLastIdx =
558
+ (sessionRow?.lastIdx || 0) + actuallyNewTransactions.length;
394
559
 
395
- let storedCoValueRowID = (
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
- storedCoValueRowID = (await promised<IDBValidKey>(
416
- coValues.put({
417
- id: msg.id,
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 allOurSessions = await new Promise<{
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 ourKnown: CojsonInternalTypes.CoValueKnownState = {
442
- id: msg.id,
443
- header: true,
444
- sessions: {},
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
- if ((sessionRow?.lastIdx || 0) < (msg.new[sessionID]?.after || 0)) {
455
- invalidAssumptions = true;
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
- const newTransactions =
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
- if (invalidAssumptions) {
534
- await this.toLocalNode.write({
535
- action: "known",
536
- ...ourKnown,
537
- isCorrection: invalidAssumptions,
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, this.inTransaction("readonly"));
618
+ return this.sendNewContentAfter(msg);
544
619
  }
545
620
 
546
621
  handleDone(_msg: CojsonInternalTypes.DoneMessage) {}
547
622
 
548
- inTransaction(mode: "readwrite" | "readonly"): {
549
- coValues: IDBObjectStore;
550
- sessions: IDBObjectStore;
551
- transactions: IDBObjectStore;
552
- signatureAfter: IDBObjectStore;
553
- } {
554
- const tx = this.db.transaction(
555
- ["coValues", "sessions", "transactions", "signatureAfter"],
556
- mode
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
- tx.onerror = (event) => {
560
- const target = event.target as unknown as {
561
- error: DOMException;
562
- source?: { name: string };
563
- } | null;
564
- throw new Error(
565
- `Error in transaction (${target?.source?.name}): ${target?.error}`,
566
- { cause: target?.error }
567
- );
568
- };
569
- const coValues = tx.objectStore("coValues");
570
- const sessions = tx.objectStore("sessions");
571
- const transactions = tx.objectStore("transactions");
572
- const signatureAfter = tx.objectStore("signatureAfter");
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
- return { coValues, sessions, transactions, signatureAfter };
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 promised<T>(request: IDBRequest<T>): Promise<T> {
579
- return new Promise<T>((resolve, reject) => {
580
- request.onsuccess = () => {
581
- resolve(request.result);
582
- };
583
- request.onerror = () => {
584
- reject(request.error);
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
+ // }