cojson-storage-indexeddb 0.8.32 → 0.8.35

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.
@@ -0,0 +1,251 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest";
2
+ import { SyncManager } from "../syncManager";
3
+ import { getDependedOnCoValues } from "../syncUtils";
4
+ import { fixtures } from "./fixtureMessages";
5
+ vi.mock("../syncUtils");
6
+ const coValueIdToLoad = "co_zKwG8NyfZ8GXqcjDHY4NS3SbU2m";
7
+ const createEmptyLoadMsg = (id) => ({
8
+ action: "load",
9
+ id,
10
+ header: false,
11
+ sessions: {},
12
+ });
13
+ const sessionsData = fixtures[coValueIdToLoad].sessionRecords;
14
+ const coValueHeader = fixtures[coValueIdToLoad].getContent({ after: 0 }).header;
15
+ const incomingContentMessage = fixtures[coValueIdToLoad].getContent({
16
+ after: 0,
17
+ });
18
+ describe("IDB sync manager", () => {
19
+ let syncManager;
20
+ let queue = {};
21
+ const IDBClient = vi.fn();
22
+ IDBClient.prototype.getCoValue = vi.fn();
23
+ IDBClient.prototype.getCoValueSessions = vi.fn();
24
+ IDBClient.prototype.addSessionUpdate = vi.fn();
25
+ IDBClient.prototype.addTransaction = vi.fn();
26
+ beforeEach(async () => {
27
+ const idbClient = new IDBClient();
28
+ syncManager = new SyncManager(idbClient, queue);
29
+ syncManager.sendStateMessage = vi.fn();
30
+ // No dependencies found
31
+ vi.mocked(getDependedOnCoValues).mockReturnValue([]);
32
+ });
33
+ afterEach(() => {
34
+ vi.clearAllMocks();
35
+ });
36
+ test("Incoming known messages are not processed", async () => {
37
+ await syncManager.handleSyncMessage({ action: "known" });
38
+ expect(syncManager.sendStateMessage).not.toBeCalled();
39
+ });
40
+ describe("Handle load incoming message", () => {
41
+ test("sends empty known message for unknown coValue", async () => {
42
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
43
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce(undefined);
44
+ await syncManager.handleSyncMessage(loadMsg);
45
+ expect(syncManager.sendStateMessage).toBeCalledWith({
46
+ action: "known",
47
+ header: false,
48
+ id: coValueIdToLoad,
49
+ sessions: {},
50
+ });
51
+ });
52
+ test("Sends known and content message for known coValue with no sessions", async () => {
53
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
54
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
55
+ id: coValueIdToLoad,
56
+ header: coValueHeader,
57
+ rowID: 3,
58
+ });
59
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce([]);
60
+ await syncManager.handleSyncMessage(loadMsg);
61
+ expect(syncManager.sendStateMessage).toBeCalledTimes(2);
62
+ expect(syncManager.sendStateMessage).toBeCalledWith({
63
+ action: "known",
64
+ header: true,
65
+ id: coValueIdToLoad,
66
+ sessions: {},
67
+ });
68
+ expect(syncManager.sendStateMessage).toBeCalledWith({
69
+ action: "content",
70
+ header: expect.objectContaining({
71
+ type: expect.any(String),
72
+ ruleset: expect.any(Object),
73
+ }),
74
+ id: coValueIdToLoad,
75
+ new: {},
76
+ priority: 0,
77
+ });
78
+ });
79
+ test("Sends both known and content messages when we have new sessions info for the requested coValue ", async () => {
80
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
81
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
82
+ id: coValueIdToLoad,
83
+ header: coValueHeader,
84
+ rowID: 3,
85
+ });
86
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce(sessionsData);
87
+ const newTxData = {
88
+ newTransactions: [
89
+ {
90
+ privacy: "trusting",
91
+ madeAt: 1732368535089,
92
+ changes: "",
93
+ },
94
+ ],
95
+ after: 0,
96
+ lastSignature: "signature_z111",
97
+ };
98
+ // mock content data combined with session updates
99
+ syncManager.handleSessionUpdate = vi.fn(async ({ sessionRow, newContentMessages }) => {
100
+ newContentMessages[0].new[sessionRow.sessionID] = newTxData;
101
+ });
102
+ await syncManager.handleSyncMessage(loadMsg);
103
+ expect(syncManager.sendStateMessage).toBeCalledTimes(2);
104
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
105
+ action: "known",
106
+ header: true,
107
+ id: coValueIdToLoad,
108
+ sessions: sessionsData.reduce((acc, sessionRow) => ({
109
+ ...acc,
110
+ [sessionRow.sessionID]: sessionRow.lastIdx,
111
+ }), {}),
112
+ });
113
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
114
+ action: "content",
115
+ header: coValueHeader,
116
+ id: coValueIdToLoad,
117
+ new: sessionsData.reduce((acc, sessionRow) => ({
118
+ ...acc,
119
+ [sessionRow.sessionID]: {
120
+ after: expect.any(Number),
121
+ lastSignature: expect.any(String),
122
+ newTransactions: expect.any(Array),
123
+ },
124
+ }), {}),
125
+ priority: 0,
126
+ });
127
+ });
128
+ test("Sends messages for unique coValue dependencies only, leaving out circular dependencies", async () => {
129
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
130
+ const dependency1 = "co_zMKhQJs5rAeGjta3JX2qEdBS6hS";
131
+ const dependency2 = "co_zP51HdyAVCuRY9ptq5iu8DhMyAb";
132
+ const dependency3 = "co_zGyBniuJmKkcirCKYrccWpjQEFY";
133
+ const dependenciesTreeWithLoop = {
134
+ [coValueIdToLoad]: [dependency1, dependency2],
135
+ [dependency1]: [],
136
+ [dependency2]: [coValueIdToLoad, dependency3],
137
+ [dependency3]: [dependency1],
138
+ };
139
+ IDBClient.prototype.getCoValue.mockImplementation((coValueId) => ({
140
+ id: coValueId,
141
+ header: coValueHeader,
142
+ rowID: 3,
143
+ }));
144
+ IDBClient.prototype.getCoValueSessions.mockResolvedValue([]);
145
+ // Fetch dependencies of the current dependency for the future recursion iterations
146
+ vi.mocked(getDependedOnCoValues).mockImplementation(({ coValueRow }) => dependenciesTreeWithLoop[coValueRow.id] || []);
147
+ await syncManager.handleSyncMessage(loadMsg);
148
+ // We send out pairs (known + content) messages only FOUR times - as many as the coValues number
149
+ // and less than amount of interconnected dependencies to loop through in dependenciesTreeWithLoop
150
+ expect(syncManager.sendStateMessage).toBeCalledTimes(4 * 2);
151
+ const knownExpected = {
152
+ action: "known",
153
+ header: true,
154
+ sessions: {},
155
+ };
156
+ const contentExpected = {
157
+ action: "content",
158
+ header: expect.any(Object),
159
+ new: {},
160
+ priority: 0,
161
+ };
162
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
163
+ ...knownExpected,
164
+ id: dependency3,
165
+ asDependencyOf: coValueIdToLoad,
166
+ });
167
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
168
+ ...contentExpected,
169
+ id: dependency3,
170
+ });
171
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(3, {
172
+ ...knownExpected,
173
+ id: dependency2,
174
+ asDependencyOf: coValueIdToLoad,
175
+ });
176
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(4, {
177
+ ...contentExpected,
178
+ id: dependency2,
179
+ });
180
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(5, {
181
+ ...knownExpected,
182
+ id: dependency1,
183
+ asDependencyOf: coValueIdToLoad,
184
+ });
185
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(6, {
186
+ ...contentExpected,
187
+ id: dependency1,
188
+ });
189
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(7, {
190
+ ...knownExpected,
191
+ id: coValueIdToLoad,
192
+ });
193
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(8, {
194
+ ...contentExpected,
195
+ id: coValueIdToLoad,
196
+ });
197
+ });
198
+ });
199
+ describe("Handle content incoming message", () => {
200
+ test("Sends correction message for unknown coValue", async () => {
201
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce(undefined);
202
+ await syncManager.handleSyncMessage({
203
+ ...incomingContentMessage,
204
+ header: undefined,
205
+ });
206
+ expect(syncManager.sendStateMessage).toBeCalledWith({
207
+ action: "known",
208
+ header: false,
209
+ id: coValueIdToLoad,
210
+ isCorrection: true,
211
+ sessions: {},
212
+ });
213
+ });
214
+ test("Saves new transaction without sending message when IDB has fewer transactions", async () => {
215
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
216
+ id: coValueIdToLoad,
217
+ header: coValueHeader,
218
+ rowID: 3,
219
+ });
220
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce([]);
221
+ const msg = {
222
+ ...incomingContentMessage,
223
+ header: undefined,
224
+ };
225
+ await syncManager.handleSyncMessage(msg);
226
+ const incomingTxCount = Object.keys(msg.new).reduce((acc, sessionID) => acc + msg.new[sessionID].newTransactions.length, 0);
227
+ expect(IDBClient.prototype.addTransaction).toBeCalledTimes(incomingTxCount);
228
+ expect(syncManager.sendStateMessage).not.toBeCalled();
229
+ });
230
+ test("Sends correction message when peer sends a message far ahead of our state due to invalid assumption", async () => {
231
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
232
+ id: coValueIdToLoad,
233
+ header: coValueHeader,
234
+ rowID: 3,
235
+ });
236
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce(sessionsData);
237
+ const farAheadContentMessage = fixtures[coValueIdToLoad].getContent({
238
+ after: 10000,
239
+ });
240
+ await syncManager.handleSyncMessage(farAheadContentMessage);
241
+ expect(syncManager.sendStateMessage).toBeCalledWith({
242
+ action: "known",
243
+ header: true,
244
+ id: coValueIdToLoad,
245
+ isCorrection: true,
246
+ sessions: expect.any(Object),
247
+ });
248
+ });
249
+ });
250
+ });
251
+ //# sourceMappingURL=syncManager.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncManager.test.js","sourceRoot":"","sources":["../../src/tests/syncManager.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "cojson-storage-indexeddb",
3
- "version": "0.8.32",
3
+ "version": "0.8.35",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "types": "src/index.ts",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "cojson": "0.8.32"
9
+ "cojson": "0.8.35"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@vitest/browser": "^0.34.1",
@@ -0,0 +1,237 @@
1
+ import { CojsonInternalTypes, SessionID } from "cojson";
2
+ import { SyncPromise } from "./syncPromises";
3
+ import RawCoID = CojsonInternalTypes.RawCoID;
4
+ import Transaction = CojsonInternalTypes.Transaction;
5
+ import Signature = CojsonInternalTypes.Signature;
6
+
7
+ export type CoValueRow = {
8
+ id: CojsonInternalTypes.RawCoID;
9
+ header: CojsonInternalTypes.CoValueHeader;
10
+ };
11
+
12
+ export type StoredCoValueRow = CoValueRow & { rowID: number };
13
+
14
+ export type TransactionRow = {
15
+ ses: number;
16
+ idx: number;
17
+ tx: CojsonInternalTypes.Transaction;
18
+ };
19
+
20
+ export type SignatureAfterRow = {
21
+ ses: number;
22
+ idx: number;
23
+ signature: CojsonInternalTypes.Signature;
24
+ };
25
+
26
+ export type SessionRow = {
27
+ coValue: number;
28
+ sessionID: SessionID;
29
+ lastIdx: number;
30
+ lastSignature: CojsonInternalTypes.Signature;
31
+ bytesSinceLastSignature?: number;
32
+ };
33
+
34
+ export type StoredSessionRow = SessionRow & { rowID: number };
35
+
36
+ export class IDBClient {
37
+ private db;
38
+
39
+ currentTx:
40
+ | {
41
+ id: number;
42
+ tx: IDBTransaction;
43
+ stores: {
44
+ coValues: IDBObjectStore;
45
+ sessions: IDBObjectStore;
46
+ transactions: IDBObjectStore;
47
+ signatureAfter: IDBObjectStore;
48
+ };
49
+ startedAt: number;
50
+ pendingRequests: ((txEntry: {
51
+ stores: {
52
+ coValues: IDBObjectStore;
53
+ sessions: IDBObjectStore;
54
+ transactions: IDBObjectStore;
55
+ signatureAfter: IDBObjectStore;
56
+ };
57
+ }) => void)[];
58
+ }
59
+ | undefined;
60
+
61
+ currentTxID = 0;
62
+
63
+ constructor(db: IDBDatabase) {
64
+ this.db = db;
65
+ }
66
+
67
+ makeRequest<T>(
68
+ handler: (stores: {
69
+ coValues: IDBObjectStore;
70
+ sessions: IDBObjectStore;
71
+ transactions: IDBObjectStore;
72
+ signatureAfter: IDBObjectStore;
73
+ }) => IDBRequest,
74
+ ): SyncPromise<T> {
75
+ return new SyncPromise((resolve, reject) => {
76
+ let txEntry = this.currentTx;
77
+
78
+ const requestEntry = ({
79
+ stores,
80
+ }: {
81
+ stores: {
82
+ coValues: IDBObjectStore;
83
+ sessions: IDBObjectStore;
84
+ transactions: IDBObjectStore;
85
+ signatureAfter: IDBObjectStore;
86
+ };
87
+ }) => {
88
+ const request = handler(stores);
89
+ request.onerror = () => {
90
+ console.error("Error in request", request.error);
91
+ this.currentTx = undefined;
92
+ reject(request.error);
93
+ };
94
+ request.onsuccess = () => {
95
+ const value = request.result as T;
96
+ resolve(value);
97
+
98
+ const next = txEntry!.pendingRequests.shift();
99
+
100
+ if (next) {
101
+ next({ stores });
102
+ } else {
103
+ if (this.currentTx === txEntry) {
104
+ this.currentTx = undefined;
105
+ }
106
+ }
107
+ };
108
+ };
109
+
110
+ // Transaction batching
111
+ if (!txEntry || performance.now() - txEntry.startedAt > 20) {
112
+ const tx = this.db.transaction(
113
+ ["coValues", "sessions", "transactions", "signatureAfter"],
114
+ "readwrite",
115
+ );
116
+ txEntry = {
117
+ id: this.currentTxID++,
118
+ tx,
119
+ stores: {
120
+ coValues: tx.objectStore("coValues"),
121
+ sessions: tx.objectStore("sessions"),
122
+ transactions: tx.objectStore("transactions"),
123
+ signatureAfter: tx.objectStore("signatureAfter"),
124
+ },
125
+ startedAt: performance.now(),
126
+ pendingRequests: [],
127
+ };
128
+
129
+ this.currentTx = txEntry;
130
+
131
+ requestEntry(txEntry);
132
+ } else {
133
+ txEntry.pendingRequests.push(requestEntry);
134
+ }
135
+ });
136
+ }
137
+
138
+ async getCoValue(coValueId: RawCoID): Promise<StoredCoValueRow | undefined> {
139
+ return this.makeRequest<StoredCoValueRow | undefined>(({ coValues }) =>
140
+ coValues.index("coValuesById").get(coValueId),
141
+ );
142
+ }
143
+
144
+ async getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]> {
145
+ return this.makeRequest<StoredSessionRow[]>(({ sessions }) =>
146
+ sessions.index("sessionsByCoValue").getAll(coValueRowId),
147
+ );
148
+ }
149
+
150
+ async getNewTransactionInSession(
151
+ sessionRow: StoredSessionRow,
152
+ firstNewTxIdx: number,
153
+ ): Promise<TransactionRow[]> {
154
+ return this.makeRequest<TransactionRow[]>(({ transactions }) =>
155
+ transactions.getAll(
156
+ IDBKeyRange.bound(
157
+ [sessionRow.rowID, firstNewTxIdx],
158
+ [sessionRow.rowID, Infinity],
159
+ ),
160
+ ),
161
+ );
162
+ }
163
+
164
+ async getSignatures(
165
+ sessionRow: StoredSessionRow,
166
+ firstNewTxIdx: number,
167
+ ): Promise<SignatureAfterRow[]> {
168
+ return this.makeRequest<SignatureAfterRow[]>(
169
+ ({ signatureAfter }: { signatureAfter: IDBObjectStore }) =>
170
+ signatureAfter.getAll(
171
+ IDBKeyRange.bound(
172
+ [sessionRow.rowID, firstNewTxIdx],
173
+ [sessionRow.rowID, Infinity],
174
+ ),
175
+ ),
176
+ );
177
+ }
178
+
179
+ async addCoValue(
180
+ msg: CojsonInternalTypes.NewContentMessage,
181
+ ): Promise<number> {
182
+ if (!msg.header) {
183
+ throw new Error("Header is required, coId: " + msg.id);
184
+ }
185
+
186
+ return (await this.makeRequest<IDBValidKey>(({ coValues }) =>
187
+ coValues.put({
188
+ id: msg.id,
189
+ header: msg.header!,
190
+ } satisfies CoValueRow),
191
+ )) as number;
192
+ }
193
+
194
+ async addSessionUpdate(
195
+ sessionRow: StoredSessionRow | undefined,
196
+ sessionUpdate: SessionRow,
197
+ ): Promise<number> {
198
+ return this.makeRequest<number>(({ sessions }) =>
199
+ sessions.put(
200
+ sessionRow?.rowID
201
+ ? {
202
+ rowID: sessionRow.rowID,
203
+ ...sessionUpdate,
204
+ }
205
+ : sessionUpdate,
206
+ ),
207
+ );
208
+ }
209
+
210
+ addTransaction(
211
+ sessionRowID: number,
212
+ idx: number,
213
+ newTransaction: Transaction,
214
+ ) {
215
+ return this.makeRequest(({ transactions }) =>
216
+ transactions.add({
217
+ ses: sessionRowID,
218
+ idx,
219
+ tx: newTransaction,
220
+ } satisfies TransactionRow),
221
+ );
222
+ }
223
+
224
+ addSignatureAfter({
225
+ sessionRowID,
226
+ idx,
227
+ signature,
228
+ }: { sessionRowID: number; idx: number; signature: Signature }) {
229
+ return this.makeRequest(({ signatureAfter }) =>
230
+ signatureAfter.put({
231
+ ses: sessionRowID,
232
+ idx,
233
+ signature,
234
+ } satisfies SignatureAfterRow),
235
+ );
236
+ }
237
+ }
package/src/idbNode.ts ADDED
@@ -0,0 +1,123 @@
1
+ import {
2
+ IncomingSyncStream,
3
+ OutgoingSyncQueue,
4
+ Peer,
5
+ cojsonInternals,
6
+ } from "cojson";
7
+ import { IDBClient } from "./idbClient";
8
+ import { SyncManager } from "./syncManager";
9
+
10
+ export class IDBNode {
11
+ private dbClient: IDBClient;
12
+ private syncManager: SyncManager;
13
+
14
+ constructor(
15
+ db: IDBDatabase,
16
+ fromLocalNode: IncomingSyncStream,
17
+ toLocalNode: OutgoingSyncQueue,
18
+ ) {
19
+ this.dbClient = new IDBClient(db);
20
+ this.syncManager = new SyncManager(this.dbClient, toLocalNode);
21
+
22
+ const processMessages = async () => {
23
+ for await (const msg of fromLocalNode) {
24
+ try {
25
+ if (msg === "Disconnected" || msg === "PingTimeout") {
26
+ throw new Error("Unexpected Disconnected message");
27
+ }
28
+ await this.syncManager.handleSyncMessage(msg);
29
+ } catch (e) {
30
+ console.error(
31
+ new Error(
32
+ `Error reading from localNode, handling msg\n\n${JSON.stringify(
33
+ msg,
34
+ (k, v) =>
35
+ k === "changes" || k === "encryptedChanges"
36
+ ? v.slice(0, 20) + "..."
37
+ : v,
38
+ )}`,
39
+ { cause: e },
40
+ ),
41
+ );
42
+ }
43
+ }
44
+ };
45
+
46
+ processMessages().catch((e) =>
47
+ console.error("Error in processMessages in IndexedDB", e),
48
+ );
49
+ }
50
+
51
+ static async asPeer(
52
+ {
53
+ trace,
54
+ localNodeName = "local",
55
+ }: { trace?: boolean; localNodeName?: string } | undefined = {
56
+ localNodeName: "local",
57
+ },
58
+ ): Promise<Peer> {
59
+ const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
60
+ localNodeName,
61
+ "indexedDB",
62
+ {
63
+ peer1role: "client",
64
+ peer2role: "storage",
65
+ trace,
66
+ crashOnClose: true,
67
+ },
68
+ );
69
+
70
+ await IDBNode.open(localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
71
+
72
+ return { ...storageAsPeer, priority: 100 };
73
+ }
74
+
75
+ static async open(
76
+ fromLocalNode: IncomingSyncStream,
77
+ toLocalNode: OutgoingSyncQueue,
78
+ ) {
79
+ const dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
80
+ const request = indexedDB.open("jazz-storage", 4);
81
+ request.onerror = () => {
82
+ reject(request.error);
83
+ };
84
+ request.onsuccess = () => {
85
+ resolve(request.result);
86
+ };
87
+ request.onupgradeneeded = async (ev) => {
88
+ const db = request.result;
89
+ if (ev.oldVersion === 0) {
90
+ const coValues = db.createObjectStore("coValues", {
91
+ autoIncrement: true,
92
+ keyPath: "rowID",
93
+ });
94
+
95
+ coValues.createIndex("coValuesById", "id", {
96
+ unique: true,
97
+ });
98
+
99
+ const sessions = db.createObjectStore("sessions", {
100
+ autoIncrement: true,
101
+ keyPath: "rowID",
102
+ });
103
+
104
+ sessions.createIndex("sessionsByCoValue", "coValue");
105
+ sessions.createIndex("uniqueSessions", ["coValue", "sessionID"], {
106
+ unique: true,
107
+ });
108
+
109
+ db.createObjectStore("transactions", {
110
+ keyPath: ["ses", "idx"],
111
+ });
112
+ }
113
+ if (ev.oldVersion <= 1) {
114
+ db.createObjectStore("signatureAfter", {
115
+ keyPath: ["ses", "idx"],
116
+ });
117
+ }
118
+ };
119
+ });
120
+
121
+ return new IDBNode(await dbPromise, fromLocalNode, toLocalNode);
122
+ }
123
+ }