cojson 0.1.4 → 0.1.6
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/dist/account.d.ts +2 -1
- package/dist/account.js.map +1 -1
- package/dist/contentTypes/coList.d.ts +66 -4
- package/dist/contentTypes/coList.js +235 -1
- package/dist/contentTypes/coList.js.map +1 -1
- package/dist/contentTypes/coMap.d.ts +1 -0
- package/dist/contentTypes/coMap.js +3 -0
- package/dist/contentTypes/coMap.js.map +1 -1
- package/dist/group.d.ts +4 -2
- package/dist/group.js +13 -0
- package/dist/group.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/streamUtils.js +28 -43
- package/dist/streamUtils.js.map +1 -1
- package/dist/sync.js +18 -5
- package/dist/sync.js.map +1 -1
- package/package.json +2 -2
- package/src/account.ts +2 -1
- package/src/contentType.test.ts +114 -5
- package/src/contentTypes/coList.ts +352 -6
- package/src/contentTypes/coMap.ts +4 -0
- package/src/group.ts +22 -6
- package/src/index.ts +2 -0
- package/src/node.ts +3 -4
- package/src/streamUtils.ts +44 -49
- package/src/sync.ts +26 -6
|
@@ -1,19 +1,262 @@
|
|
|
1
|
-
import { JsonObject, JsonValue } from
|
|
2
|
-
import { CoID } from
|
|
3
|
-
import { CoValue } from
|
|
1
|
+
import { JsonObject, JsonValue } from "../jsonValue.js";
|
|
2
|
+
import { CoID } from "../contentType.js";
|
|
3
|
+
import { CoValue, accountOrAgentIDfromSessionID } from "../coValue.js";
|
|
4
|
+
import { SessionID, TransactionID } from "../ids.js";
|
|
5
|
+
import { AccountID } from "../index.js";
|
|
6
|
+
import { isAccountID } from "../account.js";
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
type OpID = TransactionID & { changeIdx: number };
|
|
9
|
+
|
|
10
|
+
type InsertionOpPayload<T extends JsonValue> =
|
|
11
|
+
| {
|
|
12
|
+
op: "pre";
|
|
13
|
+
value: T;
|
|
14
|
+
before: OpID | "end";
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
op: "app";
|
|
18
|
+
value: T;
|
|
19
|
+
after: OpID | "start";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type DeletionOpPayload = {
|
|
23
|
+
op: "del";
|
|
24
|
+
insertion: OpID;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ListOpPayload<T extends JsonValue> =
|
|
28
|
+
| InsertionOpPayload<T>
|
|
29
|
+
| DeletionOpPayload;
|
|
30
|
+
|
|
31
|
+
type InsertionEntry<T extends JsonValue> = {
|
|
32
|
+
madeAt: number;
|
|
33
|
+
predecessors: OpID[];
|
|
34
|
+
successors: OpID[];
|
|
35
|
+
} & InsertionOpPayload<T>;
|
|
36
|
+
|
|
37
|
+
type DeletionEntry = {
|
|
38
|
+
madeAt: number;
|
|
39
|
+
deletionID: OpID;
|
|
40
|
+
} & DeletionOpPayload;
|
|
41
|
+
|
|
42
|
+
export class CoList<
|
|
43
|
+
T extends JsonValue,
|
|
44
|
+
Meta extends JsonObject | null = null
|
|
45
|
+
> {
|
|
6
46
|
id: CoID<CoList<T, Meta>>;
|
|
7
47
|
type = "colist" as const;
|
|
8
48
|
coValue: CoValue;
|
|
49
|
+
afterStart: OpID[];
|
|
50
|
+
beforeEnd: OpID[];
|
|
51
|
+
insertions: {
|
|
52
|
+
[sessionID: SessionID]: {
|
|
53
|
+
[txIdx: number]: {
|
|
54
|
+
[changeIdx: number]: InsertionEntry<T>;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
deletionsByInsertion: {
|
|
59
|
+
[deletedSessionID: SessionID]: {
|
|
60
|
+
[deletedTxIdx: number]: {
|
|
61
|
+
[deletedChangeIdx: number]: DeletionEntry[];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
9
65
|
|
|
10
66
|
constructor(coValue: CoValue) {
|
|
11
67
|
this.id = coValue.id as CoID<CoList<T, Meta>>;
|
|
12
68
|
this.coValue = coValue;
|
|
69
|
+
this.afterStart = [];
|
|
70
|
+
this.beforeEnd = [];
|
|
71
|
+
this.insertions = {};
|
|
72
|
+
this.deletionsByInsertion = {};
|
|
73
|
+
|
|
74
|
+
this.fillOpsFromCoValue();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
get meta(): Meta {
|
|
79
|
+
return this.coValue.header.meta as Meta;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected fillOpsFromCoValue() {
|
|
83
|
+
this.insertions = {};
|
|
84
|
+
this.deletionsByInsertion = {};
|
|
85
|
+
this.afterStart = [];
|
|
86
|
+
this.beforeEnd = [];
|
|
87
|
+
|
|
88
|
+
for (const {
|
|
89
|
+
txID,
|
|
90
|
+
changes,
|
|
91
|
+
madeAt,
|
|
92
|
+
} of this.coValue.getValidSortedTransactions()) {
|
|
93
|
+
for (const [changeIdx, changeUntyped] of changes.entries()) {
|
|
94
|
+
const change = changeUntyped as ListOpPayload<T>;
|
|
95
|
+
|
|
96
|
+
if (change.op === "pre" || change.op === "app") {
|
|
97
|
+
let sessionEntry = this.insertions[txID.sessionID];
|
|
98
|
+
if (!sessionEntry) {
|
|
99
|
+
sessionEntry = {};
|
|
100
|
+
this.insertions[txID.sessionID] = sessionEntry;
|
|
101
|
+
}
|
|
102
|
+
let txEntry = sessionEntry[txID.txIndex];
|
|
103
|
+
if (!txEntry) {
|
|
104
|
+
txEntry = {};
|
|
105
|
+
sessionEntry[txID.txIndex] = txEntry;
|
|
106
|
+
}
|
|
107
|
+
txEntry[changeIdx] = {
|
|
108
|
+
madeAt,
|
|
109
|
+
predecessors: [],
|
|
110
|
+
successors: [],
|
|
111
|
+
...change,
|
|
112
|
+
};
|
|
113
|
+
if (change.op === "pre") {
|
|
114
|
+
if (change.before === "end") {
|
|
115
|
+
this.beforeEnd.push({
|
|
116
|
+
...txID,
|
|
117
|
+
changeIdx,
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
const beforeEntry =
|
|
121
|
+
this.insertions[change.before.sessionID]?.[
|
|
122
|
+
change.before.txIndex
|
|
123
|
+
]?.[change.before.changeIdx];
|
|
124
|
+
if (!beforeEntry) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Not yet implemented: insertion before missing op " +
|
|
127
|
+
change.before
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
beforeEntry.predecessors.splice(0, 0, {
|
|
131
|
+
...txID,
|
|
132
|
+
changeIdx,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
if (change.after === "start") {
|
|
137
|
+
this.afterStart.push({
|
|
138
|
+
...txID,
|
|
139
|
+
changeIdx,
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
const afterEntry =
|
|
143
|
+
this.insertions[change.after.sessionID]?.[
|
|
144
|
+
change.after.txIndex
|
|
145
|
+
]?.[change.after.changeIdx];
|
|
146
|
+
if (!afterEntry) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"Not yet implemented: insertion after missing op " +
|
|
149
|
+
change.after
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
afterEntry.successors.push({
|
|
153
|
+
...txID,
|
|
154
|
+
changeIdx,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else if (change.op === "del") {
|
|
159
|
+
let sessionEntry =
|
|
160
|
+
this.deletionsByInsertion[change.insertion.sessionID];
|
|
161
|
+
if (!sessionEntry) {
|
|
162
|
+
sessionEntry = {};
|
|
163
|
+
this.deletionsByInsertion[change.insertion.sessionID] =
|
|
164
|
+
sessionEntry;
|
|
165
|
+
}
|
|
166
|
+
let txEntry = sessionEntry[change.insertion.txIndex];
|
|
167
|
+
if (!txEntry) {
|
|
168
|
+
txEntry = {};
|
|
169
|
+
sessionEntry[change.insertion.txIndex] = txEntry;
|
|
170
|
+
}
|
|
171
|
+
let changeEntry = txEntry[change.insertion.changeIdx];
|
|
172
|
+
if (!changeEntry) {
|
|
173
|
+
changeEntry = [];
|
|
174
|
+
txEntry[change.insertion.changeIdx] = changeEntry;
|
|
175
|
+
}
|
|
176
|
+
changeEntry.push({
|
|
177
|
+
madeAt,
|
|
178
|
+
deletionID: {
|
|
179
|
+
...txID,
|
|
180
|
+
changeIdx,
|
|
181
|
+
},
|
|
182
|
+
...change,
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"Unknown list operation " +
|
|
187
|
+
(change as { op: unknown }).op
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
entries(): { value: T; madeAt: number; opID: OpID }[] {
|
|
195
|
+
const arr: { value: T; madeAt: number; opID: OpID }[] = [];
|
|
196
|
+
for (const opID of this.afterStart) {
|
|
197
|
+
this.fillArrayFromOpID(opID, arr);
|
|
198
|
+
}
|
|
199
|
+
for (const opID of this.beforeEnd) {
|
|
200
|
+
this.fillArrayFromOpID(opID, arr);
|
|
201
|
+
}
|
|
202
|
+
return arr;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private fillArrayFromOpID(
|
|
206
|
+
opID: OpID,
|
|
207
|
+
arr: { value: T; madeAt: number; opID: OpID }[]
|
|
208
|
+
) {
|
|
209
|
+
const entry =
|
|
210
|
+
this.insertions[opID.sessionID]?.[opID.txIndex]?.[opID.changeIdx];
|
|
211
|
+
if (!entry) {
|
|
212
|
+
throw new Error("Missing op " + opID);
|
|
213
|
+
}
|
|
214
|
+
for (const predecessor of entry.predecessors) {
|
|
215
|
+
this.fillArrayFromOpID(predecessor, arr);
|
|
216
|
+
}
|
|
217
|
+
const deleted =
|
|
218
|
+
(this.deletionsByInsertion[opID.sessionID]?.[opID.txIndex]?.[
|
|
219
|
+
opID.changeIdx
|
|
220
|
+
]?.length || 0) > 0;
|
|
221
|
+
if (!deleted) {
|
|
222
|
+
arr.push({
|
|
223
|
+
value: entry.value,
|
|
224
|
+
madeAt: entry.madeAt,
|
|
225
|
+
opID,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
for (const successor of entry.successors) {
|
|
229
|
+
this.fillArrayFromOpID(successor, arr);
|
|
230
|
+
}
|
|
13
231
|
}
|
|
14
232
|
|
|
15
|
-
|
|
16
|
-
|
|
233
|
+
getLastEditor(idx: number): AccountID | undefined {
|
|
234
|
+
const entry = this.entries()[idx];
|
|
235
|
+
if (!entry) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const accountID = accountOrAgentIDfromSessionID(entry.opID.sessionID);
|
|
239
|
+
if (isAccountID(accountID)) {
|
|
240
|
+
return accountID;
|
|
241
|
+
} else {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
toJSON(): T[] {
|
|
247
|
+
return this.asArray();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
asArray(): T[] {
|
|
251
|
+
return this.entries().map((entry) => entry.value);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
edit(
|
|
255
|
+
changer: (editable: WriteableCoList<T, Meta>) => void
|
|
256
|
+
): CoList<T, Meta> {
|
|
257
|
+
const editable = new WriteableCoList<T, Meta>(this.coValue);
|
|
258
|
+
changer(editable);
|
|
259
|
+
return new CoList(this.coValue);
|
|
17
260
|
}
|
|
18
261
|
|
|
19
262
|
subscribe(listener: (coMap: CoList<T, Meta>) => void): () => void {
|
|
@@ -22,3 +265,106 @@ export class CoList<T extends JsonValue, Meta extends JsonObject | null = null>
|
|
|
22
265
|
});
|
|
23
266
|
}
|
|
24
267
|
}
|
|
268
|
+
|
|
269
|
+
export class WriteableCoList<
|
|
270
|
+
T extends JsonValue,
|
|
271
|
+
Meta extends JsonObject | null = null
|
|
272
|
+
> extends CoList<T, Meta> {
|
|
273
|
+
append(
|
|
274
|
+
after: number,
|
|
275
|
+
value: T,
|
|
276
|
+
privacy: "private" | "trusting" = "private"
|
|
277
|
+
): void {
|
|
278
|
+
const entries = this.entries();
|
|
279
|
+
let opIDBefore;
|
|
280
|
+
if (entries.length > 0) {
|
|
281
|
+
const entryBefore = entries[after];
|
|
282
|
+
if (!entryBefore) {
|
|
283
|
+
throw new Error("Invalid index " + after);
|
|
284
|
+
}
|
|
285
|
+
opIDBefore = entryBefore.opID;
|
|
286
|
+
} else {
|
|
287
|
+
if (after !== 0) {
|
|
288
|
+
throw new Error("Invalid index " + after);
|
|
289
|
+
}
|
|
290
|
+
opIDBefore = "start";
|
|
291
|
+
}
|
|
292
|
+
this.coValue.makeTransaction(
|
|
293
|
+
[
|
|
294
|
+
{
|
|
295
|
+
op: "app",
|
|
296
|
+
value,
|
|
297
|
+
after: opIDBefore,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
privacy
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
this.fillOpsFromCoValue();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
push(value: T, privacy: "private" | "trusting" = "private"): void {
|
|
307
|
+
// TODO: optimize
|
|
308
|
+
const entries = this.entries();
|
|
309
|
+
this.append(entries.length > 0 ? entries.length - 1 : 0, value, privacy);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
prepend(
|
|
313
|
+
before: number,
|
|
314
|
+
value: T,
|
|
315
|
+
privacy: "private" | "trusting" = "private"
|
|
316
|
+
): void {
|
|
317
|
+
const entries = this.entries();
|
|
318
|
+
let opIDAfter;
|
|
319
|
+
if (entries.length > 0) {
|
|
320
|
+
const entryAfter = entries[before];
|
|
321
|
+
if (entryAfter) {
|
|
322
|
+
opIDAfter = entryAfter.opID;
|
|
323
|
+
} else {
|
|
324
|
+
if (before !== entries.length) {
|
|
325
|
+
throw new Error("Invalid index " + before);
|
|
326
|
+
}
|
|
327
|
+
opIDAfter = "end";
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
if (before !== 0) {
|
|
331
|
+
throw new Error("Invalid index " + before);
|
|
332
|
+
}
|
|
333
|
+
opIDAfter = "end";
|
|
334
|
+
}
|
|
335
|
+
this.coValue.makeTransaction(
|
|
336
|
+
[
|
|
337
|
+
{
|
|
338
|
+
op: "pre",
|
|
339
|
+
value,
|
|
340
|
+
before: opIDAfter,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
privacy
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
this.fillOpsFromCoValue();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
delete(
|
|
350
|
+
at: number,
|
|
351
|
+
privacy: "private" | "trusting" = "private"
|
|
352
|
+
): void {
|
|
353
|
+
const entries = this.entries();
|
|
354
|
+
const entry = entries[at];
|
|
355
|
+
if (!entry) {
|
|
356
|
+
throw new Error("Invalid index " + at);
|
|
357
|
+
}
|
|
358
|
+
this.coValue.makeTransaction(
|
|
359
|
+
[
|
|
360
|
+
{
|
|
361
|
+
op: "del",
|
|
362
|
+
insertion: entry.opID,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
privacy
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
this.fillOpsFromCoValue();
|
|
369
|
+
}
|
|
370
|
+
}
|
package/src/group.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./account.js";
|
|
25
25
|
import { Role } from "./permissions.js";
|
|
26
26
|
import { base58 } from "@scure/base";
|
|
27
|
+
import { CoList } from "./contentTypes/coList.js";
|
|
27
28
|
|
|
28
29
|
export type GroupContent = {
|
|
29
30
|
profile: CoID<Profile> | null;
|
|
@@ -186,10 +187,9 @@ export class Group {
|
|
|
186
187
|
this.rotateReadKey();
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
createMap<
|
|
190
|
-
M
|
|
191
|
-
|
|
192
|
-
>(meta?: Meta): CoMap<M, Meta> {
|
|
190
|
+
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
|
191
|
+
meta?: M["meta"]
|
|
192
|
+
): M {
|
|
193
193
|
return this.node
|
|
194
194
|
.createCoValue({
|
|
195
195
|
type: "comap",
|
|
@@ -200,7 +200,23 @@ export class Group {
|
|
|
200
200
|
meta: meta || null,
|
|
201
201
|
...createdNowUnique(),
|
|
202
202
|
})
|
|
203
|
-
.getCurrentContent() as
|
|
203
|
+
.getCurrentContent() as M;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
createList<L extends CoList<JsonValue, JsonObject | null>>(
|
|
207
|
+
meta?: L["meta"]
|
|
208
|
+
): L {
|
|
209
|
+
return this.node
|
|
210
|
+
.createCoValue({
|
|
211
|
+
type: "colist",
|
|
212
|
+
ruleset: {
|
|
213
|
+
type: "ownedByGroup",
|
|
214
|
+
group: this.groupMap.id,
|
|
215
|
+
},
|
|
216
|
+
meta: meta || null,
|
|
217
|
+
...createdNowUnique(),
|
|
218
|
+
})
|
|
219
|
+
.getCurrentContent() as L;
|
|
204
220
|
}
|
|
205
221
|
|
|
206
222
|
testWithDifferentAccount(
|
|
@@ -230,4 +246,4 @@ export function secretSeedFromInviteSecret(inviteSecret: InviteSecret) {
|
|
|
230
246
|
}
|
|
231
247
|
|
|
232
248
|
return base58.decode(inviteSecret.slice("inviteSecret_z".length));
|
|
233
|
-
}
|
|
249
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
AccountID,
|
|
26
26
|
AccountContent,
|
|
27
27
|
ProfileContent,
|
|
28
|
+
ProfileMeta,
|
|
28
29
|
Profile,
|
|
29
30
|
} from "./account.js";
|
|
30
31
|
import type { InviteSecret } from "./group.js";
|
|
@@ -70,6 +71,7 @@ export type {
|
|
|
70
71
|
AccountContent,
|
|
71
72
|
Profile,
|
|
72
73
|
ProfileContent,
|
|
74
|
+
ProfileMeta,
|
|
73
75
|
InviteSecret
|
|
74
76
|
};
|
|
75
77
|
|
package/src/node.ts
CHANGED
|
@@ -31,8 +31,7 @@ import {
|
|
|
31
31
|
AccountID,
|
|
32
32
|
Profile,
|
|
33
33
|
AccountContent,
|
|
34
|
-
|
|
35
|
-
ProfileMeta,
|
|
34
|
+
AccountMap,
|
|
36
35
|
} from "./account.js";
|
|
37
36
|
import { CoMap } from "./index.js";
|
|
38
37
|
|
|
@@ -139,7 +138,7 @@ export class LocalNode {
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
async loadProfile(id: AccountID): Promise<Profile> {
|
|
142
|
-
const account = await this.load<
|
|
141
|
+
const account = await this.load<AccountMap>(id);
|
|
143
142
|
const profileID = account.get("profile");
|
|
144
143
|
|
|
145
144
|
if (!profileID) {
|
|
@@ -307,7 +306,7 @@ export class LocalNode {
|
|
|
307
306
|
account.node
|
|
308
307
|
);
|
|
309
308
|
|
|
310
|
-
const profile = accountAsGroup.createMap<
|
|
309
|
+
const profile = accountAsGroup.createMap<Profile>({
|
|
311
310
|
type: "profile",
|
|
312
311
|
});
|
|
313
312
|
|
package/src/streamUtils.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ReadableStream,
|
|
3
|
+
TransformStream,
|
|
4
|
+
WritableStream,
|
|
5
|
+
} from "isomorphic-streams";
|
|
2
6
|
import { Peer, PeerID, SyncMessage } from "./sync.js";
|
|
3
7
|
|
|
4
|
-
|
|
5
8
|
export function connectedPeers(
|
|
6
9
|
peer1id: PeerID,
|
|
7
10
|
peer2id: PeerID,
|
|
8
11
|
{
|
|
9
|
-
trace = false,
|
|
12
|
+
trace = false,
|
|
13
|
+
peer1role = "peer",
|
|
14
|
+
peer2role = "peer",
|
|
10
15
|
}: {
|
|
11
16
|
trace?: boolean;
|
|
12
17
|
peer1role?: Peer["role"];
|
|
@@ -24,9 +29,13 @@ export function connectedPeers(
|
|
|
24
29
|
new TransformStream({
|
|
25
30
|
transform(
|
|
26
31
|
chunk: SyncMessage,
|
|
27
|
-
controller: { enqueue: (msg: SyncMessage) => void
|
|
32
|
+
controller: { enqueue: (msg: SyncMessage) => void }
|
|
28
33
|
) {
|
|
29
|
-
trace &&
|
|
34
|
+
trace &&
|
|
35
|
+
console.debug(
|
|
36
|
+
`${peer2id} -> ${peer1id}`,
|
|
37
|
+
JSON.stringify(chunk, null, 2)
|
|
38
|
+
);
|
|
30
39
|
controller.enqueue(chunk);
|
|
31
40
|
},
|
|
32
41
|
})
|
|
@@ -38,9 +47,13 @@ export function connectedPeers(
|
|
|
38
47
|
new TransformStream({
|
|
39
48
|
transform(
|
|
40
49
|
chunk: SyncMessage,
|
|
41
|
-
controller: { enqueue: (msg: SyncMessage) => void
|
|
50
|
+
controller: { enqueue: (msg: SyncMessage) => void }
|
|
42
51
|
) {
|
|
43
|
-
trace &&
|
|
52
|
+
trace &&
|
|
53
|
+
console.debug(
|
|
54
|
+
`${peer1id} -> ${peer2id}`,
|
|
55
|
+
JSON.stringify(chunk, null, 2)
|
|
56
|
+
);
|
|
44
57
|
controller.enqueue(chunk);
|
|
45
58
|
},
|
|
46
59
|
})
|
|
@@ -65,39 +78,22 @@ export function connectedPeers(
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
|
|
81
|
+
let readerClosed = false;
|
|
82
|
+
|
|
83
|
+
let resolveEnqueue: (enqueue: (item: T) => void) => void;
|
|
84
|
+
const enqueuePromise = new Promise<(item: T) => void>((resolve) => {
|
|
85
|
+
resolveEnqueue = resolve;
|
|
72
86
|
});
|
|
73
87
|
|
|
74
|
-
let
|
|
75
|
-
|
|
88
|
+
let resolveClose: (close: () => void) => void;
|
|
89
|
+
const closePromise = new Promise<() => void>((resolve) => {
|
|
90
|
+
resolveClose = resolve;
|
|
91
|
+
});
|
|
76
92
|
|
|
77
93
|
const readable = new ReadableStream<T>({
|
|
78
|
-
async
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (writerClosed) {
|
|
82
|
-
controller.close();
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
retriesLeft--;
|
|
86
|
-
if (queue.length > 0) {
|
|
87
|
-
controller.enqueue(queue.shift()!);
|
|
88
|
-
if (queue.length === 0) {
|
|
89
|
-
nextItemReady = new Promise((resolve) => {
|
|
90
|
-
resolveNextItemReady = resolve;
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
return;
|
|
94
|
-
} else {
|
|
95
|
-
await nextItemReady;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
throw new Error(
|
|
99
|
-
"Should only use one retry to get next item in queue."
|
|
100
|
-
);
|
|
94
|
+
async start(controller) {
|
|
95
|
+
resolveEnqueue(controller.enqueue.bind(controller));
|
|
96
|
+
resolveClose(controller.close.bind(controller));
|
|
101
97
|
},
|
|
102
98
|
|
|
103
99
|
cancel(_reason) {
|
|
@@ -107,22 +103,21 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
107
103
|
});
|
|
108
104
|
|
|
109
105
|
const writable = new WritableStream<T>({
|
|
110
|
-
write(chunk) {
|
|
106
|
+
async write(chunk) {
|
|
107
|
+
const enqueue = await enqueuePromise;
|
|
111
108
|
if (readerClosed) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
setTimeout(() => resolveNextItemReady());
|
|
109
|
+
throw new Error("Reader closed");
|
|
110
|
+
} else {
|
|
111
|
+
// make sure write resolves before corresponding read
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
enqueue(chunk);
|
|
114
|
+
})
|
|
119
115
|
}
|
|
120
116
|
},
|
|
121
|
-
abort(
|
|
122
|
-
console.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return Promise.resolve();
|
|
117
|
+
async abort(reason) {
|
|
118
|
+
console.debug("Manually closing writer", reason);
|
|
119
|
+
const close = await closePromise;
|
|
120
|
+
close();
|
|
126
121
|
},
|
|
127
122
|
});
|
|
128
123
|
|
package/src/sync.ts
CHANGED
|
@@ -268,6 +268,7 @@ export class SyncManager {
|
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
console.log("DONE!!!");
|
|
271
272
|
} catch (e) {
|
|
272
273
|
console.error(`Error reading from peer ${peer.id}`, e);
|
|
273
274
|
}
|
|
@@ -280,13 +281,32 @@ export class SyncManager {
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
trySendToPeer(peer: PeerState, msg: SyncMessage) {
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
return new Promise<void>((resolve) => {
|
|
285
|
+
const timeout = setTimeout(() => {
|
|
286
|
+
console.error(
|
|
287
|
+
new Error(
|
|
288
|
+
`Writing to peer ${peer.id} took >1s - this should never happen as write should resolve quickly or error`
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
resolve();
|
|
292
|
+
}, 1000);
|
|
293
|
+
peer.outgoing
|
|
294
|
+
.write(msg)
|
|
295
|
+
.then(() => {
|
|
296
|
+
clearTimeout(timeout);
|
|
297
|
+
resolve();
|
|
287
298
|
})
|
|
288
|
-
|
|
289
|
-
|
|
299
|
+
.catch((e) => {
|
|
300
|
+
console.error(
|
|
301
|
+
new Error(
|
|
302
|
+
`Error writing to peer ${peer.id}, disconnecting`,
|
|
303
|
+
{
|
|
304
|
+
cause: e,
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
delete this.peers[peer.id];
|
|
309
|
+
});
|
|
290
310
|
});
|
|
291
311
|
}
|
|
292
312
|
|