mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 06:18:45 +08:00
chore(server): move server tests folder (#9614)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { mock } from 'node:test';
|
||||
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
import * as Sinon from 'sinon';
|
||||
|
||||
import { Config } from '../../base/config';
|
||||
import { DocStorageModule } from '../../core/doc';
|
||||
import { DocStorageCronJob } from '../../core/doc/job';
|
||||
import { createTestingModule } from '../utils';
|
||||
|
||||
let m: TestingModule;
|
||||
let timer: Sinon.SinonFakeTimers;
|
||||
let db: PrismaClient;
|
||||
|
||||
// cleanup database before each test
|
||||
test.before(async () => {
|
||||
timer = Sinon.useFakeTimers({
|
||||
toFake: ['setInterval'],
|
||||
});
|
||||
m = await createTestingModule({
|
||||
imports: [ScheduleModule.forRoot(), DocStorageModule],
|
||||
});
|
||||
|
||||
db = m.get(PrismaClient);
|
||||
});
|
||||
|
||||
test.after.always(async () => {
|
||||
await m.close();
|
||||
timer.restore();
|
||||
});
|
||||
|
||||
test('should poll when intervel due', async t => {
|
||||
const manager = m.get(DocStorageCronJob);
|
||||
const interval = m.get(Config).doc.manager.updatePollInterval;
|
||||
|
||||
let resolve: any;
|
||||
const fake = mock.method(manager, 'autoMergePendingDocUpdates', () => {
|
||||
return new Promise(_resolve => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
});
|
||||
|
||||
timer.tick(interval);
|
||||
t.is(fake.mock.callCount(), 1);
|
||||
|
||||
// busy
|
||||
timer.tick(interval);
|
||||
// @ts-expect-error private member
|
||||
t.is(manager.busy, true);
|
||||
t.is(fake.mock.callCount(), 1);
|
||||
|
||||
resolve();
|
||||
await timer.tickAsync(1);
|
||||
|
||||
// @ts-expect-error private member
|
||||
t.is(manager.busy, false);
|
||||
timer.tick(interval);
|
||||
t.is(fake.mock.callCount(), 2);
|
||||
});
|
||||
|
||||
test('should be able to cleanup expired history', async t => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// insert expired data
|
||||
await db.snapshotHistory.createMany({
|
||||
data: Array.from({ length: 10 })
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
workspaceId: '1',
|
||||
id: '1',
|
||||
blob: Buffer.from([1, 1]),
|
||||
timestamp: new Date(timestamp - 10 - i),
|
||||
expiredAt: new Date(timestamp - 1),
|
||||
})),
|
||||
});
|
||||
|
||||
// insert available data
|
||||
await db.snapshotHistory.createMany({
|
||||
data: Array.from({ length: 10 })
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
workspaceId: '1',
|
||||
id: '1',
|
||||
blob: Buffer.from([1, 1]),
|
||||
timestamp: new Date(timestamp + i),
|
||||
expiredAt: new Date(timestamp + 1000),
|
||||
})),
|
||||
});
|
||||
|
||||
let count = await db.snapshotHistory.count();
|
||||
t.is(count, 20);
|
||||
|
||||
await m.get(DocStorageCronJob).cleanupExpiredHistory();
|
||||
|
||||
count = await db.snapshotHistory.count();
|
||||
t.is(count, 10);
|
||||
|
||||
const example = await db.snapshotHistory.findFirst();
|
||||
t.truthy(example);
|
||||
t.true(example!.expiredAt > new Date());
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import type { Snapshot } from '@prisma/client';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
import * as Sinon from 'sinon';
|
||||
|
||||
import { DocStorageModule, PgWorkspaceDocStorageAdapter } from '../../core/doc';
|
||||
import { DocStorageOptions } from '../../core/doc/options';
|
||||
import { DocRecord } from '../../core/doc/storage';
|
||||
import { createTestingModule, initTestingDB } from '../utils';
|
||||
|
||||
let m: TestingModule;
|
||||
let adapter: PgWorkspaceDocStorageAdapter;
|
||||
let db: PrismaClient;
|
||||
|
||||
// cleanup database before each test
|
||||
test.before(async () => {
|
||||
m = await createTestingModule({
|
||||
imports: [DocStorageModule],
|
||||
});
|
||||
|
||||
adapter = m.get(PgWorkspaceDocStorageAdapter);
|
||||
db = m.get(PrismaClient);
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await initTestingDB(db);
|
||||
const options = m.get(DocStorageOptions);
|
||||
Sinon.stub(options, 'historyMaxAge').resolves(1000);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
Sinon.restore();
|
||||
});
|
||||
|
||||
test.after.always(async () => {
|
||||
await m.close();
|
||||
});
|
||||
|
||||
const snapshot: Snapshot = {
|
||||
workspaceId: '1',
|
||||
id: 'doc1',
|
||||
blob: Buffer.from([1, 0]),
|
||||
state: Buffer.from([0]),
|
||||
seq: 0,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
};
|
||||
|
||||
function getSnapshot(timestamp: number = Date.now()): DocRecord {
|
||||
return {
|
||||
spaceId: snapshot.workspaceId,
|
||||
docId: snapshot.id,
|
||||
bin: snapshot.blob,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
test('should create doc history if never created before', async t => {
|
||||
// @ts-expect-error private method
|
||||
Sinon.stub(adapter, 'lastDocHistory').resolves(null);
|
||||
|
||||
const timestamp = Date.now();
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(timestamp));
|
||||
|
||||
const history = await db.snapshotHistory.findFirst({
|
||||
where: {
|
||||
workspaceId: '1',
|
||||
id: 'doc1',
|
||||
},
|
||||
});
|
||||
|
||||
t.truthy(history);
|
||||
t.is(history?.timestamp.getTime(), timestamp);
|
||||
});
|
||||
|
||||
test('should not create history if timestamp equals to last record', async t => {
|
||||
const timestamp = new Date();
|
||||
|
||||
// @ts-expect-error private method
|
||||
Sinon.stub(adapter, 'lastDocHistory').resolves({ timestamp, state: null });
|
||||
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(timestamp));
|
||||
|
||||
const history = await db.snapshotHistory.findFirst({
|
||||
where: {
|
||||
workspaceId: '1',
|
||||
id: 'doc1',
|
||||
},
|
||||
});
|
||||
|
||||
t.falsy(history);
|
||||
});
|
||||
|
||||
test('should not create history if time diff is less than interval config', async t => {
|
||||
const timestamp = new Date();
|
||||
|
||||
// @ts-expect-error private method
|
||||
Sinon.stub(adapter, 'lastDocHistory').resolves({
|
||||
timestamp: new Date(timestamp.getTime() - 1000),
|
||||
state: Buffer.from([0, 1]),
|
||||
});
|
||||
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(timestamp));
|
||||
|
||||
const history = await db.snapshotHistory.findFirst({
|
||||
where: {
|
||||
workspaceId: '1',
|
||||
id: 'doc1',
|
||||
},
|
||||
});
|
||||
|
||||
t.falsy(history);
|
||||
});
|
||||
|
||||
test('should create history if time diff is larger than interval config and state diff', async t => {
|
||||
const timestamp = new Date();
|
||||
|
||||
// @ts-expect-error private method
|
||||
Sinon.stub(adapter, 'lastDocHistory').resolves({
|
||||
timestamp: new Date(timestamp.getTime() - 1000 * 60 * 20),
|
||||
state: Buffer.from([0, 1]),
|
||||
});
|
||||
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(timestamp));
|
||||
|
||||
const history = await db.snapshotHistory.findFirst({
|
||||
where: {
|
||||
workspaceId: '1',
|
||||
id: 'doc1',
|
||||
},
|
||||
});
|
||||
|
||||
t.truthy(history);
|
||||
});
|
||||
|
||||
test('should create history with force flag even if time diff in small', async t => {
|
||||
const timestamp = new Date();
|
||||
|
||||
// @ts-expect-error private method
|
||||
Sinon.stub(adapter, 'lastDocHistory').resolves({
|
||||
timestamp: new Date(timestamp.getTime() - 1),
|
||||
state: Buffer.from([0, 1]),
|
||||
});
|
||||
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(timestamp), true);
|
||||
|
||||
const history = await db.snapshotHistory.findFirst({
|
||||
where: {
|
||||
workspaceId: '1',
|
||||
id: 'doc1',
|
||||
},
|
||||
});
|
||||
|
||||
t.truthy(history);
|
||||
});
|
||||
|
||||
test('should correctly list all history records', async t => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// insert expired data
|
||||
await db.snapshotHistory.createMany({
|
||||
data: Array.from({ length: 10 })
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
workspaceId: snapshot.workspaceId,
|
||||
id: snapshot.id,
|
||||
blob: snapshot.blob,
|
||||
state: snapshot.state,
|
||||
timestamp: new Date(timestamp - 10 - i),
|
||||
expiredAt: new Date(timestamp - 1),
|
||||
})),
|
||||
});
|
||||
|
||||
// insert available data
|
||||
await db.snapshotHistory.createMany({
|
||||
data: Array.from({ length: 10 })
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
workspaceId: snapshot.workspaceId,
|
||||
id: snapshot.id,
|
||||
blob: snapshot.blob,
|
||||
state: snapshot.state,
|
||||
timestamp: new Date(timestamp + i),
|
||||
expiredAt: new Date(timestamp + 1000),
|
||||
})),
|
||||
});
|
||||
|
||||
const list = await adapter.listDocHistories(
|
||||
snapshot.workspaceId,
|
||||
snapshot.id,
|
||||
{ before: timestamp + 20, limit: 8 }
|
||||
);
|
||||
const count = await db.snapshotHistory.count();
|
||||
|
||||
t.is(list.length, 8);
|
||||
t.is(count, 20);
|
||||
});
|
||||
|
||||
test('should be able to get history data', async t => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(timestamp), true);
|
||||
|
||||
const history = await adapter.getDocHistory(
|
||||
snapshot.workspaceId,
|
||||
snapshot.id,
|
||||
timestamp
|
||||
);
|
||||
|
||||
t.truthy(history);
|
||||
t.deepEqual(history?.bin, snapshot.blob);
|
||||
});
|
||||
|
||||
test('should be able to get last history record', async t => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// insert available data
|
||||
await db.snapshotHistory.createMany({
|
||||
data: Array.from({ length: 10 })
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
workspaceId: snapshot.workspaceId,
|
||||
id: snapshot.id,
|
||||
blob: snapshot.blob,
|
||||
state: snapshot.state,
|
||||
timestamp: new Date(timestamp + i),
|
||||
expiredAt: new Date(timestamp + 1000),
|
||||
})),
|
||||
});
|
||||
|
||||
// @ts-expect-error private method
|
||||
const history = await adapter.lastDocHistory(
|
||||
snapshot.workspaceId,
|
||||
snapshot.id
|
||||
);
|
||||
|
||||
t.truthy(history);
|
||||
t.is(history?.timestamp.getTime(), timestamp + 9);
|
||||
});
|
||||
|
||||
test('should be able to recover from history', async t => {
|
||||
await db.snapshot.create({
|
||||
data: {
|
||||
...snapshot,
|
||||
blob: Buffer.from([1, 1]),
|
||||
state: Buffer.from([1, 1]),
|
||||
},
|
||||
});
|
||||
const history1Timestamp = snapshot.updatedAt.getTime() - 10;
|
||||
|
||||
// @ts-expect-error private method
|
||||
await adapter.createDocHistory(getSnapshot(history1Timestamp));
|
||||
|
||||
await adapter.rollbackDoc(
|
||||
snapshot.workspaceId,
|
||||
snapshot.id,
|
||||
history1Timestamp
|
||||
);
|
||||
|
||||
const [history1, history2] = await db.snapshotHistory.findMany({
|
||||
where: {
|
||||
workspaceId: snapshot.workspaceId,
|
||||
id: snapshot.id,
|
||||
},
|
||||
});
|
||||
|
||||
t.is(history1.timestamp.getTime(), history1Timestamp);
|
||||
t.is(history2.timestamp.getTime(), snapshot.updatedAt.getTime());
|
||||
|
||||
// new history data force created with snapshot state before recovered
|
||||
t.deepEqual(history2.blob, Buffer.from([1, 1]));
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { Package } from '@affine-tools/utils/workspace';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { DocRendererModule } from '../../core/doc-renderer';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
db: PrismaClient;
|
||||
}>;
|
||||
|
||||
const mobileUAString =
|
||||
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36';
|
||||
|
||||
function initTestStaticFiles(staticPath: string) {
|
||||
const files = {
|
||||
'main.a.js': `const name = 'affine'`,
|
||||
'assets-manifest.json': JSON.stringify({
|
||||
js: ['main.a.js'],
|
||||
css: [],
|
||||
publicPath: 'https://app.affine.pro/',
|
||||
gitHash: '',
|
||||
description: '',
|
||||
}),
|
||||
'admin/main.b.js': `const name = 'affine-admin'`,
|
||||
'mobile/main.c.js': `const name = 'affine-mobile'`,
|
||||
'mobile/assets-manifest.json': JSON.stringify({
|
||||
js: ['main.c.js'],
|
||||
css: [],
|
||||
publicPath: 'https://app.affine.pro/',
|
||||
gitHash: '',
|
||||
description: '',
|
||||
}),
|
||||
};
|
||||
|
||||
for (const [filename, content] of Object.entries(files)) {
|
||||
const filePath = path.join(staticPath, filename);
|
||||
mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
test.before('init selfhost server', async t => {
|
||||
const staticPath = new Package('@affine/server').join('static').value;
|
||||
initTestStaticFiles(staticPath);
|
||||
|
||||
const { app } = await createTestingApp({
|
||||
imports: [DocRendererModule],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
t.context.db = t.context.app.get(PrismaClient);
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should render correct html', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/workspace/xxxx/xxx')
|
||||
.expect(200);
|
||||
|
||||
t.true(
|
||||
res.text.includes(
|
||||
`<script src="https://app.affine.pro/main.a.js"></script>`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('should render correct mobile html', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/workspace/xxxx/xxx')
|
||||
.set('user-agent', mobileUAString)
|
||||
.expect(200);
|
||||
|
||||
t.true(
|
||||
res.text.includes(
|
||||
`<script src="https://app.affine.pro/main.c.js"></script>`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test.todo('should render correct page preview');
|
||||
@@ -0,0 +1,5 @@
|
||||
import test from 'ava';
|
||||
|
||||
test('should test through userspace', t => {
|
||||
t.pass();
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
import * as Sinon from 'sinon';
|
||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { ConfigModule } from '../../base/config';
|
||||
import {
|
||||
DocStorageModule,
|
||||
PgWorkspaceDocStorageAdapter as Adapter,
|
||||
} from '../../core/doc';
|
||||
import { createTestingModule, initTestingDB } from '../utils';
|
||||
|
||||
let m: TestingModule;
|
||||
let db: PrismaClient;
|
||||
let adapter: Adapter;
|
||||
|
||||
test.before('init testing module', async () => {
|
||||
m = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
doc: {
|
||||
manager: {
|
||||
enableUpdateAutoMerging: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DocStorageModule,
|
||||
],
|
||||
});
|
||||
db = m.get(PrismaClient);
|
||||
adapter = m.get(Adapter);
|
||||
// @ts-expect-error private method
|
||||
Sinon.stub(adapter, 'createDocHistory');
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await initTestingDB(db);
|
||||
});
|
||||
|
||||
test.after.always(async () => {
|
||||
await m?.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated `seq` would be removed
|
||||
*/
|
||||
test('should have sequential update number', async t => {
|
||||
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 adapter.pushDocUpdates('2', '2', updates);
|
||||
|
||||
// [1,2,3]
|
||||
let records = await db.update.findMany({
|
||||
where: {
|
||||
workspaceId: '2',
|
||||
id: '2',
|
||||
},
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
records.map(({ seq }) => seq),
|
||||
[1, 2, 3]
|
||||
);
|
||||
|
||||
// merge
|
||||
await adapter.getDoc('2', '2');
|
||||
|
||||
// fake the seq num is about to overflow
|
||||
await db.snapshot.update({
|
||||
where: {
|
||||
workspaceId_id: {
|
||||
id: '2',
|
||||
workspaceId: '2',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
seq: 0x3ffffffe,
|
||||
},
|
||||
});
|
||||
|
||||
await adapter.pushDocUpdates('2', '2', updates);
|
||||
|
||||
records = await db.update.findMany({
|
||||
where: {
|
||||
workspaceId: '2',
|
||||
id: '2',
|
||||
},
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
records.map(({ seq }) => seq),
|
||||
[0x3ffffffe + 1, 0x3ffffffe + 2, 0x3ffffffe + 3]
|
||||
);
|
||||
|
||||
// push a new update with new seq num
|
||||
await adapter.pushDocUpdates('2', '2', updates.slice(0, 1));
|
||||
|
||||
// let the manager ignore update with the new seq num
|
||||
// @ts-expect-error private method
|
||||
const stub = Sinon.stub(adapter, 'getDocUpdates').resolves(
|
||||
records.map(record => ({
|
||||
bin: record.blob,
|
||||
timestamp: record.createdAt.getTime(),
|
||||
}))
|
||||
);
|
||||
|
||||
await adapter.getDoc('2', '2');
|
||||
stub.restore();
|
||||
|
||||
// should not merge in one run
|
||||
t.not(await db.update.count(), 0);
|
||||
});
|
||||
|
||||
test('should retry if failed to insert updates', async t => {
|
||||
const stub = Sinon.stub();
|
||||
const createMany = db.update.createMany;
|
||||
db.update.createMany = stub;
|
||||
|
||||
stub.onCall(0).rejects(new Error());
|
||||
stub.onCall(1).resolves();
|
||||
|
||||
await t.notThrowsAsync(() =>
|
||||
adapter.pushDocUpdates('1', '1', [Buffer.from([0, 0])])
|
||||
);
|
||||
t.is(stub.callCount, 2);
|
||||
|
||||
stub.reset();
|
||||
db.update.createMany = createMany;
|
||||
});
|
||||
|
||||
test('should throw if meet max retry times', async t => {
|
||||
const stub = Sinon.stub();
|
||||
const createMany = db.update.createMany;
|
||||
db.update.createMany = stub;
|
||||
|
||||
stub.rejects(new Error());
|
||||
|
||||
await t.throwsAsync(
|
||||
() => adapter.pushDocUpdates('1', '1', [Buffer.from([0, 0])]),
|
||||
{ message: 'Failed to store doc updates.' }
|
||||
);
|
||||
t.is(stub.callCount, 4);
|
||||
|
||||
stub.reset();
|
||||
db.update.createMany = createMany;
|
||||
});
|
||||
|
||||
test('should be able to merge updates as snapshot', async t => {
|
||||
const doc = new YDoc();
|
||||
const text = doc.getText('content');
|
||||
text.insert(0, 'hello');
|
||||
const update = encodeStateAsUpdate(doc);
|
||||
|
||||
await db.workspace.create({
|
||||
data: {
|
||||
id: '1',
|
||||
public: false,
|
||||
},
|
||||
});
|
||||
|
||||
await db.update.createMany({
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
workspaceId: '1',
|
||||
blob: Buffer.from(update),
|
||||
seq: 1,
|
||||
createdAt: new Date(Date.now() + 1),
|
||||
createdBy: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
Buffer.from((await adapter.getDoc('1', '1'))!.bin),
|
||||
Buffer.from(update)
|
||||
);
|
||||
|
||||
let appendUpdate = Buffer.from([]);
|
||||
doc.on('update', update => {
|
||||
appendUpdate = Buffer.from(update);
|
||||
});
|
||||
text.insert(5, 'world');
|
||||
|
||||
await db.update.create({
|
||||
data: {
|
||||
workspaceId: '1',
|
||||
id: '1',
|
||||
blob: appendUpdate,
|
||||
seq: 2,
|
||||
createdAt: new Date(),
|
||||
createdBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
const { bin } = (await adapter.getDoc('1', '1'))!;
|
||||
const dbDoc = new YDoc();
|
||||
applyUpdate(dbDoc, bin);
|
||||
|
||||
t.is(dbDoc.getText('content').toString(), 'helloworld');
|
||||
t.deepEqual(encodeStateAsUpdate(dbDoc), encodeStateAsUpdate(doc));
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to merge updates into snapshot', async t => {
|
||||
const updates: Buffer[] = [];
|
||||
{
|
||||
const doc = new YDoc();
|
||||
doc.on('update', data => {
|
||||
updates.push(Buffer.from(data));
|
||||
});
|
||||
|
||||
const text = doc.getText('content');
|
||||
text.insert(0, 'hello');
|
||||
text.insert(5, 'world');
|
||||
text.insert(5, ' ');
|
||||
text.insert(11, '!');
|
||||
}
|
||||
|
||||
{
|
||||
await adapter.pushDocUpdates('1', '1', updates.slice(0, 2));
|
||||
// merge
|
||||
const { bin } = (await adapter.getDoc('1', '1'))!;
|
||||
const doc = new YDoc();
|
||||
applyUpdate(doc, bin);
|
||||
|
||||
t.is(doc.getText('content').toString(), 'helloworld');
|
||||
}
|
||||
|
||||
{
|
||||
await adapter.pushDocUpdates('1', '1', updates.slice(2));
|
||||
// merge
|
||||
const { bin } = (await adapter.getDoc('1', '1'))!;
|
||||
const doc = new YDoc();
|
||||
applyUpdate(doc, bin);
|
||||
|
||||
t.is(doc.getText('content').toString(), 'hello world!');
|
||||
}
|
||||
|
||||
t.is(await db.update.count(), 0);
|
||||
});
|
||||
|
||||
test('should not update snapshot if doc is outdated', async t => {
|
||||
const updates: Buffer[] = [];
|
||||
{
|
||||
const doc = new YDoc();
|
||||
doc.on('update', data => {
|
||||
updates.push(Buffer.from(data));
|
||||
});
|
||||
|
||||
const text = doc.getText('content');
|
||||
text.insert(0, 'hello');
|
||||
text.insert(5, 'world');
|
||||
text.insert(5, ' ');
|
||||
text.insert(11, '!');
|
||||
}
|
||||
|
||||
await adapter.pushDocUpdates('2', '1', updates.slice(0, 2)); // 'helloworld'
|
||||
// merge
|
||||
await adapter.getDoc('2', '1');
|
||||
// fake the snapshot is a lot newer
|
||||
await db.snapshot.update({
|
||||
where: {
|
||||
workspaceId_id: {
|
||||
workspaceId: '2',
|
||||
id: '1',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
updatedAt: new Date(Date.now() + 10000),
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
await adapter.pushDocUpdates('2', '1', updates.slice(2)); // 'hello world!'
|
||||
const { bin } = (await adapter.getDoc('2', '1'))!;
|
||||
|
||||
// all updated will merged into doc not matter it's timestamp is outdated or not,
|
||||
// but the snapshot record will not be updated
|
||||
const doc = new YDoc();
|
||||
applyUpdate(doc, bin);
|
||||
t.is(doc.getText('content').toString(), 'hello world!');
|
||||
}
|
||||
|
||||
{
|
||||
const doc = new YDoc();
|
||||
applyUpdate(doc, (await adapter.getDoc('2', '1'))!.bin);
|
||||
// the snapshot will not get touched if the new doc's timestamp is outdated
|
||||
t.is(doc.getText('content').toString(), 'helloworld');
|
||||
|
||||
// the updates are known as outdated, so they will be deleted
|
||||
t.is(await db.update.count(), 0);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user