{"record":{"id":"ff373fd85b5233e9","repo":"toeverything/AFFiNE","slug":"not-in-space","errorCode":"not_in_space","errorMessage":"You should join in Space ${spaceId} before broadcasting messages.","messagePattern":"You should join in Space (.+?) before broadcasting messages\\.","errorType":"exception","errorClass":"NotInSpace","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/sync/gateway.ts","lineNumber":907,"sourceCode":"    }\n    await this.assertAccessible(spaceId, userId, 'Workspace.Sync');\n    return this.client.join(this.room(spaceId, roomType));\n  }\n\n  async leave(spaceId: string, roomType: RoomType = 'sync') {\n    if (!this.in(spaceId, roomType)) {\n      return;\n    }\n    return this.client.leave(this.room(spaceId, roomType));\n  }\n\n  in(spaceId: string, roomType: RoomType = 'sync') {\n    return this.client.rooms.has(this.room(spaceId, roomType));\n  }\n\n  assertIn(spaceId: string, roomType: RoomType = 'sync') {\n    if (!this.client.rooms.has(this.room(spaceId, roomType))) {\n      throw new NotInSpace({ spaceId });\n    }\n  }\n\n  abstract assertAccessible(\n    spaceId: string,\n    userId: string,\n    action: WorkspaceAction\n  ): Promise<void>;\n\n  async push(\n    spaceId: string,\n    docId: string,\n    updates: Buffer[],\n    editorId: string\n  ) {\n    this.assertIn(spaceId);\n    return await this.storage.pushDocUpdates(spaceId, docId, updates, editorId);\n  }","sourceCodeStart":889,"sourceCodeEnd":925,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/sync/gateway.ts#L889-L925","documentation":"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}`.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Always emit 'space:join' and await its acknowledgment before sending any push/diff/delete/timestamps messages for that space.","On socket reconnect, re-join all previously joined spaces before resuming sync.","Gate outbound sync messages behind a per-space 'joined' flag set on join ack and cleared on leave/disconnect."],"exampleFix":"// before\nsocket.emit('space:push-doc-update', { spaceType, spaceId, docId, update });\n\n// after — await join before pushing\nawait socket.emitWithAck('space:join', { spaceType, spaceId });\njoinedSpaces.add(spaceId);\nsocket.emit('space:push-doc-update', { spaceType, spaceId, docId, update });","handlingStrategy":"validation","validationCode":"// Track joined spaces and assert before any data op\nif (!joinedSpaces.has(`${spaceType}:${spaceId}`)) {\n  await socket.emitWithAck('space:join', { spaceType, spaceId });\n  joinedSpaces.add(`${spaceType}:${spaceId}`);\n}","typeGuard":"function isJoined(joined: Set<string>, spaceType: string, spaceId: string): boolean {\n  return joined.has(`${spaceType}:${spaceId}`);\n}","tryCatchPattern":"try {\n  await operation();\n} catch (e) {\n  if (e?.code === 'not_in_space') { await join(spaceType, spaceId); await operation(); return; }\n  throw e;\n}","preventionTips":["Await the 'space:join' ack before sending any push/diff/delete/timestamps.","Re-join all spaces on reconnect.","Maintain a per-socket joined-spaces set cleared on disconnect."],"tags":["sync","websocket","rooms","ordering"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}