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 the collaborative sync WebSocket gateway when a socket emits a sync or awareness message for a space room it never joined. SyncSocketAdapter.assertIn checks client.rooms for `${spaceType}:${spaceId}` (plus `:awareness` suffix for awareness rooms) before push/diff/delete/timestamps and before 'space:load-awarenesses' / 'space:update-awareness' broadcasts. Rooms are only entered via the join handler, which also runs permission checks.

Solutions

  1. Await the join acknowledgement for the space room (and the `${docId}:awareness` room for awareness traffic) before emitting any doc:update or awareness message
  2. Make sure the spaceType and spaceId in every message exactly match the values used in the join call
  3. On socket reconnect, re-join all previously joined rooms before resuming update pushes
  4. If the join itself fails, surface that permission error to the user instead of continuing to push updates

Example fix

// before
socket.emit('doc:update', { spaceType: 'workspace', spaceId, docId, updates }); // may throw not_in_space

// after
await joinSpace(socket, spaceId, docId); // resolves only after the join ack
socket.emit('doc:update', { spaceType: 'workspace', spaceId, docId, updates });
Defensive patterns

Strategy: validation

Validate before calling

// track joined rooms client-side and gate every emission
const joined = new Set<string>();
socket.on('space:join-ack', ({ spaceId, roomType }) => joined.add(`${spaceId}:${roomType ?? 'sync'}`));
function assertJoined(spaceId: string, roomType = 'sync') {
  if (!joined.has(`${spaceId}:${roomType}`)) {
    throw new Error(`not joined: ${spaceId}:${roomType}`);
  }
}

Type guard

function isRoomJoined(joined: Set<string>, spaceId: string, roomType = 'sync'): boolean {
  return joined.has(`${spaceId}:${roomType}`);
}

Try / catch

try {
  await pushUpdates(spaceId, docId, updates);
} catch (e) {
  if (e?.code === 'not_in_space') {
    await joinSpace(socket, spaceId, docId); // re-join, then retry exactly once
    await pushUpdates(spaceId, docId, updates);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Emitting 'doc:update' push/diff/delete/timestamps for a spaceId before the space/doc join ack returns; broadcasting 'space:update-awareness' without having joined the `${docId}:awareness` room; a prior join that failed assertAccessible (no permission) so the room was never entered; resuming pushes on a reconnecting socket without re-joining rooms; sending spaceType/spaceId values that differ from the ones used at join time.

Common situations: Custom sync clients racing the join handshake against the first update; awareness traffic sent right after doc:join but before the awareness room join; using the userspace adapter with a workspace id so the room key mismatches; proxies dropping the join ack.

Related errors


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

Appendix: source

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

  room(spaceId: string, roomType: RoomType = 'sync') {
    return `${this.spaceType}:${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,
    expectedPermissionGeneration?: number,
    writeIntent: 'update_doc' | 'create_doc' = 'update_doc'
  ) {
    this.assertIn(spaceId);

View on GitHub (pinned to 2af30773ae)