toeverything/AFFiNE · error · NotInSpace

not_in_space

not_in_space

Error message

You should join in Space ${spaceId} before broadcasting messages.

What it means

Thrown by SyncSocketAdapter.assertIn() when the socket client has not joined the room for the target space. assertIn() is invoked by push(), diff(), delete(), and getTimestamps(), so any sync data operation attempted before the client joined the space room will fail. Membership is tracked via this.client.rooms.has(roomId); the room key is `${spaceType}:${spaceId}:${roomType}`.

Source

Thrown at packages/backend/server/src/core/sync/gateway.ts:907

    }
    await this.assertAccessible(spaceId, userId, 'Workspace.Sync');
    return this.client.join(this.room(spaceId, roomType));
  }

  async leave(spaceId: string, roomType: RoomType = 'sync') {
    if (!this.in(spaceId, roomType)) {
      return;
    }
    return this.client.leave(this.room(spaceId, roomType));
  }

  in(spaceId: string, roomType: RoomType = 'sync') {
    return this.client.rooms.has(this.room(spaceId, roomType));
  }

  assertIn(spaceId: string, roomType: RoomType = 'sync') {
    if (!this.client.rooms.has(this.room(spaceId, roomType))) {
      throw new NotInSpace({ spaceId });
    }
  }

  abstract assertAccessible(
    spaceId: string,
    userId: string,
    action: WorkspaceAction
  ): Promise<void>;

  async push(
    spaceId: string,
    docId: string,
    updates: Buffer[],
    editorId: string
  ) {
    this.assertIn(spaceId);
    return await this.storage.pushDocUpdates(spaceId, docId, updates, editorId);
  }

View on GitHub (pinned to 26c515e050)

Solutions

  1. Always emit 'space:join' and await its acknowledgment before sending any push/diff/delete/timestamps messages for that space.
  2. On socket reconnect, re-join all previously joined spaces before resuming sync.
  3. Gate outbound sync messages behind a per-space 'joined' flag set on join ack and cleared on leave/disconnect.

Example fix

// before
socket.emit('space:push-doc-update', { spaceType, spaceId, docId, update });

// after — await join before pushing
await socket.emitWithAck('space:join', { spaceType, spaceId });
joinedSpaces.add(spaceId);
socket.emit('space:push-doc-update', { spaceType, spaceId, docId, update });
Defensive patterns

Strategy: validation

Validate before calling

// Track joined spaces and assert before any data op
if (!joinedSpaces.has(`${spaceType}:${spaceId}`)) {
  await socket.emitWithAck('space:join', { spaceType, spaceId });
  joinedSpaces.add(`${spaceType}:${spaceId}`);
}

Type guard

function isJoined(joined: Set<string>, spaceType: string, spaceId: string): boolean {
  return joined.has(`${spaceType}:${spaceId}`);
}

Try / catch

try {
  await operation();
} catch (e) {
  if (e?.code === 'not_in_space') { await join(spaceType, spaceId); await operation(); return; }
  throw e;
}

Prevention

When it happens

Trigger: Client emits 'space:push-doc-update', 'space:diff-doc-update', 'space:delete-doc', or 'space:get-doc-timestamps' before emitting 'space:join' for that spaceId, or after the client was disconnected and reconnected without re-joining.

Common situations: Client boot sequence orders data operations before the join handshake completes; reconnection logic forgets to re-join rooms; client switches spaces and sends a leftover update for the previous space after leaving it.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/ff373fd85b5233e9. Report an issue: GitHub.