@comapeo/core 2.0.1 → 2.1.0
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/blob-store/index.d.ts +5 -8
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core-manager/index.d.ts +10 -0
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/datastore/index.d.ts +5 -4
- package/dist/datastore/index.d.ts.map +1 -1
- package/dist/generated/extensions.d.ts +31 -0
- package/dist/generated/extensions.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/drizzle-helpers.d.ts +6 -0
- package/dist/lib/drizzle-helpers.d.ts.map +1 -0
- package/dist/lib/error.d.ts +37 -0
- package/dist/lib/error.d.ts.map +1 -0
- package/dist/lib/get-own.d.ts +9 -0
- package/dist/lib/get-own.d.ts.map +1 -0
- package/dist/lib/is-hostname-ip-address.d.ts +17 -0
- package/dist/lib/is-hostname-ip-address.d.ts.map +1 -0
- package/dist/lib/ws-core-replicator.d.ts +11 -0
- package/dist/lib/ws-core-replicator.d.ts.map +1 -0
- package/dist/mapeo-manager.d.ts +18 -22
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +448 -15
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +40 -1
- package/dist/member-api.d.ts.map +1 -1
- package/dist/schema/client.d.ts +17 -5
- package/dist/schema/client.d.ts.map +1 -1
- package/dist/schema/project.d.ts +210 -0
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts +28 -2
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/drizzle/client/0001_chubby_cargill.sql +12 -0
- package/drizzle/client/meta/0001_snapshot.json +208 -0
- package/drizzle/client/meta/_journal.json +7 -0
- package/drizzle/project/0001_medical_wendell_rand.sql +22 -0
- package/drizzle/project/meta/0001_snapshot.json +1267 -0
- package/drizzle/project/meta/_journal.json +7 -0
- package/package.json +9 -5
- package/src/blob-store/index.js +3 -2
- package/src/constants.js +4 -1
- package/src/core-manager/index.js +58 -2
- package/src/datastore/README.md +1 -2
- package/src/datastore/index.js +4 -5
- package/src/fastify-plugins/blobs.js +1 -0
- package/src/generated/extensions.d.ts +31 -0
- package/src/generated/extensions.js +150 -0
- package/src/generated/extensions.ts +181 -0
- package/src/index.js +10 -0
- package/src/invite-api.js +1 -1
- package/src/lib/drizzle-helpers.js +79 -0
- package/src/lib/error.js +47 -0
- package/src/lib/get-own.js +10 -0
- package/src/lib/is-hostname-ip-address.js +26 -0
- package/src/lib/ws-core-replicator.js +47 -0
- package/src/mapeo-manager.js +71 -43
- package/src/mapeo-project.js +153 -43
- package/src/member-api.js +253 -2
- package/src/schema/client.js +4 -3
- package/src/schema/project.js +7 -0
- package/src/sync/peer-sync-controller.js +1 -0
- package/src/sync/sync-api.js +171 -3
- package/src/types.ts +4 -3
- package/dist/lib/timing-safe-equal.d.ts +0 -15
- package/dist/lib/timing-safe-equal.d.ts.map +0 -1
- package/src/lib/timing-safe-equal.js +0 -34
|
@@ -66,6 +66,20 @@ export function haveExtension_NamespaceToNumber(object: HaveExtension_Namespace)
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** A map of blob types and variants that a peer intends to download */
|
|
70
|
+
export interface DownloadIntentExtension {
|
|
71
|
+
downloadIntents: { [key: string]: DownloadIntentExtension_DownloadIntent };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DownloadIntentExtension_DownloadIntent {
|
|
75
|
+
variants: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DownloadIntentExtension_DownloadIntentsEntry {
|
|
79
|
+
key: string;
|
|
80
|
+
value: DownloadIntentExtension_DownloadIntent | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function createBaseProjectExtension(): ProjectExtension {
|
|
70
84
|
return { authCoreKeys: [] };
|
|
71
85
|
}
|
|
@@ -194,6 +208,173 @@ export const HaveExtension = {
|
|
|
194
208
|
},
|
|
195
209
|
};
|
|
196
210
|
|
|
211
|
+
function createBaseDownloadIntentExtension(): DownloadIntentExtension {
|
|
212
|
+
return { downloadIntents: {} };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const DownloadIntentExtension = {
|
|
216
|
+
encode(message: DownloadIntentExtension, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
|
217
|
+
Object.entries(message.downloadIntents).forEach(([key, value]) => {
|
|
218
|
+
DownloadIntentExtension_DownloadIntentsEntry.encode({ key: key as any, value }, writer.uint32(10).fork())
|
|
219
|
+
.ldelim();
|
|
220
|
+
});
|
|
221
|
+
return writer;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
decode(input: _m0.Reader | Uint8Array, length?: number): DownloadIntentExtension {
|
|
225
|
+
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
|
226
|
+
let end = length === undefined ? reader.len : reader.pos + length;
|
|
227
|
+
const message = createBaseDownloadIntentExtension();
|
|
228
|
+
while (reader.pos < end) {
|
|
229
|
+
const tag = reader.uint32();
|
|
230
|
+
switch (tag >>> 3) {
|
|
231
|
+
case 1:
|
|
232
|
+
if (tag !== 10) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const entry1 = DownloadIntentExtension_DownloadIntentsEntry.decode(reader, reader.uint32());
|
|
237
|
+
if (entry1.value !== undefined) {
|
|
238
|
+
message.downloadIntents[entry1.key] = entry1.value;
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if ((tag & 7) === 4 || tag === 0) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
reader.skipType(tag & 7);
|
|
246
|
+
}
|
|
247
|
+
return message;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
create<I extends Exact<DeepPartial<DownloadIntentExtension>, I>>(base?: I): DownloadIntentExtension {
|
|
251
|
+
return DownloadIntentExtension.fromPartial(base ?? ({} as any));
|
|
252
|
+
},
|
|
253
|
+
fromPartial<I extends Exact<DeepPartial<DownloadIntentExtension>, I>>(object: I): DownloadIntentExtension {
|
|
254
|
+
const message = createBaseDownloadIntentExtension();
|
|
255
|
+
message.downloadIntents = Object.entries(object.downloadIntents ?? {}).reduce<
|
|
256
|
+
{ [key: string]: DownloadIntentExtension_DownloadIntent }
|
|
257
|
+
>((acc, [key, value]) => {
|
|
258
|
+
if (value !== undefined) {
|
|
259
|
+
acc[key] = DownloadIntentExtension_DownloadIntent.fromPartial(value);
|
|
260
|
+
}
|
|
261
|
+
return acc;
|
|
262
|
+
}, {});
|
|
263
|
+
return message;
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
function createBaseDownloadIntentExtension_DownloadIntent(): DownloadIntentExtension_DownloadIntent {
|
|
268
|
+
return { variants: [] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const DownloadIntentExtension_DownloadIntent = {
|
|
272
|
+
encode(message: DownloadIntentExtension_DownloadIntent, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
|
273
|
+
for (const v of message.variants) {
|
|
274
|
+
writer.uint32(10).string(v!);
|
|
275
|
+
}
|
|
276
|
+
return writer;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
decode(input: _m0.Reader | Uint8Array, length?: number): DownloadIntentExtension_DownloadIntent {
|
|
280
|
+
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
|
281
|
+
let end = length === undefined ? reader.len : reader.pos + length;
|
|
282
|
+
const message = createBaseDownloadIntentExtension_DownloadIntent();
|
|
283
|
+
while (reader.pos < end) {
|
|
284
|
+
const tag = reader.uint32();
|
|
285
|
+
switch (tag >>> 3) {
|
|
286
|
+
case 1:
|
|
287
|
+
if (tag !== 10) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
message.variants.push(reader.string());
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if ((tag & 7) === 4 || tag === 0) {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
reader.skipType(tag & 7);
|
|
298
|
+
}
|
|
299
|
+
return message;
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
create<I extends Exact<DeepPartial<DownloadIntentExtension_DownloadIntent>, I>>(
|
|
303
|
+
base?: I,
|
|
304
|
+
): DownloadIntentExtension_DownloadIntent {
|
|
305
|
+
return DownloadIntentExtension_DownloadIntent.fromPartial(base ?? ({} as any));
|
|
306
|
+
},
|
|
307
|
+
fromPartial<I extends Exact<DeepPartial<DownloadIntentExtension_DownloadIntent>, I>>(
|
|
308
|
+
object: I,
|
|
309
|
+
): DownloadIntentExtension_DownloadIntent {
|
|
310
|
+
const message = createBaseDownloadIntentExtension_DownloadIntent();
|
|
311
|
+
message.variants = object.variants?.map((e) => e) || [];
|
|
312
|
+
return message;
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
function createBaseDownloadIntentExtension_DownloadIntentsEntry(): DownloadIntentExtension_DownloadIntentsEntry {
|
|
317
|
+
return { key: "", value: undefined };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const DownloadIntentExtension_DownloadIntentsEntry = {
|
|
321
|
+
encode(message: DownloadIntentExtension_DownloadIntentsEntry, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
|
322
|
+
if (message.key !== "") {
|
|
323
|
+
writer.uint32(10).string(message.key);
|
|
324
|
+
}
|
|
325
|
+
if (message.value !== undefined) {
|
|
326
|
+
DownloadIntentExtension_DownloadIntent.encode(message.value, writer.uint32(18).fork()).ldelim();
|
|
327
|
+
}
|
|
328
|
+
return writer;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
decode(input: _m0.Reader | Uint8Array, length?: number): DownloadIntentExtension_DownloadIntentsEntry {
|
|
332
|
+
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
|
333
|
+
let end = length === undefined ? reader.len : reader.pos + length;
|
|
334
|
+
const message = createBaseDownloadIntentExtension_DownloadIntentsEntry();
|
|
335
|
+
while (reader.pos < end) {
|
|
336
|
+
const tag = reader.uint32();
|
|
337
|
+
switch (tag >>> 3) {
|
|
338
|
+
case 1:
|
|
339
|
+
if (tag !== 10) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
message.key = reader.string();
|
|
344
|
+
continue;
|
|
345
|
+
case 2:
|
|
346
|
+
if (tag !== 18) {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
message.value = DownloadIntentExtension_DownloadIntent.decode(reader, reader.uint32());
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if ((tag & 7) === 4 || tag === 0) {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
reader.skipType(tag & 7);
|
|
357
|
+
}
|
|
358
|
+
return message;
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
create<I extends Exact<DeepPartial<DownloadIntentExtension_DownloadIntentsEntry>, I>>(
|
|
362
|
+
base?: I,
|
|
363
|
+
): DownloadIntentExtension_DownloadIntentsEntry {
|
|
364
|
+
return DownloadIntentExtension_DownloadIntentsEntry.fromPartial(base ?? ({} as any));
|
|
365
|
+
},
|
|
366
|
+
fromPartial<I extends Exact<DeepPartial<DownloadIntentExtension_DownloadIntentsEntry>, I>>(
|
|
367
|
+
object: I,
|
|
368
|
+
): DownloadIntentExtension_DownloadIntentsEntry {
|
|
369
|
+
const message = createBaseDownloadIntentExtension_DownloadIntentsEntry();
|
|
370
|
+
message.key = object.key ?? "";
|
|
371
|
+
message.value = (object.value !== undefined && object.value !== null)
|
|
372
|
+
? DownloadIntentExtension_DownloadIntent.fromPartial(object.value)
|
|
373
|
+
: undefined;
|
|
374
|
+
return message;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
197
378
|
declare const self: any | undefined;
|
|
198
379
|
declare const window: any | undefined;
|
|
199
380
|
declare const global: any | undefined;
|
package/src/index.js
CHANGED
|
@@ -3,9 +3,19 @@ import {
|
|
|
3
3
|
COORDINATOR_ROLE_ID,
|
|
4
4
|
MEMBER_ROLE_ID,
|
|
5
5
|
} from './roles.js'
|
|
6
|
+
import { kProjectReplicate } from './mapeo-project.js'
|
|
6
7
|
export { plugin as CoMapeoMapsFastifyPlugin } from './fastify-plugins/maps.js'
|
|
7
8
|
export { FastifyController } from './fastify-controller.js'
|
|
8
9
|
export { MapeoManager } from './mapeo-manager.js'
|
|
10
|
+
/** @import { MapeoProject } from './mapeo-project.js' */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {MapeoProject} project
|
|
14
|
+
* @param {Parameters<MapeoProject.prototype[kProjectReplicate]>} args
|
|
15
|
+
* @returns {ReturnType<MapeoProject.prototype[kProjectReplicate]>}
|
|
16
|
+
*/
|
|
17
|
+
export const replicateProject = (project, ...args) =>
|
|
18
|
+
project[kProjectReplicate](...args)
|
|
9
19
|
|
|
10
20
|
export const roles = /** @type {const} */ ({
|
|
11
21
|
CREATOR_ROLE_ID,
|
package/src/invite-api.js
CHANGED
|
@@ -3,7 +3,7 @@ import { pEvent } from 'p-event'
|
|
|
3
3
|
import { InviteResponse_Decision } from './generated/rpc.js'
|
|
4
4
|
import { assert, keyToId, noop } from './utils.js'
|
|
5
5
|
import HashMap from './lib/hashmap.js'
|
|
6
|
-
import timingSafeEqual from '
|
|
6
|
+
import timingSafeEqual from 'string-timing-safe-equal'
|
|
7
7
|
import { Logger } from './logger.js'
|
|
8
8
|
/** @import { MapBuffers } from './types.js' */
|
|
9
9
|
/**
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
import { assert } from '../utils.js'
|
|
3
|
+
import { migrate as drizzleMigrate } from 'drizzle-orm/better-sqlite3/migrator'
|
|
4
|
+
import { DRIZZLE_MIGRATIONS_TABLE } from '../constants.js'
|
|
5
|
+
/** @import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} queryResult
|
|
9
|
+
* @returns {number}
|
|
10
|
+
*/
|
|
11
|
+
const getNumberResult = (queryResult) => {
|
|
12
|
+
assert(
|
|
13
|
+
queryResult &&
|
|
14
|
+
typeof queryResult === 'object' &&
|
|
15
|
+
'result' in queryResult &&
|
|
16
|
+
typeof queryResult.result === 'number',
|
|
17
|
+
'expected query to return proper result'
|
|
18
|
+
)
|
|
19
|
+
return queryResult.result
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the number of rows in a table using `SELECT COUNT(*)`.
|
|
24
|
+
* Returns 0 if the table doesn't exist.
|
|
25
|
+
*
|
|
26
|
+
* @param {BetterSQLite3Database} db
|
|
27
|
+
* @param {string} tableName
|
|
28
|
+
* @returns {number}
|
|
29
|
+
*/
|
|
30
|
+
const safeCountTableRows = (db, tableName) =>
|
|
31
|
+
db.transaction((tx) => {
|
|
32
|
+
const existsQuery = sql`
|
|
33
|
+
SELECT EXISTS (
|
|
34
|
+
SELECT 1
|
|
35
|
+
FROM sqlite_schema
|
|
36
|
+
WHERE type IS 'table'
|
|
37
|
+
AND name IS ${tableName}
|
|
38
|
+
) AS result
|
|
39
|
+
`
|
|
40
|
+
const existsResult = tx.get(existsQuery)
|
|
41
|
+
const exists = getNumberResult(existsResult)
|
|
42
|
+
if (!exists) return 0
|
|
43
|
+
|
|
44
|
+
const countQuery = sql`
|
|
45
|
+
SELECT COUNT(*) AS result
|
|
46
|
+
FROM ${sql.identifier(tableName)}
|
|
47
|
+
`
|
|
48
|
+
const countResult = tx.get(countQuery)
|
|
49
|
+
return getNumberResult(countResult)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @internal
|
|
54
|
+
* @typedef {'initialized database' | 'migrated' | 'no migration'} MigrationResult
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Wrapper around Drizzle's migration function. Returns what happened during
|
|
59
|
+
* migration; did a migration occur?
|
|
60
|
+
*
|
|
61
|
+
* @param {BetterSQLite3Database} db
|
|
62
|
+
* @param {object} options
|
|
63
|
+
* @param {string} options.migrationsFolder
|
|
64
|
+
* @returns {MigrationResult}
|
|
65
|
+
*/
|
|
66
|
+
export const migrate = (db, { migrationsFolder }) => {
|
|
67
|
+
const migrationsBefore = safeCountTableRows(db, DRIZZLE_MIGRATIONS_TABLE)
|
|
68
|
+
drizzleMigrate(db, {
|
|
69
|
+
migrationsFolder,
|
|
70
|
+
migrationsTable: DRIZZLE_MIGRATIONS_TABLE,
|
|
71
|
+
})
|
|
72
|
+
const migrationsAfter = safeCountTableRows(db, DRIZZLE_MIGRATIONS_TABLE)
|
|
73
|
+
|
|
74
|
+
if (migrationsAfter === migrationsBefore) return 'no migration'
|
|
75
|
+
|
|
76
|
+
if (migrationsBefore === 0) return 'initialized database'
|
|
77
|
+
|
|
78
|
+
return 'migrated'
|
|
79
|
+
}
|
package/src/lib/error.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an `Error` with a `code` property.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const err = new ErrorWithCode('INVALID_DATA', 'data was invalid')
|
|
6
|
+
* err.message
|
|
7
|
+
* // => 'data was invalid'
|
|
8
|
+
* err.code
|
|
9
|
+
* // => 'INVALID_DATA'
|
|
10
|
+
*/
|
|
11
|
+
export class ErrorWithCode extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} code
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {unknown} [options.cause]
|
|
17
|
+
*/
|
|
18
|
+
constructor(code, message, options) {
|
|
19
|
+
super(message, options)
|
|
20
|
+
/** @readonly */ this.code = code
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the error message from an object if possible.
|
|
26
|
+
* Otherwise, stringify the argument.
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} maybeError
|
|
29
|
+
* @returns {string}
|
|
30
|
+
* @example
|
|
31
|
+
* try {
|
|
32
|
+
* // do something
|
|
33
|
+
* } catch (err) {
|
|
34
|
+
* console.error(getErrorMessage(err))
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
export function getErrorMessage(maybeError) {
|
|
38
|
+
if (maybeError && typeof maybeError === 'object' && 'message' in maybeError) {
|
|
39
|
+
try {
|
|
40
|
+
const { message } = maybeError
|
|
41
|
+
if (typeof message === 'string') return message
|
|
42
|
+
} catch (_err) {
|
|
43
|
+
// Ignored
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return 'unknown error'
|
|
47
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from 'node:net'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Is this hostname an IP address?
|
|
5
|
+
*
|
|
6
|
+
* @param {string} hostname
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
* @example
|
|
9
|
+
* isHostnameIpAddress('100.64.0.42')
|
|
10
|
+
* // => false
|
|
11
|
+
*
|
|
12
|
+
* isHostnameIpAddress('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]')
|
|
13
|
+
* // => true
|
|
14
|
+
*
|
|
15
|
+
* isHostnameIpAddress('example.com')
|
|
16
|
+
* // => false
|
|
17
|
+
*/
|
|
18
|
+
export function isHostnameIpAddress(hostname) {
|
|
19
|
+
if (isIPv4(hostname)) return true
|
|
20
|
+
|
|
21
|
+
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
|
22
|
+
return isIPv6(hostname.slice(1, -1))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { pipeline } from 'node:stream/promises'
|
|
2
|
+
import { Transform } from 'node:stream'
|
|
3
|
+
import { createWebSocketStream } from 'ws'
|
|
4
|
+
/** @import { WebSocket } from 'ws' */
|
|
5
|
+
/** @import { ReplicationStream } from '../types.js' */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {WebSocket} ws
|
|
9
|
+
* @param {ReplicationStream} replicationStream
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
*/
|
|
12
|
+
export function wsCoreReplicator(ws, replicationStream) {
|
|
13
|
+
// This is purely to satisfy typescript at its worst. `pipeline` expects a
|
|
14
|
+
// NodeJS ReadWriteStream, but our replicationStream is a streamx Duplex
|
|
15
|
+
// stream. The difference is that streamx does not implement the
|
|
16
|
+
// `setEncoding`, `unpipe`, `wrap` or `isPaused` methods. The `pipeline`
|
|
17
|
+
// function does not depend on any of these methods (I have read through the
|
|
18
|
+
// NodeJS source code at cebf21d (v22.9.0) to confirm this), so we can safely
|
|
19
|
+
// cast the stream to a NodeJS ReadWriteStream.
|
|
20
|
+
const _replicationStream = /** @type {NodeJS.ReadWriteStream} */ (
|
|
21
|
+
/** @type {unknown} */ (replicationStream)
|
|
22
|
+
)
|
|
23
|
+
return pipeline(
|
|
24
|
+
_replicationStream,
|
|
25
|
+
wsSafetyTransform(ws),
|
|
26
|
+
createWebSocketStream(ws),
|
|
27
|
+
_replicationStream
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Avoid writing data to a closing or closed websocket, which would result in an
|
|
33
|
+
* error. Instead we drop the data and wait for the stream close/end events to
|
|
34
|
+
* propagate and close the streams cleanly.
|
|
35
|
+
*
|
|
36
|
+
* @param {WebSocket} ws
|
|
37
|
+
*/
|
|
38
|
+
function wsSafetyTransform(ws) {
|
|
39
|
+
return new Transform({
|
|
40
|
+
transform(chunk, encoding, callback) {
|
|
41
|
+
if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
|
42
|
+
return callback()
|
|
43
|
+
}
|
|
44
|
+
callback(null, chunk)
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
package/src/mapeo-manager.js
CHANGED
|
@@ -16,10 +16,11 @@ import {
|
|
|
16
16
|
kBlobStore,
|
|
17
17
|
kClearDataIfLeft,
|
|
18
18
|
kProjectLeave,
|
|
19
|
+
kSetIsArchiveDevice,
|
|
19
20
|
kSetOwnDeviceInfo,
|
|
20
21
|
} from './mapeo-project.js'
|
|
21
22
|
import {
|
|
22
|
-
|
|
23
|
+
deviceSettingsTable,
|
|
23
24
|
projectKeysTable,
|
|
24
25
|
projectSettingsTable,
|
|
25
26
|
} from './schema/client.js'
|
|
@@ -44,7 +45,6 @@ import { LocalPeers } from './local-peers.js'
|
|
|
44
45
|
import { InviteApi } from './invite-api.js'
|
|
45
46
|
import { LocalDiscovery } from './discovery/local-discovery.js'
|
|
46
47
|
import { Roles } from './roles.js'
|
|
47
|
-
import NoiseSecretStream from '@hyperswarm/secret-stream'
|
|
48
48
|
import { Logger } from './logger.js'
|
|
49
49
|
import {
|
|
50
50
|
kSyncState,
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
kRescindFullStopRequest,
|
|
53
53
|
} from './sync/sync-api.js'
|
|
54
54
|
/** @import { ProjectSettingsValue as ProjectValue } from '@comapeo/schema' */
|
|
55
|
+
/** @import NoiseSecretStream from '@hyperswarm/secret-stream' */
|
|
55
56
|
/** @import { SetNonNullable } from 'type-fest' */
|
|
56
57
|
/** @import { CoreStorage, Namespace } from './types.js' */
|
|
57
58
|
/** @import { DeviceInfoParam } from './schema/client.js' */
|
|
@@ -79,9 +80,6 @@ export const DEFAULT_FALLBACK_MAP_FILE_PATH = require.resolve(
|
|
|
79
80
|
export const DEFAULT_ONLINE_STYLE_URL =
|
|
80
81
|
'https://demotiles.maplibre.org/style.json'
|
|
81
82
|
|
|
82
|
-
export const kRPC = Symbol('rpc')
|
|
83
|
-
export const kManagerReplicate = Symbol('replicate manager')
|
|
84
|
-
|
|
85
83
|
/**
|
|
86
84
|
* @typedef {Omit<import('./local-peers.js').PeerInfo, 'protomux'>} PublicPeerInfo
|
|
87
85
|
*/
|
|
@@ -221,33 +219,10 @@ export class MapeoManager extends TypedEmitter {
|
|
|
221
219
|
this.#localDiscovery.on('connection', this.#replicate.bind(this))
|
|
222
220
|
}
|
|
223
221
|
|
|
224
|
-
/**
|
|
225
|
-
* MapeoRPC instance, used for tests
|
|
226
|
-
*/
|
|
227
|
-
get [kRPC]() {
|
|
228
|
-
return this.#localPeers
|
|
229
|
-
}
|
|
230
|
-
|
|
231
222
|
get deviceId() {
|
|
232
223
|
return this.#deviceId
|
|
233
224
|
}
|
|
234
225
|
|
|
235
|
-
/**
|
|
236
|
-
* Create a Mapeo replication stream. This replication connects the Mapeo RPC
|
|
237
|
-
* channel and allows invites. All active projects will sync automatically to
|
|
238
|
-
* this replication stream. Only use for local (trusted) connections, because
|
|
239
|
-
* the RPC channel key is public. To sync a specific project without
|
|
240
|
-
* connecting RPC, use project[kProjectReplication].
|
|
241
|
-
*
|
|
242
|
-
* @param {boolean} isInitiator
|
|
243
|
-
*/
|
|
244
|
-
[kManagerReplicate](isInitiator) {
|
|
245
|
-
const noiseStream = new NoiseSecretStream(isInitiator, undefined, {
|
|
246
|
-
keyPair: this.#keyManager.getIdentityKeypair(),
|
|
247
|
-
})
|
|
248
|
-
return this.#replicate(noiseStream)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
226
|
/**
|
|
252
227
|
* @param {'blobs' | 'icons' | 'maps'} mediaType
|
|
253
228
|
* @returns {Promise<string>}
|
|
@@ -507,6 +482,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
507
482
|
async #createProjectInstance(projectKeys) {
|
|
508
483
|
validateProjectKeys(projectKeys)
|
|
509
484
|
const projectId = keyToId(projectKeys.projectKey)
|
|
485
|
+
const isArchiveDevice = this.getIsArchiveDevice()
|
|
510
486
|
const project = new MapeoProject({
|
|
511
487
|
...this.#projectStorage(projectId),
|
|
512
488
|
...projectKeys,
|
|
@@ -517,6 +493,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
517
493
|
localPeers: this.#localPeers,
|
|
518
494
|
logger: this.#loggerBase,
|
|
519
495
|
getMediaBaseUrl: this.#getMediaBaseUrl.bind(this),
|
|
496
|
+
isArchiveDevice,
|
|
520
497
|
})
|
|
521
498
|
await project[kClearDataIfLeft]()
|
|
522
499
|
return project
|
|
@@ -579,7 +556,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
579
556
|
* downloaded their proof of project membership and the project config.
|
|
580
557
|
*
|
|
581
558
|
* @param {Pick<import('./generated/rpc.js').ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string }} projectJoinDetails
|
|
582
|
-
* @param {{ waitForSync?: boolean }} [opts]
|
|
559
|
+
* @param {{ waitForSync?: boolean }} [opts] Set opts.waitForSync = false to not wait for sync during addProject()
|
|
583
560
|
* @returns {Promise<string>}
|
|
584
561
|
*/
|
|
585
562
|
addProject = async (
|
|
@@ -733,9 +710,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
733
710
|
}
|
|
734
711
|
|
|
735
712
|
/**
|
|
736
|
-
* @typedef {
|
|
737
|
-
* import('./schema/client.js').DeviceInfoParam['deviceType'],
|
|
738
|
-
* 'selfHostedServer'>} RPCDeviceType
|
|
713
|
+
* @typedef {import('./schema/client.js').DeviceInfoParam['deviceType']} RPCDeviceType
|
|
739
714
|
*/
|
|
740
715
|
|
|
741
716
|
/**
|
|
@@ -746,10 +721,10 @@ export class MapeoManager extends TypedEmitter {
|
|
|
746
721
|
async setDeviceInfo(deviceInfo) {
|
|
747
722
|
const values = { deviceId: this.#deviceId, deviceInfo }
|
|
748
723
|
this.#db
|
|
749
|
-
.insert(
|
|
724
|
+
.insert(deviceSettingsTable)
|
|
750
725
|
.values(values)
|
|
751
726
|
.onConflictDoUpdate({
|
|
752
|
-
target:
|
|
727
|
+
target: deviceSettingsTable.deviceId,
|
|
753
728
|
set: values,
|
|
754
729
|
})
|
|
755
730
|
.run()
|
|
@@ -762,13 +737,22 @@ export class MapeoManager extends TypedEmitter {
|
|
|
762
737
|
})
|
|
763
738
|
)
|
|
764
739
|
|
|
765
|
-
|
|
766
|
-
this
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
740
|
+
if (deviceInfo.deviceType !== 'selfHostedServer') {
|
|
741
|
+
// We have to make a copy of this because TypeScript can't guarantee that
|
|
742
|
+
// `deviceInfo` won't be mutated by the time it gets to the
|
|
743
|
+
// `sendDeviceInfo` call below.
|
|
744
|
+
const deviceInfoToSend = {
|
|
745
|
+
...deviceInfo,
|
|
746
|
+
deviceType: deviceInfo.deviceType,
|
|
747
|
+
}
|
|
748
|
+
await Promise.all(
|
|
749
|
+
this.#localPeers.peers
|
|
750
|
+
.filter(({ status }) => status === 'connected')
|
|
751
|
+
.map((peer) =>
|
|
752
|
+
this.#localPeers.sendDeviceInfo(peer.deviceId, deviceInfoToSend)
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
}
|
|
772
756
|
|
|
773
757
|
this.#l.log('set device info %o', deviceInfo)
|
|
774
758
|
}
|
|
@@ -784,8 +768,8 @@ export class MapeoManager extends TypedEmitter {
|
|
|
784
768
|
getDeviceInfo() {
|
|
785
769
|
const row = this.#db
|
|
786
770
|
.select()
|
|
787
|
-
.from(
|
|
788
|
-
.where(eq(
|
|
771
|
+
.from(deviceSettingsTable)
|
|
772
|
+
.where(eq(deviceSettingsTable.deviceId, this.#deviceId))
|
|
789
773
|
.get()
|
|
790
774
|
return {
|
|
791
775
|
deviceId: this.#deviceId,
|
|
@@ -794,6 +778,50 @@ export class MapeoManager extends TypedEmitter {
|
|
|
794
778
|
}
|
|
795
779
|
}
|
|
796
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Set whether this device is an archive device. Archive devices will download
|
|
783
|
+
* all media during sync, where-as non-archive devices will not download media
|
|
784
|
+
* original variants, and only download preview and thumbnail variants.
|
|
785
|
+
* @param {boolean} isArchiveDevice
|
|
786
|
+
* @returns {void}
|
|
787
|
+
*/
|
|
788
|
+
setIsArchiveDevice(isArchiveDevice) {
|
|
789
|
+
const values = { deviceId: this.#deviceId, isArchiveDevice }
|
|
790
|
+
const result = this.#db
|
|
791
|
+
.insert(deviceSettingsTable)
|
|
792
|
+
.values(values)
|
|
793
|
+
.onConflictDoUpdate({
|
|
794
|
+
target: deviceSettingsTable.deviceId,
|
|
795
|
+
set: values,
|
|
796
|
+
})
|
|
797
|
+
.run()
|
|
798
|
+
if (!result || result.changes === 0) {
|
|
799
|
+
throw new Error('Failed to set isArchiveDevice')
|
|
800
|
+
}
|
|
801
|
+
for (const project of this.#activeProjects.values()) {
|
|
802
|
+
project[kSetIsArchiveDevice](isArchiveDevice)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Get whether this device is an archive device. Archive devices will download
|
|
808
|
+
* all media during sync, where-as non-archive devices will not download media
|
|
809
|
+
* original variants, and only download preview and thumbnail variants.
|
|
810
|
+
* @returns {boolean} isArchiveDevice
|
|
811
|
+
*/
|
|
812
|
+
getIsArchiveDevice() {
|
|
813
|
+
const row = this.#db
|
|
814
|
+
.select()
|
|
815
|
+
.from(deviceSettingsTable)
|
|
816
|
+
.where(eq(deviceSettingsTable.deviceId, this.#deviceId))
|
|
817
|
+
.get()
|
|
818
|
+
if (typeof row?.isArchiveDevice === 'boolean') {
|
|
819
|
+
return row.isArchiveDevice
|
|
820
|
+
} else {
|
|
821
|
+
return true
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
797
825
|
/**
|
|
798
826
|
* @returns {InviteApi}
|
|
799
827
|
*/
|