Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
232 lines
6.1 KiB
TypeScript
232 lines
6.1 KiB
TypeScript
import {
|
|
type AwarenessRecord,
|
|
AwarenessStorageBase,
|
|
} from '../../storage/awareness';
|
|
import type { SpaceType } from '../../utils/universal-id';
|
|
import {
|
|
base64ToUint8Array,
|
|
SocketConnection,
|
|
SPACE_JOIN_BATCH_LIMIT,
|
|
uint8ArrayToBase64,
|
|
} from './socket';
|
|
|
|
interface CloudAwarenessStorageOptions {
|
|
isSelfHosted: boolean;
|
|
serverBaseUrl: string;
|
|
type: SpaceType;
|
|
id: string;
|
|
}
|
|
|
|
export class CloudAwarenessStorage extends AwarenessStorageBase {
|
|
static readonly identifier = 'CloudAwarenessStorage';
|
|
|
|
constructor(private readonly options: CloudAwarenessStorageOptions) {
|
|
super();
|
|
}
|
|
|
|
connection = new SocketConnection(
|
|
this.options.serverBaseUrl,
|
|
this.options.isSelfHosted
|
|
);
|
|
|
|
private get socket() {
|
|
return this.connection.inner.socket;
|
|
}
|
|
|
|
private readonly activeAwarenessIds = new Set<string>();
|
|
private readonly joinedAwarenessIds = new Set<string>();
|
|
private joinPromise: Promise<void> | undefined;
|
|
|
|
private joinActiveAwareness(): Promise<void> {
|
|
if (
|
|
this.connection.status !== 'connected' ||
|
|
this.activeAwarenessIds.size === 0
|
|
) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (this.joinPromise) {
|
|
return this.joinPromise;
|
|
}
|
|
|
|
const batchPromise = (async () => {
|
|
while (this.connection.status === 'connected') {
|
|
await Promise.resolve();
|
|
const pendingIds = [...this.activeAwarenessIds].filter(
|
|
docId => !this.joinedAwarenessIds.has(docId)
|
|
);
|
|
if (pendingIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (
|
|
let index = 0;
|
|
index < pendingIds.length;
|
|
index += SPACE_JOIN_BATCH_LIMIT
|
|
) {
|
|
const spaces = pendingIds
|
|
.slice(index, index + SPACE_JOIN_BATCH_LIMIT)
|
|
.map(docId => ({
|
|
spaceType: this.options.type,
|
|
spaceId: this.options.id,
|
|
docId,
|
|
}));
|
|
const response = await this.socket.emitWithAck('space:join-batch', {
|
|
spaces,
|
|
clientVersion: BUILD_CONFIG.appVersion,
|
|
});
|
|
|
|
if ('error' in response) {
|
|
throw new Error(
|
|
`Awareness join failed: ${response.error.name}: ${response.error.message}`
|
|
);
|
|
}
|
|
if (!response.data.success) {
|
|
throw new Error('Awareness join was rejected');
|
|
}
|
|
}
|
|
|
|
for (const docId of pendingIds) {
|
|
if (this.activeAwarenessIds.has(docId)) {
|
|
this.joinedAwarenessIds.add(docId);
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
|
|
const sharedPromise = batchPromise.finally(() => {
|
|
this.joinPromise = undefined;
|
|
});
|
|
this.joinPromise = sharedPromise;
|
|
return sharedPromise;
|
|
}
|
|
|
|
override async update(record: AwarenessRecord): Promise<void> {
|
|
const encodedUpdate = await uint8ArrayToBase64(record.bin);
|
|
this.socket.emit('space:update-awareness', {
|
|
spaceType: this.options.type,
|
|
spaceId: this.options.id,
|
|
docId: record.docId,
|
|
awarenessUpdate: encodedUpdate,
|
|
});
|
|
}
|
|
|
|
override subscribeUpdate(
|
|
id: string,
|
|
onUpdate: (update: AwarenessRecord, origin?: string) => void,
|
|
onCollect: () => Promise<AwarenessRecord | null>
|
|
): () => void {
|
|
this.activeAwarenessIds.add(id);
|
|
|
|
// leave awareness
|
|
const leave = () => {
|
|
this.activeAwarenessIds.delete(id);
|
|
this.joinedAwarenessIds.delete(id);
|
|
this.socket.off('space:collect-awareness', handleCollectAwareness);
|
|
this.socket.off(
|
|
'space:broadcast-awareness-update',
|
|
handleBroadcastAwarenessUpdate
|
|
);
|
|
if (this.connection.status !== 'connected') return;
|
|
this.socket.emit('space:leave-batch', {
|
|
spaceType: this.options.type,
|
|
spaceId: this.options.id,
|
|
docIds: [id],
|
|
});
|
|
};
|
|
|
|
// join awareness, and collect awareness from others
|
|
const joinAndCollect = async () => {
|
|
this.socket.on('space:collect-awareness', handleCollectAwareness);
|
|
this.socket.on(
|
|
'space:broadcast-awareness-update',
|
|
handleBroadcastAwarenessUpdate
|
|
);
|
|
await this.joinActiveAwareness();
|
|
if (this.connection.status !== 'connected') return;
|
|
this.socket.emit('space:load-awarenesses', {
|
|
spaceType: this.options.type,
|
|
spaceId: this.options.id,
|
|
docId: id,
|
|
});
|
|
};
|
|
|
|
const handleCollectAwareness = ({
|
|
spaceId,
|
|
spaceType,
|
|
docId,
|
|
}: {
|
|
spaceId: string;
|
|
spaceType: string;
|
|
docId: string;
|
|
}) => {
|
|
if (
|
|
spaceId === this.options.id &&
|
|
spaceType === this.options.type &&
|
|
docId === id
|
|
) {
|
|
(async () => {
|
|
const record = await onCollect();
|
|
if (record) {
|
|
const encodedUpdate = await uint8ArrayToBase64(record.bin);
|
|
this.socket.emit('space:update-awareness', {
|
|
spaceType: this.options.type,
|
|
spaceId: this.options.id,
|
|
docId: record.docId,
|
|
awarenessUpdate: encodedUpdate,
|
|
});
|
|
}
|
|
})().catch(err => console.error('awareness upload failed', err));
|
|
}
|
|
};
|
|
|
|
const handleBroadcastAwarenessUpdate = ({
|
|
spaceType,
|
|
spaceId,
|
|
docId,
|
|
awarenessUpdate,
|
|
}: {
|
|
spaceType: string;
|
|
spaceId: string;
|
|
docId: string;
|
|
awarenessUpdate: string;
|
|
}) => {
|
|
if (
|
|
spaceId === this.options.id &&
|
|
spaceType === this.options.type &&
|
|
docId === id
|
|
) {
|
|
onUpdate({
|
|
bin: base64ToUint8Array(awarenessUpdate),
|
|
docId: id,
|
|
});
|
|
}
|
|
};
|
|
|
|
if (this.connection.status === 'connected') {
|
|
joinAndCollect().catch(err =>
|
|
console.error('awareness join failed', err)
|
|
);
|
|
}
|
|
|
|
const unsubscribeConnectionStatusChanged = this.connection.onStatusChanged(
|
|
status => {
|
|
if (status !== 'connected') {
|
|
this.joinedAwarenessIds.clear();
|
|
}
|
|
if (status === 'connected') {
|
|
joinAndCollect().catch(err =>
|
|
console.error('awareness join failed', err)
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
leave();
|
|
|
|
unsubscribeConnectionStatusChanged();
|
|
};
|
|
}
|
|
}
|