mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(server): cluster level event system (#9884)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
import { Cache, type EventPayload, OnEvent } from '../../base';
|
||||
import { Cache, OnEvent } from '../../base';
|
||||
import { PgWorkspaceDocStorageAdapter } from '../doc';
|
||||
import {
|
||||
type PageDocContent,
|
||||
@@ -78,15 +78,15 @@ export class DocContentService {
|
||||
return content;
|
||||
}
|
||||
|
||||
@OnEvent('snapshot.updated')
|
||||
@OnEvent('doc.snapshot.updated')
|
||||
async markDocContentCacheStale({
|
||||
workspaceId,
|
||||
id,
|
||||
}: EventPayload<'snapshot.updated'>) {
|
||||
docId,
|
||||
}: Events['doc.snapshot.updated']) {
|
||||
const key =
|
||||
workspaceId === id
|
||||
workspaceId === docId
|
||||
? `workspace:${workspaceId}:content`
|
||||
: `workspace:${workspaceId}:doc:${id}:content`;
|
||||
: `workspace:${workspaceId}:doc:${docId}:content`;
|
||||
await this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Cache,
|
||||
DocHistoryNotFound,
|
||||
DocNotFound,
|
||||
EventEmitter,
|
||||
EventBus,
|
||||
FailedToSaveUpdates,
|
||||
FailedToUpsertSnapshot,
|
||||
metrics,
|
||||
@@ -22,7 +22,18 @@ import {
|
||||
} from '../storage';
|
||||
|
||||
const UPDATES_QUEUE_CACHE_KEY = 'doc:manager:updates';
|
||||
|
||||
declare global {
|
||||
interface Events {
|
||||
'doc.snapshot.deleted': {
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
};
|
||||
'doc.snapshot.updated': {
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
@Injectable()
|
||||
export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
||||
private readonly logger = new Logger(PgWorkspaceDocStorageAdapter.name);
|
||||
@@ -31,7 +42,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
||||
private readonly db: PrismaClient,
|
||||
private readonly mutex: Mutex,
|
||||
private readonly cache: Cache,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly event: EventBus,
|
||||
protected override readonly options: DocStorageOptions
|
||||
) {
|
||||
super(options);
|
||||
@@ -470,9 +481,9 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
||||
const updatedSnapshot = result.at(0);
|
||||
|
||||
if (updatedSnapshot) {
|
||||
this.event.emit('snapshot.updated', {
|
||||
this.event.emit('doc.snapshot.updated', {
|
||||
workspaceId: snapshot.spaceId,
|
||||
id: snapshot.docId,
|
||||
docId: snapshot.docId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,7 @@ import { Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
|
||||
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import {
|
||||
CallMetric,
|
||||
Config,
|
||||
type EventPayload,
|
||||
metrics,
|
||||
OnEvent,
|
||||
} from '../../base';
|
||||
import { CallMetric, Config, metrics, OnEvent } from '../../base';
|
||||
import { PgWorkspaceDocStorageAdapter } from './adapters/workspace';
|
||||
|
||||
@Injectable()
|
||||
@@ -81,7 +75,7 @@ export class DocStorageCronJob implements OnModuleInit {
|
||||
}
|
||||
|
||||
@OnEvent('user.deleted')
|
||||
async clearUserWorkspaces(payload: EventPayload<'user.deleted'>) {
|
||||
async clearUserWorkspaces(payload: Events['user.deleted']) {
|
||||
for (const workspace of payload.ownedWorkspaces) {
|
||||
await this.workspace.deleteSpace(workspace);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type EventPayload, OnEvent, Runtime } from '../../base';
|
||||
import { Runtime } from '../../base';
|
||||
import { Models } from '../../models';
|
||||
import { FeatureService } from './service';
|
||||
import { FeatureType } from './types';
|
||||
@@ -167,9 +167,4 @@ export class FeatureManagementService {
|
||||
async listFeatureWorkspaces(feature: FeatureType) {
|
||||
return this.feature.listWorkspacesByFeature(feature);
|
||||
}
|
||||
|
||||
@OnEvent('user.admin.created')
|
||||
async onAdminUserCreated({ id }: EventPayload<'user.admin.created'>) {
|
||||
await this.addAdmin(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { groupBy } from 'lodash-es';
|
||||
|
||||
import {
|
||||
DocAccessDenied,
|
||||
EventEmitter,
|
||||
EventBus,
|
||||
SpaceAccessDenied,
|
||||
SpaceOwnerNotFound,
|
||||
} from '../../base';
|
||||
@@ -15,7 +15,7 @@ import { Permission, PublicPageMode } from './types';
|
||||
export class PermissionService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaClient,
|
||||
private readonly event: EventEmitter
|
||||
private readonly event: EventBus
|
||||
) {}
|
||||
|
||||
private get acceptedCondition() {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Request, Response } from 'express';
|
||||
|
||||
import {
|
||||
ActionForbidden,
|
||||
EventEmitter,
|
||||
InternalServerError,
|
||||
Mutex,
|
||||
PasswordRequired,
|
||||
@@ -24,7 +23,6 @@ export class CustomSetupController {
|
||||
constructor(
|
||||
private readonly models: Models,
|
||||
private readonly auth: AuthService,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly mutex: Mutex,
|
||||
private readonly server: ServerService,
|
||||
private readonly runtime: Runtime
|
||||
@@ -62,7 +60,6 @@ export class CustomSetupController {
|
||||
if (!lock) {
|
||||
throw new InternalServerError();
|
||||
}
|
||||
|
||||
const user = await this.models.user.create({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
@@ -70,7 +67,12 @@ export class CustomSetupController {
|
||||
});
|
||||
|
||||
try {
|
||||
await this.event.emitAsync('user.admin.created', user);
|
||||
await this.models.userFeature.add(
|
||||
user.id,
|
||||
'administrator',
|
||||
'selfhost setup'
|
||||
);
|
||||
|
||||
await this.auth.setCookies(req, res, user.id);
|
||||
res.send({ id: user.id, email: user.email, name: user.name });
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import type {
|
||||
BlobInputType,
|
||||
EventPayload,
|
||||
PutObjectMetadata,
|
||||
StorageProvider,
|
||||
} from '../../../base';
|
||||
@@ -47,7 +46,7 @@ export class AvatarStorage {
|
||||
}
|
||||
|
||||
@OnEvent('user.deleted')
|
||||
async onUserDeleted(user: EventPayload<'user.deleted'>) {
|
||||
async onUserDeleted(user: Events['user.deleted']) {
|
||||
if (user.avatarUrl) {
|
||||
await this.delete(user.avatarUrl);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import {
|
||||
autoMetadata,
|
||||
Config,
|
||||
EventEmitter,
|
||||
type EventPayload,
|
||||
EventBus,
|
||||
type GetObjectMetadata,
|
||||
ListObjectsMetadata,
|
||||
OnEvent,
|
||||
@@ -21,7 +20,7 @@ export class WorkspaceBlobStorage {
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly event: EventBus,
|
||||
private readonly storageFactory: StorageProviderFactory,
|
||||
private readonly db: PrismaClient
|
||||
) {
|
||||
@@ -105,7 +104,7 @@ export class WorkspaceBlobStorage {
|
||||
});
|
||||
|
||||
deletedBlobs.forEach(blob => {
|
||||
this.event.emit('workspace.blob.deleted', {
|
||||
this.event.emit('workspace.blob.delete', {
|
||||
workspaceId: workspaceId,
|
||||
key: blob.key,
|
||||
});
|
||||
@@ -152,10 +151,7 @@ export class WorkspaceBlobStorage {
|
||||
}
|
||||
|
||||
@OnEvent('workspace.blob.sync')
|
||||
async syncBlobMeta({
|
||||
workspaceId,
|
||||
key,
|
||||
}: EventPayload<'workspace.blob.sync'>) {
|
||||
async syncBlobMeta({ workspaceId, key }: Events['workspace.blob.sync']) {
|
||||
try {
|
||||
const meta = await this.provider.head(`${workspaceId}/${key}`);
|
||||
|
||||
@@ -176,23 +172,23 @@ export class WorkspaceBlobStorage {
|
||||
}
|
||||
|
||||
@OnEvent('workspace.deleted')
|
||||
async onWorkspaceDeleted(workspaceId: EventPayload<'workspace.deleted'>) {
|
||||
const blobs = await this.list(workspaceId);
|
||||
async onWorkspaceDeleted({ id }: Events['workspace.deleted']) {
|
||||
const blobs = await this.list(id);
|
||||
|
||||
// to reduce cpu time holding
|
||||
blobs.forEach(blob => {
|
||||
this.event.emit('workspace.blob.deleted', {
|
||||
workspaceId: workspaceId,
|
||||
this.event.emit('workspace.blob.delete', {
|
||||
workspaceId: id,
|
||||
key: blob.key,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent('workspace.blob.deleted')
|
||||
@OnEvent('workspace.blob.delete')
|
||||
async onDeleteWorkspaceBlob({
|
||||
workspaceId,
|
||||
key,
|
||||
}: EventPayload<'workspace.blob.deleted'>) {
|
||||
}: Events['workspace.blob.delete']) {
|
||||
await this.delete(workspaceId, key, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { Config, type EventPayload, OnEvent } from '../../base';
|
||||
import { Config, OnEvent } from '../../base';
|
||||
|
||||
@Injectable()
|
||||
export class UserEventsListener {
|
||||
@@ -9,7 +9,7 @@ export class UserEventsListener {
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
@OnEvent('user.updated')
|
||||
async onUserUpdated(user: EventPayload<'user.updated'>) {
|
||||
async onUserUpdated(user: Events['user.updated']) {
|
||||
const { enabled, customerIo } = this.config.metrics;
|
||||
if (enabled && customerIo?.token) {
|
||||
const payload = {
|
||||
@@ -33,7 +33,7 @@ export class UserEventsListener {
|
||||
}
|
||||
|
||||
@OnEvent('user.deleted')
|
||||
async onUserDeleted(user: EventPayload<'user.deleted'>) {
|
||||
async onUserDeleted(user: Events['user.deleted']) {
|
||||
const { enabled, customerIo } = this.config.metrics;
|
||||
if (enabled && customerIo?.token) {
|
||||
try {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@nestjs/graphql';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import type { Payload } from '../../base/event/def';
|
||||
import { type CurrentUser } from '../auth/session';
|
||||
|
||||
@ObjectType()
|
||||
@@ -91,11 +90,3 @@ export class ManageUserInput {
|
||||
@Field({ description: 'User name', nullable: true })
|
||||
name?: string;
|
||||
}
|
||||
|
||||
declare module '../../base/event/def' {
|
||||
interface UserEvents {
|
||||
admin: {
|
||||
created: Payload<{ id: string }>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getStreamAsBuffer } from 'get-stream';
|
||||
|
||||
import {
|
||||
Cache,
|
||||
type EventPayload,
|
||||
MailService,
|
||||
OnEvent,
|
||||
URLHelper,
|
||||
@@ -256,7 +255,7 @@ export class WorkspaceService {
|
||||
async onMemberLeave({
|
||||
user,
|
||||
workspaceId,
|
||||
}: EventPayload<'workspace.members.leave'>) {
|
||||
}: Events['workspace.members.leave']) {
|
||||
const workspace = await this.getWorkspaceInfo(workspaceId);
|
||||
const owner = await this.permission.getWorkspaceOwner(workspaceId);
|
||||
await this.mailer.sendMemberLeaveEmail(owner.email, {
|
||||
@@ -269,7 +268,7 @@ export class WorkspaceService {
|
||||
async onMemberRemoved({
|
||||
userId,
|
||||
workspaceId,
|
||||
}: EventPayload<'workspace.members.requestDeclined'>) {
|
||||
}: Events['workspace.members.requestDeclined']) {
|
||||
const user = await this.models.user.get(userId);
|
||||
if (!user) return;
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ import { nanoid } from 'nanoid';
|
||||
|
||||
import {
|
||||
Cache,
|
||||
EventEmitter,
|
||||
type EventPayload,
|
||||
EventBus,
|
||||
MemberNotFoundInSpace,
|
||||
OnEvent,
|
||||
RequestMutex,
|
||||
@@ -43,7 +42,7 @@ export class TeamWorkspaceResolver {
|
||||
|
||||
constructor(
|
||||
private readonly cache: Cache,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly event: EventBus,
|
||||
private readonly url: URLHelper,
|
||||
private readonly prisma: PrismaClient,
|
||||
private readonly permissions: PermissionService,
|
||||
@@ -344,7 +343,7 @@ export class TeamWorkspaceResolver {
|
||||
@OnEvent('workspace.members.reviewRequested')
|
||||
async onReviewRequested({
|
||||
inviteId,
|
||||
}: EventPayload<'workspace.members.reviewRequested'>) {
|
||||
}: Events['workspace.members.reviewRequested']) {
|
||||
// send review request mail to owner and admin
|
||||
await this.workspaceService.sendReviewRequestedEmail(inviteId);
|
||||
}
|
||||
@@ -352,7 +351,7 @@ export class TeamWorkspaceResolver {
|
||||
@OnEvent('workspace.members.requestApproved')
|
||||
async onApproveRequest({
|
||||
inviteId,
|
||||
}: EventPayload<'workspace.members.requestApproved'>) {
|
||||
}: Events['workspace.members.requestApproved']) {
|
||||
// send approve mail
|
||||
await this.workspaceService.sendReviewApproveEmail(inviteId);
|
||||
}
|
||||
@@ -361,7 +360,7 @@ export class TeamWorkspaceResolver {
|
||||
async onDeclineRequest({
|
||||
userId,
|
||||
workspaceId,
|
||||
}: EventPayload<'workspace.members.requestDeclined'>) {
|
||||
}: Events['workspace.members.requestDeclined']) {
|
||||
const user = await this.models.user.getPublicUser(userId);
|
||||
// send decline mail
|
||||
await this.workspaceService.sendReviewDeclinedEmail(
|
||||
@@ -375,7 +374,7 @@ export class TeamWorkspaceResolver {
|
||||
userId,
|
||||
workspaceId,
|
||||
permission,
|
||||
}: EventPayload<'workspace.members.roleChanged'>) {
|
||||
}: Events['workspace.members.roleChanged']) {
|
||||
// send role changed mail
|
||||
await this.workspaceService.sendRoleChangedEmail(userId, {
|
||||
id: workspaceId,
|
||||
@@ -388,7 +387,7 @@ export class TeamWorkspaceResolver {
|
||||
workspaceId,
|
||||
from,
|
||||
to,
|
||||
}: EventPayload<'workspace.members.ownershipTransferred'>) {
|
||||
}: Events['workspace.members.ownershipTransferred']) {
|
||||
// send ownership transferred mail
|
||||
const fromUser = await this.models.user.getPublicUser(from);
|
||||
const toUser = await this.models.user.getPublicUser(to);
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
AlreadyInSpace,
|
||||
Cache,
|
||||
DocNotFound,
|
||||
EventEmitter,
|
||||
EventBus,
|
||||
InternalServerError,
|
||||
MemberQuotaExceeded,
|
||||
QueryTooLong,
|
||||
@@ -83,7 +83,7 @@ export class WorkspaceResolver {
|
||||
private readonly permissions: PermissionService,
|
||||
private readonly quota: QuotaManagementService,
|
||||
private readonly models: Models,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly event: EventBus,
|
||||
private readonly mutex: RequestMutex,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly workspaceStorage: PgWorkspaceDocStorageAdapter
|
||||
@@ -389,7 +389,7 @@ export class WorkspaceResolver {
|
||||
});
|
||||
await this.workspaceStorage.deleteSpace(id);
|
||||
|
||||
this.event.emit('workspace.deleted', id);
|
||||
this.event.emit('workspace.deleted', { id });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user