Skip to content

Commit

Permalink
feat: you can now leave a project if you're the only member (#720)
Browse files Browse the repository at this point in the history
Closes [#717], which has more background on the motivation for this
change.

[#717]: #717
  • Loading branch information
EvanHahn authored Jul 23, 2024
1 parent 534c285 commit b62a064
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 45 deletions.
53 changes: 19 additions & 34 deletions src/mapeo-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -630,11 +630,12 @@ export class MapeoProject extends TypedEmitter {
return this.#iconApi
}

async [kProjectLeave]() {
// 1. Check that the device can leave the project
/**
* @returns {Promise<void>}
*/
async #throwIfCannotLeaveProject() {
const roleDocs = await this.#dataTypes.role.getMany()

// 1.1 Check that we are not blocked in the project
const ownRole = roleDocs.find(({ docId }) => this.#deviceId === docId)

if (ownRole?.roleId === BLOCKED_ROLE_ID) {
Expand All @@ -643,49 +644,33 @@ export class MapeoProject extends TypedEmitter {

const allRoles = await this.#roles.getAll()

// 1.2 Check that we are not the only device in the project
if (allRoles.size <= 1) {
throw new Error('Cannot leave a project as the only device')
}
const isOnlyDevice = allRoles.size <= 1
if (isOnlyDevice) return

// 1.3 Check if there are other known devices that are either the project creator or a coordinator
const projectCreatorDeviceId = await this.#coreOwnership.getOwner(
this.#projectId
)
let otherCreatorOrCoordinatorExists = false

for (const deviceId of allRoles.keys()) {
// Skip self (see 1.1 and 1.2 for relevant checks)
if (deviceId === this.#deviceId) continue

// Check if the device is the project creator first because
// it is a derived role that is not stored in the role docs explicitly
if (deviceId === projectCreatorDeviceId) {
otherCreatorOrCoordinatorExists = true
break
}

// Determine if the the device is a coordinator based on the role docs
const isCoordinator = roleDocs.some(
(doc) => doc.docId === deviceId && doc.roleId === COORDINATOR_ROLE_ID
)

if (isCoordinator) {
otherCreatorOrCoordinatorExists = true
break
}
const isCreatorOrCoordinator =
deviceId === projectCreatorDeviceId ||
roleDocs.some(
(doc) => doc.docId === deviceId && doc.roleId === COORDINATOR_ROLE_ID
)
if (isCreatorOrCoordinator) return
}

if (!otherCreatorOrCoordinatorExists) {
throw new Error(
'Cannot leave a project that does not have an external creator or another coordinator'
)
}
throw new Error(
'Cannot leave a project that does not have an external creator or another coordinator'
)
}

async [kProjectLeave]() {
await this.#throwIfCannotLeaveProject()

// 2. Assign LEFT role for device
await this.#roles.assignRole(this.#deviceId, LEFT_ROLE_ID)

// 3. Clear data
await this[kClearDataIfLeft]()
}

Expand Down
27 changes: 16 additions & 11 deletions test-e2e/project-leave.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,7 @@ import {
waitForSync,
} from './utils.js'

test('Creator cannot leave project if they are the only member', async (t) => {
const [creatorManager] = await createManagers(1, t)

const projectId = await creatorManager.createProject({ name: 'mapeo' })

await assert.rejects(async () => {
await creatorManager.leaveProject(projectId)
}, 'attempting to leave fails')
})

test('Creator cannot leave project if no other coordinators exist', async (t) => {
test("Creator cannot leave project if they're the only coordinator", async (t) => {
const managers = await createManagers(2, t)

const disconnectPeers = connectPeers(managers)
Expand Down Expand Up @@ -114,6 +104,21 @@ test('Blocked member cannot leave project', async (t) => {
}, 'Member attempting to leave project fails')
})

test('leaving a project as the only member', async (t) => {
const [manager] = await createManagers(1, t)

const projectId = await manager.createProject({ name: 'mapeo' })
const creatorProject = await manager.getProject(projectId)

await manager.leaveProject(projectId)

assert.deepEqual(
await creatorProject.$getOwnRole(),
ROLES[LEFT_ROLE_ID],
'creator now has LEFT role'
)
})

test('Creator can leave project if another coordinator exists', async (t) => {
const managers = await createManagers(2, t)

Expand Down

0 comments on commit b62a064

Please sign in to comment.