mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(server): cluster level event system (#9884)
This commit is contained in:
@@ -3,8 +3,8 @@ import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { InstalledLicense, PrismaClient } from '@prisma/client';
|
||||
|
||||
import {
|
||||
EventEmitter,
|
||||
type EventPayload,
|
||||
Config,
|
||||
EventBus,
|
||||
InternalServerError,
|
||||
LicenseNotFound,
|
||||
OnEvent,
|
||||
@@ -27,9 +27,10 @@ export class LicenseService {
|
||||
private readonly logger = new Logger(LicenseService.name);
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly db: PrismaClient,
|
||||
private readonly quota: QuotaManagementService,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly event: EventBus,
|
||||
private readonly permission: PermissionService
|
||||
) {}
|
||||
|
||||
@@ -151,7 +152,11 @@ export class LicenseService {
|
||||
}
|
||||
|
||||
@OnEvent('workspace.members.updated')
|
||||
async updateTeamSeats(payload: EventPayload<'workspace.members.updated'>) {
|
||||
async updateTeamSeats(payload: Events['workspace.members.updated']) {
|
||||
if (!this.config.isSelfhosted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { workspaceId, count } = payload;
|
||||
|
||||
const license = await this.db.installedLicense.findUnique({
|
||||
@@ -308,7 +313,7 @@ export class LicenseService {
|
||||
plan,
|
||||
recurring,
|
||||
quantity,
|
||||
}: EventPayload<'workspace.subscription.activated'>) {
|
||||
}: Events['workspace.subscription.activated']) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.SelfHostedTeam:
|
||||
await this.quota.addTeamWorkspace(
|
||||
@@ -331,7 +336,7 @@ export class LicenseService {
|
||||
async onWorkspaceSubscriptionCanceled({
|
||||
workspaceId,
|
||||
plan,
|
||||
}: EventPayload<'workspace.subscription.canceled'>) {
|
||||
}: Events['workspace.subscription.canceled']) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.SelfHostedTeam:
|
||||
await this.quota.removeTeamWorkspace(workspaceId);
|
||||
|
||||
@@ -2,11 +2,10 @@ import assert from 'node:assert';
|
||||
|
||||
import type { RawBodyRequest } from '@nestjs/common';
|
||||
import { Controller, Logger, Post, Req } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import type { Request } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { Config, InternalServerError } from '../../base';
|
||||
import { Config, EventBus, InternalServerError } from '../../base';
|
||||
import { Public } from '../../core/auth';
|
||||
|
||||
@Controller('/api/stripe')
|
||||
@@ -17,7 +16,7 @@ export class StripeWebhookController {
|
||||
constructor(
|
||||
config: Config,
|
||||
private readonly stripe: Stripe,
|
||||
private readonly event: EventEmitter2
|
||||
private readonly event: EventBus
|
||||
) {
|
||||
assert(config.plugins.payment.stripe);
|
||||
this.webhookKey = config.plugins.payment.stripe.keys.webhookKey;
|
||||
@@ -41,7 +40,7 @@ export class StripeWebhookController {
|
||||
|
||||
// Stripe requires responseing webhook immediately and handle event asynchronously.
|
||||
setImmediate(() => {
|
||||
this.event.emitAsync(`stripe:${event.type}`, event).catch(e => {
|
||||
this.event.emitAsync(`stripe.${event.type}` as any, event).catch(e => {
|
||||
this.logger.error('Failed to handle Stripe Webhook event.', e);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { EventEmitter, type EventPayload } from '../../base';
|
||||
import { EventBus } from '../../base';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export class SubscriptionCronJobs {
|
||||
constructor(
|
||||
private readonly db: PrismaClient,
|
||||
private readonly event: EventEmitter
|
||||
private readonly event: EventBus
|
||||
) {}
|
||||
|
||||
private getDateRange(after: number, base: number | Date = Date.now()) {
|
||||
@@ -77,14 +77,14 @@ export class SubscriptionCronJobs {
|
||||
// should not reach here
|
||||
continue;
|
||||
}
|
||||
this.event.emit('workspace.subscription.notify', {
|
||||
workspaceId: subscription.targetId,
|
||||
expirationDate: end,
|
||||
deletionDate:
|
||||
subscription.status === 'canceled'
|
||||
? this.getDateRange(180, end).end
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (!subscription.nextBillAt) {
|
||||
this.event.emit('workspace.subscription.notify', {
|
||||
workspaceId: subscription.targetId,
|
||||
expirationDate: end,
|
||||
deletionDate: this.getDateRange(180, end).end,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ export class SubscriptionCronJobs {
|
||||
async handleUserSubscriptionCanceled({
|
||||
userId,
|
||||
plan,
|
||||
}: EventPayload<'user.subscription.canceled'>) {
|
||||
}: Events['user.subscription.canceled']) {
|
||||
await this.db.subscription.delete({
|
||||
where: {
|
||||
targetId_plan: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
EventEmitter,
|
||||
EventBus,
|
||||
InternalServerError,
|
||||
InvalidCheckoutParameters,
|
||||
Runtime,
|
||||
@@ -58,7 +58,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
|
||||
db: PrismaClient,
|
||||
private readonly runtime: Runtime,
|
||||
private readonly feature: FeatureManagementService,
|
||||
private readonly event: EventEmitter,
|
||||
private readonly event: EventBus,
|
||||
private readonly url: URLHelper
|
||||
) {
|
||||
super(stripe, db);
|
||||
|
||||
@@ -5,8 +5,7 @@ import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
EventEmitter,
|
||||
type EventPayload,
|
||||
EventBus,
|
||||
OnEvent,
|
||||
SubscriptionAlreadyExists,
|
||||
SubscriptionPlanNotFound,
|
||||
@@ -49,7 +48,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
||||
stripe: Stripe,
|
||||
db: PrismaClient,
|
||||
private readonly url: URLHelper,
|
||||
private readonly event: EventEmitter
|
||||
private readonly event: EventBus
|
||||
) {
|
||||
super(stripe, db);
|
||||
}
|
||||
@@ -269,7 +268,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
||||
async onMembersUpdated({
|
||||
workspaceId,
|
||||
count,
|
||||
}: EventPayload<'workspace.members.updated'>) {
|
||||
}: Events['workspace.members.updated']) {
|
||||
const subscription = await this.getSubscription({
|
||||
plan: SubscriptionPlan.Team,
|
||||
workspaceId,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import type { EventPayload } from '../../base';
|
||||
import { FeatureManagementService } from '../../core/features';
|
||||
import { PermissionService } from '../../core/permission';
|
||||
import {
|
||||
@@ -29,7 +28,7 @@ export class QuotaOverride {
|
||||
plan,
|
||||
recurring,
|
||||
quantity,
|
||||
}: EventPayload<'workspace.subscription.activated'>) {
|
||||
}: Events['workspace.subscription.activated']) {
|
||||
switch (plan) {
|
||||
case 'team': {
|
||||
const hasTeamWorkspace = await this.quota.hasWorkspaceQuota(
|
||||
@@ -62,7 +61,7 @@ export class QuotaOverride {
|
||||
async onWorkspaceSubscriptionCanceled({
|
||||
workspaceId,
|
||||
plan,
|
||||
}: EventPayload<'workspace.subscription.canceled'>) {
|
||||
}: Events['workspace.subscription.canceled']) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.Team:
|
||||
await this.manager.removeTeamWorkspace(workspaceId);
|
||||
@@ -77,7 +76,7 @@ export class QuotaOverride {
|
||||
userId,
|
||||
plan,
|
||||
recurring,
|
||||
}: EventPayload<'user.subscription.activated'>) {
|
||||
}: Events['user.subscription.activated']) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.AI:
|
||||
await this.feature.addCopilot(userId, 'subscription activated');
|
||||
@@ -100,7 +99,7 @@ export class QuotaOverride {
|
||||
async onUserSubscriptionCanceled({
|
||||
userId,
|
||||
plan,
|
||||
}: EventPayload<'user.subscription.canceled'>) {
|
||||
}: Events['user.subscription.canceled']) {
|
||||
switch (plan) {
|
||||
case SubscriptionPlan.AI:
|
||||
await this.feature.removeCopilot(userId);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { User, Workspace } from '@prisma/client';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import type { Payload } from '../../base/event/def';
|
||||
|
||||
export enum SubscriptionRecurring {
|
||||
Monthly = 'monthly',
|
||||
Yearly = 'yearly',
|
||||
@@ -50,41 +48,44 @@ export enum CouponType {
|
||||
ProEarlyAccessAIOneYearFree = 'ai_pro_ea_one_year_free',
|
||||
}
|
||||
|
||||
declare module '../../base/event/def' {
|
||||
interface UserEvents {
|
||||
subscription: {
|
||||
activated: Payload<{
|
||||
userId: User['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
}>;
|
||||
canceled: Payload<{
|
||||
userId: User['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
}>;
|
||||
declare global {
|
||||
interface Events {
|
||||
'user.subscription.activated': {
|
||||
userId: User['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
};
|
||||
'user.subscription.canceled': {
|
||||
userId: User['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkspaceEvents {
|
||||
subscription: {
|
||||
activated: Payload<{
|
||||
workspaceId: Workspace['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
quantity: number;
|
||||
}>;
|
||||
canceled: Payload<{
|
||||
workspaceId: Workspace['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
}>;
|
||||
notify: Payload<{
|
||||
workspaceId: Workspace['id'];
|
||||
expirationDate: Date;
|
||||
deletionDate: Date | undefined;
|
||||
}>;
|
||||
'workspace.subscription.activated': {
|
||||
workspaceId: Workspace['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
quantity: number;
|
||||
};
|
||||
'workspace.subscription.canceled': {
|
||||
workspaceId: Workspace['id'];
|
||||
plan: SubscriptionPlan;
|
||||
recurring: SubscriptionRecurring;
|
||||
};
|
||||
'workspace.subscription.notify': {
|
||||
workspaceId: Workspace['id'];
|
||||
expirationDate: Date;
|
||||
deletionDate: Date;
|
||||
};
|
||||
|
||||
'stripe.invoice.created': Stripe.InvoiceCreatedEvent;
|
||||
'stripe.invoice.updated': Stripe.InvoiceUpdatedEvent;
|
||||
'stripe.invoice.finalization_failed': Stripe.InvoiceFinalizationFailedEvent;
|
||||
'stripe.invoice.payment_failed': Stripe.InvoicePaymentFailedEvent;
|
||||
'stripe.invoice.paid': Stripe.InvoicePaidEvent;
|
||||
'stripe.customer.subscription.created': Stripe.CustomerSubscriptionCreatedEvent;
|
||||
'stripe.customer.subscription.updated': Stripe.CustomerSubscriptionUpdatedEvent;
|
||||
'stripe.customer.subscription.deleted': Stripe.CustomerSubscriptionDeletedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { OnEvent } from '../../base';
|
||||
import { SubscriptionService } from './service';
|
||||
|
||||
const OnStripeEvent = (
|
||||
event: Stripe.Event.Type,
|
||||
opts?: Parameters<typeof OnEvent>[1]
|
||||
) => OnEvent(`stripe:${event}`, opts);
|
||||
|
||||
/**
|
||||
* Stripe webhook events sent in random order, and may be even sent more than once.
|
||||
*
|
||||
@@ -22,11 +17,11 @@ export class StripeWebhook {
|
||||
private readonly stripe: Stripe
|
||||
) {}
|
||||
|
||||
@OnStripeEvent('invoice.created')
|
||||
@OnStripeEvent('invoice.updated')
|
||||
@OnStripeEvent('invoice.finalization_failed')
|
||||
@OnStripeEvent('invoice.payment_failed')
|
||||
@OnStripeEvent('invoice.paid')
|
||||
@OnEvent('stripe.invoice.created')
|
||||
@OnEvent('stripe.invoice.updated')
|
||||
@OnEvent('stripe.invoice.finalization_failed')
|
||||
@OnEvent('stripe.invoice.payment_failed')
|
||||
@OnEvent('stripe.invoice.paid')
|
||||
async onInvoiceUpdated(
|
||||
event:
|
||||
| Stripe.InvoiceCreatedEvent
|
||||
@@ -39,8 +34,8 @@ export class StripeWebhook {
|
||||
await this.service.saveStripeInvoice(invoice);
|
||||
}
|
||||
|
||||
@OnStripeEvent('customer.subscription.created')
|
||||
@OnStripeEvent('customer.subscription.updated')
|
||||
@OnEvent('stripe.customer.subscription.created')
|
||||
@OnEvent('stripe.customer.subscription.updated')
|
||||
async onSubscriptionChanges(
|
||||
event:
|
||||
| Stripe.CustomerSubscriptionUpdatedEvent
|
||||
@@ -56,7 +51,7 @@ export class StripeWebhook {
|
||||
await this.service.saveStripeSubscription(subscription);
|
||||
}
|
||||
|
||||
@OnStripeEvent('customer.subscription.deleted')
|
||||
@OnEvent('stripe.customer.subscription.deleted')
|
||||
async onSubscriptionDeleted(event: Stripe.CustomerSubscriptionDeletedEvent) {
|
||||
await this.service.deleteStripeSubscription(event.data.object);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user