mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-19 00:10:39 +08:00
refactor(server): use userDoc model on PgUserspaceDocStorageAdapter (#9845)
close CLOUD-104
This commit is contained in:
@@ -1,5 +1,145 @@
|
|||||||
import test from 'ava';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
test('should test through userspace', t => {
|
import ava, { TestFn } from 'ava';
|
||||||
t.pass();
|
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
|
import { ConfigModule } from '../../base/config';
|
||||||
|
import {
|
||||||
|
DocStorageModule,
|
||||||
|
PgUserspaceDocStorageAdapter as Adapter,
|
||||||
|
} from '../../core/doc';
|
||||||
|
import { Models, type User } from '../../models';
|
||||||
|
import { createTestingModule, type TestingModule } from '../utils';
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
module: TestingModule;
|
||||||
|
models: Models;
|
||||||
|
adapter: Adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = ava as TestFn<Context>;
|
||||||
|
|
||||||
|
test.before(async t => {
|
||||||
|
const module = await createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
doc: {
|
||||||
|
manager: {
|
||||||
|
enableUpdateAutoMerging: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DocStorageModule,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
t.context.models = module.get(Models);
|
||||||
|
t.context.adapter = module.get(Adapter);
|
||||||
|
t.context.module = module;
|
||||||
|
});
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
test.beforeEach(async t => {
|
||||||
|
await t.context.module.initTestingDB();
|
||||||
|
user = await t.context.models.user.create({
|
||||||
|
email: 'test@affine.pro',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async t => {
|
||||||
|
await t.context.module.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should push user doc updates work', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const doc = new YDoc();
|
||||||
|
const text = doc.getText('content');
|
||||||
|
const updates: Uint8Array[] = [];
|
||||||
|
|
||||||
|
doc.on('update', update => {
|
||||||
|
updates.push(update);
|
||||||
|
});
|
||||||
|
|
||||||
|
text.insert(0, 'hello');
|
||||||
|
text.insert(5, 'world');
|
||||||
|
text.insert(5, ' ');
|
||||||
|
|
||||||
|
let timestamp = await t.context.adapter.pushDocUpdates(
|
||||||
|
user.id,
|
||||||
|
docId,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
t.truthy(timestamp);
|
||||||
|
|
||||||
|
let record = await t.context.adapter.getDoc(user.id, docId);
|
||||||
|
const newDoc = new YDoc();
|
||||||
|
applyUpdate(newDoc, record!.bin);
|
||||||
|
|
||||||
|
t.is(newDoc.getText('content').toString(), 'hello world');
|
||||||
|
// find all timestamps
|
||||||
|
const timestamps = await t.context.adapter.getSpaceDocTimestamps(user.id);
|
||||||
|
t.deepEqual(timestamps, {
|
||||||
|
[docId]: timestamp,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete user doc work', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const doc = new YDoc();
|
||||||
|
const text = doc.getText('content');
|
||||||
|
const updates: Uint8Array[] = [];
|
||||||
|
|
||||||
|
doc.on('update', update => {
|
||||||
|
updates.push(update);
|
||||||
|
});
|
||||||
|
|
||||||
|
text.insert(0, 'hello');
|
||||||
|
text.insert(5, 'world');
|
||||||
|
text.insert(5, ' ');
|
||||||
|
|
||||||
|
let timestamp = await t.context.adapter.pushDocUpdates(
|
||||||
|
user.id,
|
||||||
|
docId,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
t.truthy(timestamp);
|
||||||
|
|
||||||
|
let record = await t.context.adapter.getDoc(user.id, docId);
|
||||||
|
t.truthy(record);
|
||||||
|
|
||||||
|
await t.context.adapter.deleteDoc(user.id, docId);
|
||||||
|
|
||||||
|
record = await t.context.adapter.getDoc(user.id, docId);
|
||||||
|
t.falsy(record);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete all user docs work', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const doc = new YDoc();
|
||||||
|
const text = doc.getText('content');
|
||||||
|
const updates: Uint8Array[] = [];
|
||||||
|
|
||||||
|
doc.on('update', update => {
|
||||||
|
updates.push(update);
|
||||||
|
});
|
||||||
|
|
||||||
|
text.insert(0, 'hello');
|
||||||
|
text.insert(5, 'world');
|
||||||
|
text.insert(5, ' ');
|
||||||
|
|
||||||
|
let timestamp = await t.context.adapter.pushDocUpdates(
|
||||||
|
user.id,
|
||||||
|
docId,
|
||||||
|
updates
|
||||||
|
);
|
||||||
|
t.truthy(timestamp);
|
||||||
|
|
||||||
|
let record = await t.context.adapter.getDoc(user.id, docId);
|
||||||
|
t.truthy(record);
|
||||||
|
|
||||||
|
await t.context.adapter.deleteSpace(user.id);
|
||||||
|
|
||||||
|
record = await t.context.adapter.getDoc(user.id, docId);
|
||||||
|
t.falsy(record);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
import { Mutex } from '../../../base';
|
import { Mutex } from '../../../base';
|
||||||
|
import { Models } from '../../../models';
|
||||||
import { DocStorageOptions } from '../options';
|
import { DocStorageOptions } from '../options';
|
||||||
import { DocRecord, DocStorageAdapter } from '../storage';
|
import { DocRecord, DocStorageAdapter } from '../storage';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: PrismaClient,
|
|
||||||
private readonly mutex: Mutex,
|
private readonly mutex: Mutex,
|
||||||
|
private readonly models: Models,
|
||||||
options: DocStorageOptions
|
options: DocStorageOptions
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -42,7 +42,7 @@ export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async getDoc(spaceId: string, docId: string) {
|
override async getDoc(spaceId: string, docId: string) {
|
||||||
return this.getDocSnapshot(spaceId, docId);
|
return await this.getDocSnapshot(spaceId, docId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushDocUpdates(
|
async pushDocUpdates(
|
||||||
@@ -79,103 +79,46 @@ export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteDoc(userId: string, docId: string) {
|
async deleteDoc(userId: string, docId: string) {
|
||||||
await this.db.userSnapshot.deleteMany({
|
await this.models.userDoc.delete(userId, docId);
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
id: docId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSpace(userId: string) {
|
async deleteSpace(userId: string) {
|
||||||
await this.db.userSnapshot.deleteMany({
|
await this.models.userDoc.deleteAllByUserId(userId);
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSpaceDocTimestamps(userId: string, after?: number) {
|
async getSpaceDocTimestamps(userId: string, after?: number) {
|
||||||
const snapshots = await this.db.userSnapshot.findMany({
|
return await this.models.userDoc.findTimestampsByUserId(userId, after);
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
updatedAt: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
...(after
|
|
||||||
? {
|
|
||||||
updatedAt: {
|
|
||||||
gt: new Date(after),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: Record<string, number> = {};
|
|
||||||
|
|
||||||
snapshots.forEach(s => {
|
|
||||||
result[s.id] = s.updatedAt.getTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getDocSnapshot(userId: string, docId: string) {
|
protected async getDocSnapshot(userId: string, docId: string) {
|
||||||
const snapshot = await this.db.userSnapshot.findUnique({
|
const snapshot = await this.models.userDoc.get(userId, docId);
|
||||||
where: {
|
|
||||||
userId_id: {
|
|
||||||
userId,
|
|
||||||
id: docId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
spaceId: userId,
|
spaceId: snapshot.spaceId,
|
||||||
docId,
|
docId: snapshot.docId,
|
||||||
bin: snapshot.blob,
|
bin: snapshot.blob,
|
||||||
timestamp: snapshot.updatedAt.getTime(),
|
timestamp: snapshot.timestamp,
|
||||||
editor: snapshot.userId,
|
editor: snapshot.editorId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async setDocSnapshot(snapshot: DocRecord) {
|
protected async setDocSnapshot(snapshot: DocRecord) {
|
||||||
// we always get lock before writing to user snapshot table,
|
// we always get lock before writing to user snapshot table,
|
||||||
// so a simple upsert without testing on updatedAt is safe
|
// so a simple upsert without testing on updatedAt is safe
|
||||||
await this.db.userSnapshot.upsert({
|
await this.models.userDoc.upsert({
|
||||||
where: {
|
...snapshot,
|
||||||
userId_id: {
|
blob: Buffer.from(snapshot.bin),
|
||||||
userId: snapshot.spaceId,
|
|
||||||
id: snapshot.docId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
blob: Buffer.from(snapshot.bin),
|
|
||||||
updatedAt: new Date(snapshot.timestamp),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userId: snapshot.spaceId,
|
|
||||||
id: snapshot.docId,
|
|
||||||
blob: Buffer.from(snapshot.bin),
|
|
||||||
createdAt: new Date(snapshot.timestamp),
|
|
||||||
updatedAt: new Date(snapshot.timestamp),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async lockDocForUpdate(
|
protected override async lockDocForUpdate(spaceId: string, docId: string) {
|
||||||
workspaceId: string,
|
const lock = await this.mutex.acquire(`userspace:${spaceId}:${docId}`);
|
||||||
docId: string
|
|
||||||
) {
|
|
||||||
const lock = await this.mutex.acquire(`userspace:${workspaceId}:${docId}`);
|
|
||||||
|
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
throw new Error('Too many concurrent writings');
|
throw new Error('Too many concurrent writings');
|
||||||
|
|||||||
Reference in New Issue
Block a user