refactor(infra): migrate to new infra (#5565)

This commit is contained in:
EYHN
2024-01-30 07:16:39 +00:00
parent 1e3499c323
commit 329fc19852
170 changed files with 2007 additions and 4354 deletions
@@ -1,5 +1,5 @@
import { DebugLogger } from '@affine/debug';
import type { AwarenessProvider } from '@affine/workspace';
import type { AwarenessProvider } from '@toeverything/infra';
import {
applyAwarenessUpdate,
type Awareness,
@@ -14,30 +14,66 @@ const logger = new DebugLogger('affine:awareness:socketio');
type AwarenessChanges = Record<'added' | 'updated' | 'removed', number[]>;
export function createCloudAwarenessProvider(
workspaceId: string,
awareness: Awareness
): AwarenessProvider {
const socket = getIoManager().socket('/');
export class AffineCloudAwarenessProvider implements AwarenessProvider {
socket = getIoManager().socket('/');
const awarenessBroadcast = ({
constructor(
private readonly workspaceId: string,
private readonly awareness: Awareness
) {}
connect(): void {
this.socket.on('server-awareness-broadcast', this.awarenessBroadcast);
this.socket.on(
'new-client-awareness-init',
this.newClientAwarenessInitHandler
);
this.awareness.on('update', this.awarenessUpdate);
window.addEventListener('beforeunload', this.windowBeforeUnloadHandler);
this.socket.connect();
this.socket.on('connect', () => this.handleConnect());
this.socket.emit('client-handshake-awareness', this.workspaceId);
this.socket.emit('awareness-init', this.workspaceId);
}
disconnect(): void {
removeAwarenessStates(
this.awareness,
[this.awareness.clientID],
'disconnect'
);
this.awareness.off('update', this.awarenessUpdate);
this.socket.emit('client-leave-awareness', this.workspaceId);
this.socket.off('server-awareness-broadcast', this.awarenessBroadcast);
this.socket.off(
'new-client-awareness-init',
this.newClientAwarenessInitHandler
);
this.socket.off('connect', this.handleConnect);
window.removeEventListener('unload', this.windowBeforeUnloadHandler);
}
awarenessBroadcast = ({
workspaceId: wsId,
awarenessUpdate,
}: {
workspaceId: string;
awarenessUpdate: string;
}) => {
if (wsId !== workspaceId) {
if (wsId !== this.workspaceId) {
return;
}
applyAwarenessUpdate(
awareness,
this.awareness,
base64ToUint8Array(awarenessUpdate),
'remote'
);
};
const awarenessUpdate = (changes: AwarenessChanges, origin: unknown) => {
awarenessUpdate = (changes: AwarenessChanges, origin: unknown) => {
if (origin === 'remote') {
return;
}
@@ -46,63 +82,41 @@ export function createCloudAwarenessProvider(
res.concat(cur)
);
const update = encodeAwarenessUpdate(awareness, changedClients);
const update = encodeAwarenessUpdate(this.awareness, changedClients);
uint8ArrayToBase64(update)
.then(encodedUpdate => {
socket.emit('awareness-update', {
workspaceId: workspaceId,
this.socket.emit('awareness-update', {
workspaceId: this.workspaceId,
awarenessUpdate: encodedUpdate,
});
})
.catch(err => logger.error(err));
};
const newClientAwarenessInitHandler = () => {
const awarenessUpdate = encodeAwarenessUpdate(awareness, [
awareness.clientID,
newClientAwarenessInitHandler = () => {
const awarenessUpdate = encodeAwarenessUpdate(this.awareness, [
this.awareness.clientID,
]);
uint8ArrayToBase64(awarenessUpdate)
.then(encodedAwarenessUpdate => {
socket.emit('awareness-update', {
guid: workspaceId,
this.socket.emit('awareness-update', {
guid: this.workspaceId,
awarenessUpdate: encodedAwarenessUpdate,
});
})
.catch(err => logger.error(err));
};
const windowBeforeUnloadHandler = () => {
removeAwarenessStates(awareness, [awareness.clientID], 'window unload');
windowBeforeUnloadHandler = () => {
removeAwarenessStates(
this.awareness,
[this.awareness.clientID],
'window unload'
);
};
function handleConnect() {
socket.emit('client-handshake-awareness', workspaceId);
socket.emit('awareness-init', workspaceId);
}
return {
connect: () => {
socket.on('server-awareness-broadcast', awarenessBroadcast);
socket.on('new-client-awareness-init', newClientAwarenessInitHandler);
awareness.on('update', awarenessUpdate);
window.addEventListener('beforeunload', windowBeforeUnloadHandler);
socket.connect();
socket.on('connect', handleConnect);
socket.emit('client-handshake-awareness', workspaceId);
socket.emit('awareness-init', workspaceId);
},
disconnect: () => {
removeAwarenessStates(awareness, [awareness.clientID], 'disconnect');
awareness.off('update', awarenessUpdate);
socket.emit('client-leave-awareness', workspaceId);
socket.off('server-awareness-broadcast', awarenessBroadcast);
socket.off('new-client-awareness-init', newClientAwarenessInitHandler);
socket.off('connect', handleConnect);
window.removeEventListener('unload', windowBeforeUnloadHandler);
},
handleConnect = () => {
this.socket.emit('client-handshake-awareness', this.workspaceId);
this.socket.emit('awareness-init', this.workspaceId);
};
}
@@ -7,69 +7,70 @@ import {
setBlobMutation,
} from '@affine/graphql';
import { fetcher } from '@affine/graphql';
import type { BlobStorage } from '@affine/workspace';
import { BlobStorageOverCapacity } from '@affine/workspace';
import { type BlobStorage, BlobStorageOverCapacity } from '@toeverything/infra';
import { isArray } from 'lodash-es';
import { bufferToBlob } from '../utils/buffer-to-blob';
export const createAffineCloudBlobStorage = (
workspaceId: string
): BlobStorage => {
return {
name: 'affine-cloud',
readonly: false,
get: async key => {
const suffix = key.startsWith('/')
? key
: `/api/workspaces/${workspaceId}/blobs/${key}`;
export class AffineCloudBlobStorage implements BlobStorage {
constructor(private readonly workspaceId: string) {}
return fetchWithTraceReport(getBaseUrl() + suffix).then(async res => {
if (!res.ok) {
// status not in the range 200-299
return null;
name = 'affine-cloud';
readonly = false;
async get(key: string) {
const suffix = key.startsWith('/')
? key
: `/api/workspaces/${this.workspaceId}/blobs/${key}`;
return fetchWithTraceReport(getBaseUrl() + suffix).then(async res => {
if (!res.ok) {
// status not in the range 200-299
return null;
}
return bufferToBlob(await res.arrayBuffer());
});
}
async set(key: string, value: Blob) {
// set blob will check blob size & quota
return await fetcher({
query: setBlobMutation,
variables: {
workspaceId: this.workspaceId,
blob: new File([value], key),
},
})
.then(res => res.setBlob)
.catch(err => {
if (isArray(err)) {
err.map(e => {
if (e instanceof GraphQLError && e.extensions.code === 413) {
throw new BlobStorageOverCapacity(e);
} else throw e;
});
}
return bufferToBlob(await res.arrayBuffer());
throw err;
});
},
set: async (key, value) => {
// set blob will check blob size & quota
return await fetcher({
query: setBlobMutation,
variables: {
workspaceId,
blob: new File([value], key),
},
})
.then(res => res.setBlob)
.catch(err => {
if (isArray(err)) {
err.map(e => {
if (e instanceof GraphQLError && e.extensions.code === 413) {
throw new BlobStorageOverCapacity(e);
} else throw e;
});
}
throw err;
});
},
list: async () => {
const result = await fetcher({
query: listBlobsQuery,
variables: {
workspaceId,
},
});
return result.listBlobs;
},
delete: async (key: string) => {
await fetcher({
query: deleteBlobMutation,
variables: {
workspaceId,
hash: key,
},
});
},
};
};
}
async delete(key: string) {
await fetcher({
query: deleteBlobMutation,
variables: {
workspaceId: key,
hash: key,
},
});
}
async list() {
const result = await fetcher({
query: listBlobsQuery,
variables: {
workspaceId: this.workspaceId,
},
});
return result.listBlobs;
}
}
+128 -106
View File
@@ -5,18 +5,26 @@ import {
getWorkspacesQuery,
} from '@affine/graphql';
import { fetcher } from '@affine/graphql';
import type { WorkspaceListProvider } from '@affine/workspace';
import { globalBlockSuiteSchema } from '@affine/workspace';
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { WorkspaceListProvider } from '@toeverything/infra';
import {
type BlobStorage,
type SyncStorage,
type WorkspaceInfo,
type WorkspaceMetadata,
} from '@toeverything/infra';
import { globalBlockSuiteSchema } from '@toeverything/infra';
import { difference } from 'lodash-es';
import { nanoid } from 'nanoid';
import { getSession } from 'next-auth/react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { createLocalBlobStorage } from '../local/blob';
import { createLocalStorage } from '../local/sync';
import { IndexedDBBlobStorage } from '../local/blob-indexeddb';
import { SQLiteBlobStorage } from '../local/blob-sqlite';
import { IndexedDBSyncStorage } from '../local/sync-indexeddb';
import { SQLiteSyncStorage } from '../local/sync-sqlite';
import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from './consts';
import { createAffineStaticStorage } from './sync';
import { AffineStaticSyncStorage } from './sync';
async function getCloudWorkspaceList() {
const session = await getSession();
@@ -41,120 +49,134 @@ async function getCloudWorkspaceList() {
}
}
export function createCloudWorkspaceListProvider(): WorkspaceListProvider {
const notifyChannel = new BroadcastChannel(
export class CloudWorkspaceListProvider implements WorkspaceListProvider {
name = WorkspaceFlavour.AFFINE_CLOUD;
notifyChannel = new BroadcastChannel(
CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY
);
return {
name: WorkspaceFlavour.AFFINE_CLOUD,
async getList() {
return getCloudWorkspaceList();
},
async create(initial) {
const tempId = nanoid();
getList(): Promise<WorkspaceMetadata[]> {
return getCloudWorkspaceList();
}
async delete(workspaceId: string): Promise<void> {
await fetcher({
query: deleteWorkspaceMutation,
variables: {
id: workspaceId,
},
});
// notify all browser tabs, so they can update their workspace list
this.notifyChannel.postMessage(null);
}
async create(
initial: (
workspace: BlockSuiteWorkspace,
blobStorage: BlobStorage
) => Promise<void>
): Promise<WorkspaceMetadata> {
const tempId = nanoid();
const workspace = new BlockSuiteWorkspace({
id: tempId,
idGenerator: () => nanoid(),
schema: globalBlockSuiteSchema,
});
const workspace = new BlockSuiteWorkspace({
id: tempId,
idGenerator: () => nanoid(),
schema: globalBlockSuiteSchema,
});
// create workspace on cloud, get workspace id
const {
createWorkspace: { id: workspaceId },
} = await fetcher({
query: createWorkspaceMutation,
});
// create workspace on cloud, get workspace id
const {
createWorkspace: { id: workspaceId },
} = await fetcher({
query: createWorkspaceMutation,
});
// save the initial state to local storage, then sync to cloud
const blobStorage = createLocalBlobStorage(workspaceId);
const syncStorage = createLocalStorage(workspaceId);
// save the initial state to local storage, then sync to cloud
const blobStorage = environment.isDesktop
? new SQLiteBlobStorage(workspaceId)
: new IndexedDBBlobStorage(workspaceId);
const syncStorage = environment.isDesktop
? new SQLiteSyncStorage(workspaceId)
: new IndexedDBSyncStorage(workspaceId);
// apply initial state
await initial(workspace, blobStorage);
// apply initial state
await initial(workspace, blobStorage);
// save workspace to local storage, should be vary fast
await syncStorage.push(workspaceId, encodeStateAsUpdate(workspace.doc));
for (const subdocs of workspace.doc.getSubdocs()) {
await syncStorage.push(subdocs.guid, encodeStateAsUpdate(subdocs));
}
// save workspace to local storage, should be vary fast
await syncStorage.push(workspaceId, encodeStateAsUpdate(workspace.doc));
for (const subdocs of workspace.doc.getSubdocs()) {
await syncStorage.push(subdocs.guid, encodeStateAsUpdate(subdocs));
}
// notify all browser tabs, so they can update their workspace list
notifyChannel.postMessage(null);
// notify all browser tabs, so they can update their workspace list
this.notifyChannel.postMessage(null);
return workspaceId;
},
async delete(id) {
await fetcher({
query: deleteWorkspaceMutation,
variables: {
id,
},
});
// notify all browser tabs, so they can update their workspace list
notifyChannel.postMessage(null);
},
subscribe(callback) {
let lastWorkspaceIDs: string[] = [];
return { id: workspaceId, flavour: WorkspaceFlavour.AFFINE_CLOUD };
}
subscribe(
callback: (changed: {
added?: WorkspaceMetadata[] | undefined;
deleted?: WorkspaceMetadata[] | undefined;
}) => void
): () => void {
let lastWorkspaceIDs: string[] = [];
function scan() {
(async () => {
const allWorkspaceIDs = (await getCloudWorkspaceList()).map(
workspace => workspace.id
);
const added = difference(allWorkspaceIDs, lastWorkspaceIDs);
const deleted = difference(lastWorkspaceIDs, allWorkspaceIDs);
lastWorkspaceIDs = allWorkspaceIDs;
callback({
added: added.map(id => ({
id,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
})),
deleted: deleted.map(id => ({
id,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
})),
});
})().catch(err => {
console.error(err);
function scan() {
(async () => {
const allWorkspaceIDs = (await getCloudWorkspaceList()).map(
workspace => workspace.id
);
const added = difference(allWorkspaceIDs, lastWorkspaceIDs);
const deleted = difference(lastWorkspaceIDs, allWorkspaceIDs);
lastWorkspaceIDs = allWorkspaceIDs;
callback({
added: added.map(id => ({
id,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
})),
deleted: deleted.map(id => ({
id,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
})),
});
}
scan();
// rescan if other tabs notify us
notifyChannel.addEventListener('message', scan);
return () => {
notifyChannel.removeEventListener('message', scan);
};
},
async getInformation(id) {
// get information from both cloud and local storage
// we use affine 'static' storage here, which use http protocol, no need to websocket.
const cloudStorage = createAffineStaticStorage(id);
const localStorage = createLocalStorage(id);
// download root doc
const localData = await localStorage.pull(id, new Uint8Array([]));
const cloudData = await cloudStorage.pull(id, new Uint8Array([]));
if (!cloudData && !localData) {
return;
}
const bs = new BlockSuiteWorkspace({
id,
schema: globalBlockSuiteSchema,
})().catch(err => {
console.error(err);
});
}
if (localData) applyUpdate(bs.doc, localData.data);
if (cloudData) applyUpdate(bs.doc, cloudData.data);
scan();
return {
name: bs.meta.name,
avatar: bs.meta.avatar,
};
},
};
// rescan if other tabs notify us
this.notifyChannel.addEventListener('message', scan);
return () => {
this.notifyChannel.removeEventListener('message', scan);
};
}
async getInformation(id: string): Promise<WorkspaceInfo | undefined> {
// get information from both cloud and local storage
// we use affine 'static' storage here, which use http protocol, no need to websocket.
const cloudStorage: SyncStorage = new AffineStaticSyncStorage(id);
const localStorage = environment.isDesktop
? new SQLiteSyncStorage(id)
: new IndexedDBSyncStorage(id);
// download root doc
const localData = await localStorage.pull(id, new Uint8Array([]));
const cloudData = await cloudStorage.pull(id, new Uint8Array([]));
if (!cloudData && !localData) {
return;
}
const bs = new BlockSuiteWorkspace({
id,
schema: globalBlockSuiteSchema,
});
if (localData) applyUpdate(bs.doc, localData.data);
if (cloudData) applyUpdate(bs.doc, cloudData.data);
return {
name: bs.meta.name,
avatar: bs.meta.avatar,
};
}
}
@@ -1,6 +1,7 @@
import { DebugLogger } from '@affine/debug';
import { fetchWithTraceReport } from '@affine/graphql';
import type { SyncStorage } from '@affine/workspace';
import { type SyncStorage } from '@toeverything/infra';
import type { CleanupService } from '@toeverything/infra/lifecycle';
import { getIoManager } from '../../utils/affine-io';
import { base64ToUint8Array, uint8ArrayToBase64 } from '../../utils/base64';
@@ -8,22 +9,21 @@ import { MultipleBatchSyncSender } from './batch-sync-sender';
const logger = new DebugLogger('affine:storage:socketio');
export function createAffineStorage(
workspaceId: string
): SyncStorage & { disconnect: () => void } {
logger.debug('createAffineStorage', workspaceId);
const socket = getIoManager().socket('/');
export class AffineSyncStorage implements SyncStorage {
name = 'affine-cloud';
const syncSender = new MultipleBatchSyncSender(async (guid, updates) => {
socket = getIoManager().socket('/');
syncSender = new MultipleBatchSyncSender(async (guid, updates) => {
const payload = await Promise.all(
updates.map(update => uint8ArrayToBase64(update))
);
return new Promise(resolve => {
socket.emit(
this.socket.emit(
'client-update-v2',
{
workspaceId,
workspaceId: this.workspaceId,
guid,
updates: payload,
},
@@ -35,7 +35,7 @@ export function createAffineStorage(
// TODO: raise error with different code to users
if (response.error) {
logger.error('client-update-v2 error', {
workspaceId,
workspaceId: this.workspaceId,
guid,
response,
});
@@ -51,145 +51,160 @@ export function createAffineStorage(
});
});
function handleConnect() {
socket.emit(
constructor(
private readonly workspaceId: string,
cleanupService: CleanupService
) {
this.socket.on('connect', this.handleConnect);
this.socket.connect();
this.socket.emit(
'client-handshake-sync',
workspaceId,
this.workspaceId,
(response: { error?: any }) => {
if (!response.error) {
syncSender.start();
this.syncSender.start();
}
}
);
cleanupService.add(() => {
this.cleanup();
});
}
socket.on('connect', handleConnect);
socket.connect();
socket.emit(
'client-handshake-sync',
workspaceId,
(response: { error?: any }) => {
if (!response.error) {
syncSender.start();
handleConnect = () => {
this.socket.emit(
'client-handshake-sync',
this.workspaceId,
(response: { error?: any }) => {
if (!response.error) {
this.syncSender.start();
}
}
}
);
);
};
return {
name: 'affine-cloud',
async pull(docId, state) {
const stateVector = state ? await uint8ArrayToBase64(state) : undefined;
async pull(
docId: string,
state: Uint8Array
): Promise<{ data: Uint8Array; state?: Uint8Array } | null> {
const stateVector = state ? await uint8ArrayToBase64(state) : undefined;
return new Promise((resolve, reject) => {
logger.debug('doc-load-v2', {
workspaceId: workspaceId,
return new Promise((resolve, reject) => {
logger.debug('doc-load-v2', {
workspaceId: this.workspaceId,
guid: docId,
stateVector,
});
this.socket.emit(
'doc-load-v2',
{
workspaceId: this.workspaceId,
guid: docId,
stateVector,
});
socket.emit(
'doc-load-v2',
{
workspaceId: workspaceId,
},
(
response: // TODO: reuse `EventError` with server
{ error: any } | { data: { missing: string; state: string } }
) => {
logger.debug('doc-load callback', {
workspaceId: this.workspaceId,
guid: docId,
stateVector,
},
(
response: // TODO: reuse `EventError` with server
{ error: any } | { data: { missing: string; state: string } }
) => {
logger.debug('doc-load callback', {
workspaceId: workspaceId,
guid: docId,
stateVector,
response,
});
if ('error' in response) {
// TODO: result `EventError` with server
if (response.error.code === 'DOC_NOT_FOUND') {
resolve(null);
} else {
reject(new Error(response.error.message));
}
} else {
resolve({
data: base64ToUint8Array(response.data.missing),
state: response.data.state
? base64ToUint8Array(response.data.state)
: undefined,
});
}
}
);
});
},
async push(docId, update) {
logger.debug('client-update-v2', {
workspaceId,
guid: docId,
update,
});
await syncSender.send(docId, update);
},
async subscribe(cb, disconnect) {
const handleUpdate = async (message: {
workspaceId: string;
guid: string;
updates: string[];
}) => {
if (message.workspaceId === workspaceId) {
message.updates.forEach(update => {
cb(message.guid, base64ToUint8Array(update));
response,
});
}
};
socket.on('server-updates', handleUpdate);
socket.on('disconnect', reason => {
socket.off('server-updates', handleUpdate);
disconnect(reason);
});
return () => {
socket.off('server-updates', handleUpdate);
};
},
disconnect() {
syncSender.stop();
socket.emit('client-leave-sync', workspaceId);
socket.off('connect', handleConnect);
},
};
}
export function createAffineStaticStorage(workspaceId: string): SyncStorage {
logger.debug('createAffineStaticStorage', workspaceId);
return {
name: 'affine-cloud-static',
async pull(docId) {
const response = await fetchWithTraceReport(
`/api/workspaces/${workspaceId}/docs/${docId}`,
{
priority: 'high',
if ('error' in response) {
// TODO: result `EventError` with server
if (response.error.code === 'DOC_NOT_FOUND') {
resolve(null);
} else {
reject(new Error(response.error.message));
}
} else {
resolve({
data: base64ToUint8Array(response.data.missing),
state: response.data.state
? base64ToUint8Array(response.data.state)
: undefined,
});
}
}
);
if (response.ok) {
const arrayBuffer = await response.arrayBuffer();
});
}
return { data: new Uint8Array(arrayBuffer) };
async push(docId: string, update: Uint8Array) {
logger.debug('client-update-v2', {
workspaceId: this.workspaceId,
guid: docId,
update,
});
await this.syncSender.send(docId, update);
}
async subscribe(
cb: (docId: string, data: Uint8Array) => void,
disconnect: (reason: string) => void
) {
const handleUpdate = async (message: {
workspaceId: string;
guid: string;
updates: string[];
}) => {
if (message.workspaceId === this.workspaceId) {
message.updates.forEach(update => {
cb(message.guid, base64ToUint8Array(update));
});
}
};
this.socket.on('server-updates', handleUpdate);
return null;
},
async push() {
throw new Error('Not implemented');
},
async subscribe() {
throw new Error('Not implemented');
},
};
this.socket.on('disconnect', reason => {
this.socket.off('server-updates', handleUpdate);
disconnect(reason);
});
return () => {
this.socket.off('server-updates', handleUpdate);
};
}
cleanup() {
this.syncSender.stop();
this.socket.emit('client-leave-sync', this.workspaceId);
this.socket.off('connect', this.handleConnect);
}
}
export class AffineStaticSyncStorage implements SyncStorage {
name = 'affine-cloud-static';
constructor(private readonly workspaceId: string) {}
async pull(
docId: string
): Promise<{ data: Uint8Array; state?: Uint8Array | undefined } | null> {
const response = await fetchWithTraceReport(
`/api/workspaces/${this.workspaceId}/docs/${docId}`,
{
priority: 'high',
}
);
if (response.ok) {
const arrayBuffer = await response.arrayBuffer();
return { data: new Uint8Array(arrayBuffer) };
}
return null;
}
push(): Promise<void> {
throw new Error('Method not implemented.');
}
subscribe(): Promise<() => void> {
throw new Error('Method not implemented.');
}
}
@@ -1,77 +1,55 @@
import { setupEditorFlags } from '@affine/env/global';
import type { WorkspaceFactory } from '@affine/workspace';
import { BlobEngine, SyncEngine, WorkspaceEngine } from '@affine/workspace';
import { globalBlockSuiteSchema } from '@affine/workspace';
import { Workspace } from '@affine/workspace';
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { nanoid } from 'nanoid';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { WorkspaceFactory } from '@toeverything/infra';
import {
AwarenessContext,
AwarenessProvider,
RemoteBlobStorage,
RemoteSyncStorage,
WorkspaceIdContext,
WorkspaceScope,
} from '@toeverything/infra';
import type { ServiceCollection } from '@toeverything/infra/di';
import { CleanupService } from '@toeverything/infra/lifecycle';
import { createBroadcastChannelAwarenessProvider } from '../local/awareness';
import { createLocalBlobStorage } from '../local/blob';
import { createStaticBlobStorage } from '../local/blob-static';
import { createLocalStorage } from '../local/sync';
import { createCloudAwarenessProvider } from './awareness';
import { createAffineCloudBlobStorage } from './blob';
import { createAffineStorage } from './sync';
import { LocalWorkspaceFactory } from '../local';
import { IndexedDBBlobStorage, SQLiteBlobStorage } from '../local';
import { AffineCloudAwarenessProvider } from './awareness';
import { AffineCloudBlobStorage } from './blob';
import { AffineSyncStorage } from './sync';
export const cloudWorkspaceFactory: WorkspaceFactory = {
name: 'affine-cloud',
openWorkspace(metadata) {
const blobEngine = new BlobEngine(createLocalBlobStorage(metadata.id), [
createAffineCloudBlobStorage(metadata.id),
createStaticBlobStorage(),
]);
export class CloudWorkspaceFactory implements WorkspaceFactory {
name = WorkspaceFlavour.AFFINE_CLOUD;
configureWorkspace(services: ServiceCollection): void {
// configure local-first providers
new LocalWorkspaceFactory().configureWorkspace(services);
// create blocksuite workspace
const bs = new BlockSuiteWorkspace({
id: metadata.id,
blobStorages: [
() => ({
crud: blobEngine,
}),
],
idGenerator: () => nanoid(),
schema: globalBlockSuiteSchema,
});
const affineStorage = createAffineStorage(metadata.id);
const syncEngine = new SyncEngine(bs.doc, createLocalStorage(metadata.id), [
affineStorage,
]);
const awarenessProviders = [
createBroadcastChannelAwarenessProvider(
metadata.id,
bs.awarenessStore.awareness
),
createCloudAwarenessProvider(metadata.id, bs.awarenessStore.awareness),
];
const engine = new WorkspaceEngine(
blobEngine,
syncEngine,
awarenessProviders
);
setupEditorFlags(bs);
const workspace = new Workspace(metadata, engine, bs);
workspace.onStop.once(() => {
// affine sync storage need manually disconnect
affineStorage.disconnect();
});
return workspace;
},
services
.scope(WorkspaceScope)
.addImpl(RemoteBlobStorage('affine-cloud'), AffineCloudBlobStorage, [
WorkspaceIdContext,
])
.addImpl(RemoteSyncStorage('affine-cloud'), AffineSyncStorage, [
WorkspaceIdContext,
CleanupService,
])
.addImpl(
AwarenessProvider('affine-cloud'),
AffineCloudAwarenessProvider,
[WorkspaceIdContext, AwarenessContext]
);
}
async getWorkspaceBlob(id: string, blobKey: string): Promise<Blob | null> {
// try to get blob from local storage first
const localBlobStorage = createLocalBlobStorage(id);
const localBlobStorage = environment.isDesktop
? new SQLiteBlobStorage(id)
: new IndexedDBBlobStorage(id);
const localBlob = await localBlobStorage.get(blobKey);
if (localBlob) {
return localBlob;
}
const blobStorage = createAffineCloudBlobStorage(id);
const blobStorage = new AffineCloudBlobStorage(id);
return await blobStorage.get(blobKey);
},
};
}
}