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
- Await the join acknowledgement for the space room (and the `${docId}:awareness` room for awareness traffic) before emitting any doc:update or awareness message
- Make sure the spaceType and spaceId in every message exactly match the values used in the join call
- On socket reconnect, re-join all previously joined rooms before resuming update pushes
- 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
- Treat join as an awaited prerequisite: never emit doc:update or awareness messages before the join ack
- Join the `${docId}:awareness` room separately before broadcasting awareness updates
- Keep a client-side set of joined rooms and clear it on disconnect/reconnect so stale state cannot be trusted
- Log the spaceType/spaceId of failed pushes to catch adapter mismatches early
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)