refactor(server): move bin content parser to doc reader (#10302)
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { User, Workspace } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import { Doc as YDoc } from 'yjs';
|
||||
|
||||
import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
|
||||
import { AppModule } from '../../../app.module';
|
||||
import { Config } from '../../../base';
|
||||
import { ConfigModule } from '../../../base/config';
|
||||
import { Models } from '../../../models';
|
||||
import { PgWorkspaceDocStorageAdapter } from '../../doc';
|
||||
import { DocContentService } from '..';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
models: Models;
|
||||
app: TestingApp;
|
||||
docContentService: DocContentService;
|
||||
config: Config;
|
||||
adapter: PgWorkspaceDocStorageAdapter;
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
flavor: {
|
||||
doc: false,
|
||||
},
|
||||
docService: {
|
||||
endpoint: '',
|
||||
},
|
||||
}),
|
||||
AppModule,
|
||||
],
|
||||
});
|
||||
|
||||
t.context.models = app.get(Models);
|
||||
t.context.docContentService = app.get(DocContentService);
|
||||
t.context.config = app.get(Config);
|
||||
t.context.adapter = app.get(PgWorkspaceDocStorageAdapter);
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
let user: User;
|
||||
let workspace: Workspace;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
t.context.config.docService.endpoint = t.context.app.url();
|
||||
await t.context.app.initTestingDB();
|
||||
user = await t.context.models.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
workspace = await t.context.models.workspace.create(user.id);
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should get doc content from doc service rpc', async t => {
|
||||
const docId = randomUUID();
|
||||
const { docContentService } = t.context;
|
||||
|
||||
const doc = new YDoc();
|
||||
const text = doc.getText('content');
|
||||
const updates: Buffer[] = [];
|
||||
|
||||
doc.on('update', update => {
|
||||
updates.push(Buffer.from(update));
|
||||
});
|
||||
|
||||
text.insert(0, 'hello');
|
||||
text.insert(5, 'world');
|
||||
text.insert(5, ' ');
|
||||
|
||||
await t.context.adapter.pushDocUpdates(workspace.id, docId, updates, user.id);
|
||||
|
||||
const docContent = await docContentService.getPageContent(
|
||||
workspace.id,
|
||||
docId
|
||||
);
|
||||
// TODO(@fengmk2): should create a test ydoc with blocks
|
||||
t.is(docContent, null);
|
||||
});
|
||||
@@ -8,8 +8,8 @@ import isMobile from 'is-mobile';
|
||||
import { Config, metrics, URLHelper } from '../../base';
|
||||
import { htmlSanitize } from '../../native';
|
||||
import { Public } from '../auth';
|
||||
import { DocReader } from '../doc';
|
||||
import { PermissionService } from '../permission';
|
||||
import { DocContentService } from './service';
|
||||
|
||||
interface RenderOptions {
|
||||
title: string;
|
||||
@@ -50,7 +50,7 @@ export class DocRendererController {
|
||||
private readonly mobileAssets: HtmlAssets = defaultAssets;
|
||||
|
||||
constructor(
|
||||
private readonly doc: DocContentService,
|
||||
private readonly doc: DocReader,
|
||||
private readonly permission: PermissionService,
|
||||
private readonly config: Config,
|
||||
private readonly url: URLHelper
|
||||
@@ -114,7 +114,7 @@ export class DocRendererController {
|
||||
}
|
||||
|
||||
if (allowUrlPreview) {
|
||||
return this.doc.getPageContent(workspaceId, docId);
|
||||
return this.doc.getDocContent(workspaceId, docId);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { FactoryProvider } from '@nestjs/common';
|
||||
|
||||
import { Config, OnEvent } from '../../base';
|
||||
import { DocContentService } from './service';
|
||||
|
||||
class DocEventsListener {
|
||||
constructor(private readonly doc: DocContentService) {}
|
||||
|
||||
@OnEvent('doc.snapshot.updated')
|
||||
async handleDocSnapshotUpdated({
|
||||
workspaceId,
|
||||
docId,
|
||||
}: Events['doc.snapshot.updated']) {
|
||||
await this.doc.markDocContentCacheStale(workspaceId, docId);
|
||||
}
|
||||
}
|
||||
|
||||
export const DocEventsListenerProvider: FactoryProvider = {
|
||||
provide: DocEventsListener,
|
||||
useFactory: (config: Config, doc: DocContentService) => {
|
||||
if (config.flavor.renderer) {
|
||||
return new DocEventsListener(doc);
|
||||
}
|
||||
return;
|
||||
},
|
||||
inject: [Config, DocContentService],
|
||||
};
|
||||
@@ -3,15 +3,9 @@ import { Module } from '@nestjs/common';
|
||||
import { DocStorageModule } from '../doc';
|
||||
import { PermissionModule } from '../permission';
|
||||
import { DocRendererController } from './controller';
|
||||
import { DocEventsListenerProvider } from './event';
|
||||
import { DocContentService } from './service';
|
||||
|
||||
@Module({
|
||||
imports: [DocStorageModule, PermissionModule],
|
||||
providers: [DocContentService, DocEventsListenerProvider],
|
||||
controllers: [DocRendererController],
|
||||
exports: [DocContentService],
|
||||
})
|
||||
export class DocRendererModule {}
|
||||
|
||||
export { DocContentService };
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
import { Cache } from '../../base';
|
||||
import { DocReader } from '../doc';
|
||||
import {
|
||||
type PageDocContent,
|
||||
parsePageDoc,
|
||||
parseWorkspaceDoc,
|
||||
type WorkspaceDocContent,
|
||||
} from '../utils/blocksuite';
|
||||
|
||||
@Injectable()
|
||||
export class DocContentService {
|
||||
constructor(
|
||||
private readonly cache: Cache,
|
||||
private readonly docReader: DocReader
|
||||
) {}
|
||||
|
||||
async getPageContent(
|
||||
workspaceId: string,
|
||||
guid: string
|
||||
): Promise<PageDocContent | null> {
|
||||
const cacheKey = `workspace:${workspaceId}:doc:${guid}:content`;
|
||||
const cachedResult = await this.cache.get<PageDocContent>(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const docRecord = await this.docReader.getDoc(workspaceId, guid);
|
||||
if (!docRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = new Doc();
|
||||
applyUpdate(doc, docRecord.bin);
|
||||
|
||||
const content = parsePageDoc(doc);
|
||||
|
||||
if (content) {
|
||||
await this.cache.set(cacheKey, content, {
|
||||
ttl:
|
||||
7 *
|
||||
24 *
|
||||
60 *
|
||||
60 *
|
||||
1000 /* TODO(@forehalo): we need time constants helper */,
|
||||
});
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async getWorkspaceContent(
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceDocContent | null> {
|
||||
const cacheKey = `workspace:${workspaceId}:content`;
|
||||
const cachedResult = await this.cache.get<WorkspaceDocContent>(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const docRecord = await this.docReader.getDoc(workspaceId, workspaceId);
|
||||
if (!docRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = new Doc();
|
||||
applyUpdate(doc, docRecord.bin);
|
||||
|
||||
const content = parseWorkspaceDoc(doc);
|
||||
|
||||
if (content) {
|
||||
await this.cache.set(cacheKey, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
async markDocContentCacheStale(workspaceId: string, docId: string) {
|
||||
const key =
|
||||
workspaceId === docId
|
||||
? `workspace:${workspaceId}:content`
|
||||
: `workspace:${workspaceId}:doc:${docId}:content`;
|
||||
await this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user