@comapeo/core 5.0.0 → 5.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.
@@ -22,6 +22,13 @@
22
22
  "when": 1749657174497,
23
23
  "tag": "0002_cooing_princess_powerful",
24
24
  "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1761590068314,
30
+ "tag": "0003_lying_piledriver",
31
+ "breakpoints": true
25
32
  }
26
33
  ]
27
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comapeo/core",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Offline p2p mapping library",
5
5
  "main": "src/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -164,7 +164,7 @@
164
164
  },
165
165
  "dependencies": {
166
166
  "@comapeo/fallback-smp": "^1.0.0",
167
- "@comapeo/schema": "2.1.1",
167
+ "@comapeo/schema": "2.2.0",
168
168
  "@digidem/types": "^2.3.0",
169
169
  "@fastify/error": "^3.4.1",
170
170
  "@fastify/type-provider-typebox": "^4.1.0",
@@ -1056,8 +1056,6 @@ export class MapeoManager extends TypedEmitter {
1056
1056
  const project = await this.getProject(projectPublicId)
1057
1057
 
1058
1058
  await project[kProjectLeave]()
1059
-
1060
- this.#activeProjects.delete(projectPublicId)
1061
1059
  }
1062
1060
 
1063
1061
  async getMapStyleJsonUrl() {
@@ -69,6 +69,7 @@ import { createWriteStream } from 'fs'
69
69
  import ensureError from 'ensure-error'
70
70
  /** @import { ProjectSettingsValue, Observation, Track } from '@comapeo/schema' */
71
71
  /** @import { Attachment, CoreStorage, BlobFilter, BlobId, BlobStoreEntriesStream, KeyPair, Namespace, ReplicationStream, GenericBlobFilter, MapeoValueMap, MapeoDocMap } from './types.js' */
72
+ /** @import {Role} from './roles.js' */
72
73
  /** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
73
74
  /** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
74
75
  /** @typedef {Map<string,Attachment>} SeenAttachments*/
@@ -109,7 +110,18 @@ const EMPTY_PROJECT_SETTINGS = Object.freeze({ sendStats: false })
109
110
  const VARIANT_EXPORT_ORDER = ['original', 'preview', 'thumbnail']
110
111
 
111
112
  /**
112
- * @extends {TypedEmitter<{ close: () => void }>}
113
+ * @typedef RoleChangeEvent
114
+ * @property {Role & {reason: string | null}} role
115
+ */
116
+
117
+ /**
118
+ * @typedef {object} ProjectEvents
119
+ * @property {() => void} close Project resources have been cleared up
120
+ * @property {(changeEvent: RoleChangeEvent) => void} own-role-change
121
+ */
122
+
123
+ /**
124
+ * @extends {TypedEmitter<ProjectEvents>}
113
125
  */
114
126
  export class MapeoProject extends TypedEmitter {
115
127
  #projectKey
@@ -506,6 +518,16 @@ export class MapeoProject extends TypedEmitter {
506
518
  // for the core.
507
519
  localPeers.on('discovery-key', onDiscoverykey)
508
520
 
521
+ this.#roles.on('update', (roleDocIds) => {
522
+ for (const roleDocId of roleDocIds) {
523
+ // Ignore docs not about ourselves
524
+ if (roleDocId !== this.#deviceId) continue
525
+ this.#handleRoleChange().catch((e) => {
526
+ this.#l.log(`Error: Could not handle role change`, ensureError(e))
527
+ })
528
+ }
529
+ })
530
+
509
531
  this.once('close', () => {
510
532
  localPeers.off('peer-add', onPeerAdd)
511
533
  localPeers.off('discovery-key', onDiscoverykey)
@@ -718,8 +740,18 @@ export class MapeoProject extends TypedEmitter {
718
740
  return (await this.$getProjectSettings()).name
719
741
  }
720
742
 
743
+ /**
744
+ * @returns {Promise<Role & {reason: string | null}>}
745
+ */
721
746
  async $getOwnRole() {
722
- return this.#roles.getRole(this.#deviceId)
747
+ const reason = await this.#roles.getRoleReason(this.#deviceId)
748
+ const role = await this.#roles.getRole(this.#deviceId)
749
+ return { ...role, reason }
750
+ }
751
+
752
+ async #handleRoleChange() {
753
+ const role = await this.$getOwnRole()
754
+ this.emit('own-role-change', { role })
723
755
  }
724
756
 
725
757
  /**
@@ -1280,12 +1312,6 @@ export class MapeoProject extends TypedEmitter {
1280
1312
  async #throwIfCannotLeaveProject() {
1281
1313
  const roleDocs = await this.#dataTypes.role.getMany()
1282
1314
 
1283
- const ownRole = roleDocs.find(({ docId }) => this.#deviceId === docId)
1284
-
1285
- if (ownRole?.roleId === BLOCKED_ROLE_ID) {
1286
- throw new Error('Cannot leave a project as a blocked device')
1287
- }
1288
-
1289
1315
  const allRoles = await this.#roles.getAll()
1290
1316
 
1291
1317
  const isOnlyDevice = allRoles.size <= 1
@@ -1311,9 +1337,13 @@ export class MapeoProject extends TypedEmitter {
1311
1337
  }
1312
1338
 
1313
1339
  async [kProjectLeave]() {
1340
+ const ownRole = await this.$getOwnRole()
1341
+
1314
1342
  await this.#throwIfCannotLeaveProject()
1315
1343
 
1316
- await this.#roles.assignRole(this.#deviceId, LEFT_ROLE_ID)
1344
+ if (ownRole.roleId !== BLOCKED_ROLE_ID) {
1345
+ await this.#roles.assignRole(this.#deviceId, LEFT_ROLE_ID)
1346
+ }
1317
1347
 
1318
1348
  await this[kClearData]()
1319
1349
  }
package/src/member-api.js CHANGED
@@ -22,6 +22,7 @@ import { InviteAbortedError, ProjectDetailsSendFailError } from './errors.js'
22
22
  import { wsCoreReplicator } from './lib/ws-core-replicator.js'
23
23
  import {
24
24
  BLOCKED_ROLE_ID,
25
+ LEFT_ROLE_ID,
25
26
  MEMBER_ROLE_ID,
26
27
  ROLES,
27
28
  isRoleIdForNewInvite,
@@ -355,6 +356,25 @@ export class MemberApi extends TypedEmitter {
355
356
  })
356
357
  }
357
358
 
359
+ /**
360
+ * Remove a member from the project
361
+ * @param {string} deviceId Device id of member to remove
362
+ * @param {object} [opts]
363
+ * @param {string} opts.reason
364
+ */
365
+ async remove(deviceId, opts) {
366
+ const member = await this.getById(deviceId)
367
+ const { roleId } = member.role
368
+
369
+ if (roleId === BLOCKED_ROLE_ID || roleId === LEFT_ROLE_ID) {
370
+ throw new ErrorWithCode('ALREADY_BLOCKED', 'Member already blocked')
371
+ }
372
+
373
+ // Add blocked role to project
374
+ // Should error if you don't have permission to do so
375
+ await this.#roles.assignRole(deviceId, BLOCKED_ROLE_ID, opts)
376
+ }
377
+
358
378
  /**
359
379
  * Remove a server peer. Only works when the peer is reachable
360
380
  *
package/src/roles.js CHANGED
@@ -292,6 +292,19 @@ export class Roles extends TypedEmitter {
292
292
  return ROLES[roleId]
293
293
  }
294
294
 
295
+ /**
296
+ * Get the reason for the role of `deviceId` (if it exists).
297
+ *
298
+ * @param {string} deviceId
299
+ * @returns {Promise<string | null>}
300
+ */
301
+ async getRoleReason(deviceId) {
302
+ const roleRecord = await this.#dataType
303
+ .getByDocId(deviceId)
304
+ .catch(nullIfNotFound)
305
+ return roleRecord?.reason ?? null
306
+ }
307
+
295
308
  /**
296
309
  * Get roles of all devices in the project. For your own device, if you have
297
310
  * not yet synced your own role record, the "no role" capabilties is
@@ -346,8 +359,10 @@ export class Roles extends TypedEmitter {
346
359
  *
347
360
  * @param {string} deviceId
348
361
  * @param {RoleIdAssignableToAnyone} roleId
362
+ * @param {object} [opts]
363
+ * @param {string} opts.reason
349
364
  */
350
- async assignRole(deviceId, roleId) {
365
+ async assignRole(deviceId, roleId, opts) {
351
366
  assert(
352
367
  isRoleIdAssignableToAnyone(roleId),
353
368
  `Role ID should be assignable to anyone but got ${roleId}`
@@ -398,6 +413,7 @@ export class Roles extends TypedEmitter {
398
413
  schemaName: 'role',
399
414
  roleId,
400
415
  fromIndex,
416
+ reason: opts?.reason,
401
417
  }
402
418
  )
403
419
  } else {