From 011e71429befebb27b4c010ddb84fbf116d66a5c Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 23 Jul 2024 10:42:10 +0800 Subject: [PATCH 01/70] feat: pub/sub manager --- .../src/__tests__/pub-sub-manager.test.ts | 83 ++++++++++++++++++ packages/core/server/src/application.ts | 9 ++ packages/core/server/src/pub-sub-manager.ts | 87 +++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 packages/core/server/src/__tests__/pub-sub-manager.test.ts create mode 100644 packages/core/server/src/pub-sub-manager.ts diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts new file mode 100644 index 0000000000..c6f21f3362 --- /dev/null +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -0,0 +1,83 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createMockServer, MockServer } from '@nocobase/test'; +import { createClient } from 'redis'; +import Plugin from '../plugin'; +import { IPubSubAdapter } from '../pub-sub-manager'; + +export class RedisPubSubAdapter implements IPubSubAdapter { + publisher; + subscriber; + + constructor() { + this.publisher = createClient(); + this.subscriber = this.publisher.duplicate(); + } + + async connect() { + await this.publisher.connect(); + await this.subscriber.connect(); + } + + async close() { + await this.publisher.disconnect(); + await this.subscriber.disconnect(); + } + + async subscribe(channel, callback) { + return this.subscriber.subscribe(channel, callback, true); + } + + async unsubscribe(channel, callback) { + return this.subscriber.unsubscribe(channel, callback, true); + } + + async publish(channel, message) { + return this.publisher.publish(channel, message); + } + + onMessage(callback) {} +} + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('pub-sub-manager', () => { + test('case1', async () => { + let count = 0; + class Plugin1 extends Plugin { + async beforeLoad() { + this.app.pubSubManager.setAdapter(new RedisPubSubAdapter()); + await this.app.pubSubManager.subscribe('chan1nel', (message) => { + ++count; + console.log(`Channel1 subscriber collected message: ${message}`); + }); + } + } + const appOpts = { + pubSubManager: { + name: 'app1', + }, + plugins: [Plugin1, 'nocobase'], + }; + const node1: MockServer = await createMockServer({ + ...appOpts, + name: 'node1', + }); + const node2: MockServer = await createMockServer({ + ...appOpts, + name: 'node2', + }); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await sleep(1000); + expect(count).toBe(2); + await node1.destroy(); + await node2.destroy(); + }); +}); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 065a440794..e2bb8416c3 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -36,6 +36,7 @@ import lodash from 'lodash'; import { RecordableHistogram } from 'node:perf_hooks'; import path, { basename, resolve } from 'path'; import semver from 'semver'; +import packageJson from '../package.json'; import { createACL } from './acl'; import { AppCommand } from './app-command'; import { AppSupervisor } from './app-supervisor'; @@ -59,6 +60,7 @@ import { dataTemplate } from './middlewares/data-template'; import validateFilterParams from './middlewares/validate-filter-params'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; +import { PubSubManager } from './pub-sub-manager'; import { SyncManager } from './sync-manager'; import packageJson from '../package.json'; @@ -97,6 +99,7 @@ export interface ApplicationOptions { */ resourcer?: ResourceManagerOptions; resourceManager?: ResourceManagerOptions; + pubSubManager?: any; bodyParser?: any; cors?: any; dataWrapping?: boolean; @@ -226,6 +229,7 @@ export class Application exten * @internal */ public syncManager: SyncManager; + public pubSubManager: PubSubManager; public requestLogger: Logger; private sqlLogger: Logger; protected _logger: SystemLogger; @@ -518,6 +522,10 @@ export class Application exten await this.cacheManager.close(); } + if (this.pubSubManager) { + await this.pubSubManager.close(); + } + if (this.telemetry.started) { await this.telemetry.shutdown(); } @@ -1121,6 +1129,7 @@ export class Application exten this._cli = this.createCLI(); this._i18n = createI18n(options); this.syncManager = new SyncManager(this); + this.pubSubManager = new PubSubManager(this, options.pubSubManager); this.context.db = this.db; /** diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts new file mode 100644 index 0000000000..919af3d736 --- /dev/null +++ b/packages/core/server/src/pub-sub-manager.ts @@ -0,0 +1,87 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import Application from './application'; + +export class PubSubManager { + adapter: IPubSubAdapter; + subscribes = new Map(); + + constructor( + protected app: Application, + protected options: any = {}, + ) { + app.on('afterStart', async () => { + await this.connect(); + }); + app.on('afterStop', async () => { + await this.close(); + }); + } + + get prefix() { + return this.options.name || this.app.name; + } + + setAdapter(adapter: IPubSubAdapter) { + this.adapter = adapter; + } + + async connect() { + await this.adapter.connect(); + // subscribe 要在 connect 之后 + for (const [channel, callbacks] of this.subscribes) { + for (const callback of callbacks) { + await this.adapter.subscribe(`${this.prefix}.${channel}`, callback); + } + } + } + + async close() { + return await this.adapter.close(); + } + + async subscribe(channel, callback) { + if (!this.subscribes.has(channel)) { + const set = new Set(); + this.subscribes.set(channel, set); + } + const set = this.subscribes.get(channel); + set.add(callback); + } + + async unsubscribe(channel, callback) { + const set = this.subscribes.get(channel); + if (set) { + set.delete(callback); + } + return this.adapter.unsubscribe(`${this.prefix}.${channel}`, callback); + } + + async publish(channel, message) { + return this.adapter.publish(`${this.prefix}.${channel}`, message); + } + + onMessage(callback) { + return this.adapter.onMessage((channel, message) => { + if (channel.startsWith(`${this.prefix}.`)) { + callback(channel, message); + } + }); + } +} + +export interface IPubSubAdapter { + connect(): Promise; + close(): Promise; + subscribe(channel: string, callback): Promise; + unsubscribe(channel: string, callback): Promise; + publish(channel: string, message): Promise; + onMessage(callback): void; +} From 6ac9117e9de7e2f2956a8e341f35faedaebac70d Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 23 Jul 2024 12:32:06 +0800 Subject: [PATCH 02/70] fix: test case --- .../src/__tests__/pub-sub-manager.test.ts | 11 ++++++++++ packages/core/server/src/plugin.ts | 9 ++++++++ packages/core/server/src/pub-sub-manager.ts | 22 +++++++++++++++++++ packages/core/server/src/sync-manager.ts | 10 ++++----- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index c6f21f3362..63707ddd95 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -52,6 +52,14 @@ describe('pub-sub-manager', () => { test('case1', async () => { let count = 0; class Plugin1 extends Plugin { + get name() { + return 'Plugin1'; + } + + onMessage() { + ++count; + } + async beforeLoad() { this.app.pubSubManager.setAdapter(new RedisPubSubAdapter()); await this.app.pubSubManager.subscribe('chan1nel', (message) => { @@ -77,6 +85,9 @@ describe('pub-sub-manager', () => { await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); await sleep(1000); expect(count).toBe(2); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await sleep(1000); + expect(count).toBe(4); await node1.destroy(); await node2.destroy(); }); diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index aced18f95a..dca582a795 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -148,6 +148,15 @@ export abstract class Plugin implements PluginInterface { this.app.syncManager.publish(this.name, message); } + onMessage(message) {} + async sendMessage(message) { + if (!this.name) { + return; + } + console.log('sendMessage', this.name); + await this.app.pubSubManager.publish(this.name, message); + } + /** * @deprecated */ diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index 919af3d736..98e2e79346 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -23,6 +23,13 @@ export class PubSubManager { app.on('afterStop', async () => { await this.close(); }); + app.on('beforeLoadPlugin', async (plugin) => { + if (!plugin.name) { + return; + } + console.log('beforeLoadPlugin', plugin.name); + await this.subscribe(plugin.name, plugin.onMessage.bind(plugin)); + }); } get prefix() { @@ -34,6 +41,9 @@ export class PubSubManager { } async connect() { + if (!this.adapter) { + return; + } await this.adapter.connect(); // subscribe 要在 connect 之后 for (const [channel, callbacks] of this.subscribes) { @@ -44,6 +54,9 @@ export class PubSubManager { } async close() { + if (!this.adapter) { + return; + } return await this.adapter.close(); } @@ -61,14 +74,23 @@ export class PubSubManager { if (set) { set.delete(callback); } + if (!this.adapter) { + return; + } return this.adapter.unsubscribe(`${this.prefix}.${channel}`, callback); } async publish(channel, message) { + if (!this.adapter) { + return; + } return this.adapter.publish(`${this.prefix}.${channel}`, message); } onMessage(callback) { + if (!this.adapter) { + return; + } return this.adapter.onMessage((channel, message) => { if (channel.startsWith(`${this.prefix}.`)) { callback(channel, message); diff --git a/packages/core/server/src/sync-manager.ts b/packages/core/server/src/sync-manager.ts index faace9d7a6..e8fc42df5b 100644 --- a/packages/core/server/src/sync-manager.ts +++ b/packages/core/server/src/sync-manager.ts @@ -7,10 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { isEqual, uniqWith } from 'lodash'; import { randomUUID } from 'node:crypto'; import EventEmitter from 'node:events'; import Application from './application'; -import { isEqual, uniqWith } from 'lodash'; export abstract class SyncAdapter extends EventEmitter { abstract get ready(): boolean; @@ -42,6 +42,10 @@ export class SyncManager { return this.adapter ? this.adapter.ready : false; } + constructor(private app: Application) { + this.nodeId = `${process.env.NODE_ID || randomUUID()}-${process.pid}`; + } + private onMessage(namespace, message) { this.app.logger.info(`emit sync event in namespace ${namespace}`); this.eventEmitter.emit(namespace, message); @@ -81,10 +85,6 @@ export class SyncManager { } }; - constructor(private app: Application) { - this.nodeId = `${process.env.NODE_ID || randomUUID()}-${process.pid}`; - } - public init(adapter: SyncAdapter) { if (this.adapter) { throw new Error('sync adapter is already exists'); From acc22c14b2f179392aae6f3d26549eab33ee02b1 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 23 Jul 2024 13:37:04 +0800 Subject: [PATCH 03/70] fix: test error --- packages/core/server/src/__tests__/pub-sub-manager.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 63707ddd95..cba670c19a 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -8,6 +8,7 @@ */ import { createMockServer, MockServer } from '@nocobase/test'; +import { uid } from '@nocobase/utils'; import { createClient } from 'redis'; import Plugin from '../plugin'; import { IPubSubAdapter } from '../pub-sub-manager'; @@ -76,11 +77,11 @@ describe('pub-sub-manager', () => { }; const node1: MockServer = await createMockServer({ ...appOpts, - name: 'node1', + name: 'app1_' + uid(), }); const node2: MockServer = await createMockServer({ ...appOpts, - name: 'node2', + name: 'app1_' + uid(), }); await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); await sleep(1000); From adc10b50dffd45041c22eb3b68ffd21d2ba4caa0 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 23 Jul 2024 14:06:53 +0800 Subject: [PATCH 04/70] fix: test error --- packages/core/server/src/__tests__/pub-sub-manager.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index cba670c19a..f1c0d6122e 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -18,7 +18,9 @@ export class RedisPubSubAdapter implements IPubSubAdapter { subscriber; constructor() { - this.publisher = createClient(); + this.publisher = createClient({ + url: process.env.REDIS_URL || 'redis://redis:6379', + }); this.subscriber = this.publisher.duplicate(); } From 1c2de28a0d1c4228413f3b2b21c8c4edea2e901c Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 23 Jul 2024 15:30:32 +0800 Subject: [PATCH 05/70] feat: skip self --- .../src/__tests__/pub-sub-manager.test.ts | 6 +- packages/core/server/src/pub-sub-manager.ts | 56 +++++++++++++------ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index f1c0d6122e..7794a7ac31 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -19,7 +19,7 @@ export class RedisPubSubAdapter implements IPubSubAdapter { constructor() { this.publisher = createClient({ - url: process.env.REDIS_URL || 'redis://redis:6379', + url: process.env.PUB_SUB_REDIS_URL || 'redis://redis:6379', }); this.subscriber = this.publisher.duplicate(); } @@ -87,10 +87,10 @@ describe('pub-sub-manager', () => { }); await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); await sleep(1000); - expect(count).toBe(2); + expect(count).toBe(1); await node1.pm.get(Plugin1).sendMessage('plugin send message'); await sleep(1000); - expect(count).toBe(4); + expect(count).toBe(2); await node1.destroy(); await node2.destroy(); }); diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index 98e2e79346..a1646fd3cc 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -7,16 +7,19 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { uid } from '@nocobase/utils'; import Application from './application'; export class PubSubManager { adapter: IPubSubAdapter; subscribes = new Map(); + publisherId: string; constructor( protected app: Application, protected options: any = {}, ) { + this.publisherId = uid(); app.on('afterStart', async () => { await this.connect(); }); @@ -47,8 +50,8 @@ export class PubSubManager { await this.adapter.connect(); // subscribe 要在 connect 之后 for (const [channel, callbacks] of this.subscribes) { - for (const callback of callbacks) { - await this.adapter.subscribe(`${this.prefix}.${channel}`, callback); + for (const [, fn] of callbacks) { + await this.adapter.subscribe(`${this.prefix}.${channel}`, fn); } } } @@ -60,41 +63,60 @@ export class PubSubManager { return await this.adapter.close(); } - async subscribe(channel, callback) { + async subscribe(channel, callback, skipSelf = true) { + const fn = (wrappedMessage) => { + const { publisherId, message } = JSON.parse(wrappedMessage); + if (skipSelf && publisherId === this.publisherId) { + return; + } + callback(message); + }; if (!this.subscribes.has(channel)) { - const set = new Set(); - this.subscribes.set(channel, set); + const map = new Map(); + this.subscribes.set(channel, map); } - const set = this.subscribes.get(channel); - set.add(callback); + const map = this.subscribes.get(channel); + map.set(callback, fn); } async unsubscribe(channel, callback) { - const set = this.subscribes.get(channel); - if (set) { - set.delete(callback); + const map: Map = this.subscribes.get(channel); + let fn = null; + if (map) { + fn = map.get(callback); } - if (!this.adapter) { + if (!this.adapter || !fn) { return; } - return this.adapter.unsubscribe(`${this.prefix}.${channel}`, callback); + return this.adapter.unsubscribe(`${this.prefix}.${channel}`, fn); } async publish(channel, message) { if (!this.adapter) { return; } - return this.adapter.publish(`${this.prefix}.${channel}`, message); + + const wrappedMessage = JSON.stringify({ + publisherId: this.publisherId, + message: message, + }); + + return this.adapter.publish(`${this.prefix}.${channel}`, wrappedMessage); } - onMessage(callback) { + onMessage(callback, skipSelf = true) { if (!this.adapter) { return; } - return this.adapter.onMessage((channel, message) => { - if (channel.startsWith(`${this.prefix}.`)) { - callback(channel, message); + return this.adapter.onMessage((channel: string, wrappedMessage) => { + if (!channel.startsWith(`${this.prefix}.`)) { + return; } + const { publisherId, message } = JSON.parse(wrappedMessage); + if (skipSelf && publisherId === this.publisherId) { + return; + } + callback(channel, message); }); } } From fb3edcf9689631d767bed25bff9c1675d26080d5 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 23 Jul 2024 17:08:54 +0800 Subject: [PATCH 06/70] feat: debounce --- .../src/__tests__/pub-sub-manager.test.ts | 27 ++++++++++++++----- packages/core/server/src/pub-sub-manager.ts | 25 ++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 7794a7ac31..b90fa84951 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -65,10 +65,14 @@ describe('pub-sub-manager', () => { async beforeLoad() { this.app.pubSubManager.setAdapter(new RedisPubSubAdapter()); - await this.app.pubSubManager.subscribe('chan1nel', (message) => { - ++count; - console.log(`Channel1 subscriber collected message: ${message}`); - }); + await this.app.pubSubManager.subscribe( + 'chan1nel', + (message) => { + ++count; + console.log(`Channel1 subscriber collected message: ${message}`); + }, + { debounce: 1000 }, + ); } } const appOpts = { @@ -86,10 +90,21 @@ describe('pub-sub-manager', () => { name: 'app1_' + uid(), }); await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await sleep(1000); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await sleep(2000); expect(count).toBe(1); await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await sleep(1000); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await sleep(2000); expect(count).toBe(2); await node1.destroy(); await node2.destroy(); diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index a1646fd3cc..eba30bc9fc 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -8,8 +8,14 @@ */ import { uid } from '@nocobase/utils'; +import _ from 'lodash'; import Application from './application'; +export interface PubSubManagerCallbackOptions { + skipSelf?: boolean; + debounce?: number; +} + export class PubSubManager { adapter: IPubSubAdapter; subscribes = new Map(); @@ -30,8 +36,7 @@ export class PubSubManager { if (!plugin.name) { return; } - console.log('beforeLoadPlugin', plugin.name); - await this.subscribe(plugin.name, plugin.onMessage.bind(plugin)); + await this.subscribe(plugin.name, plugin.onMessage.bind(plugin), { debounce: 1000 }); }); } @@ -63,7 +68,8 @@ export class PubSubManager { return await this.adapter.close(); } - async subscribe(channel, callback, skipSelf = true) { + async subscribe(channel: string, callback, options: PubSubManagerCallbackOptions = {}) { + const { skipSelf = true, debounce = 0 } = options; const fn = (wrappedMessage) => { const { publisherId, message } = JSON.parse(wrappedMessage); if (skipSelf && publisherId === this.publisherId) { @@ -71,12 +77,13 @@ export class PubSubManager { } callback(message); }; + const debounceCallback = debounce ? _.debounce(fn, debounce) : fn; if (!this.subscribes.has(channel)) { const map = new Map(); this.subscribes.set(channel, map); } const map = this.subscribes.get(channel); - map.set(callback, fn); + map.set(callback, debounceCallback); } async unsubscribe(channel, callback) { @@ -104,10 +111,16 @@ export class PubSubManager { return this.adapter.publish(`${this.prefix}.${channel}`, wrappedMessage); } - onMessage(callback, skipSelf = true) { + onMessage(callback, options: PubSubManagerCallbackOptions = {}) { if (!this.adapter) { return; } + const { skipSelf = true, debounce = 0 } = options; + const debounceCallback = debounce + ? _.debounce((channel, message) => { + callback(channel, message); + }, debounce) + : callback; return this.adapter.onMessage((channel: string, wrappedMessage) => { if (!channel.startsWith(`${this.prefix}.`)) { return; @@ -116,7 +129,7 @@ export class PubSubManager { if (skipSelf && publisherId === this.publisherId) { return; } - callback(channel, message); + debounceCallback(channel, message); }); } } From 44e90d01345ca9716b430491b1fd77d41e5c8461 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 24 Jul 2024 10:05:09 +0800 Subject: [PATCH 07/70] feat: improve code --- .../src/__tests__/pub-sub-manager.test.ts | 69 ++++-------------- packages/core/server/src/index.ts | 5 +- packages/core/server/src/plugin.ts | 2 +- packages/core/server/src/pub-sub-manager.ts | 34 +++++---- .../test/src/server/memory-pub-sub-adapter.ts | 70 +++++++++++++++++++ packages/core/test/src/server/mock-server.ts | 3 + 6 files changed, 109 insertions(+), 74 deletions(-) create mode 100644 packages/core/test/src/server/memory-pub-sub-adapter.ts diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index b90fa84951..6afdacaa61 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -9,45 +9,7 @@ import { createMockServer, MockServer } from '@nocobase/test'; import { uid } from '@nocobase/utils'; -import { createClient } from 'redis'; import Plugin from '../plugin'; -import { IPubSubAdapter } from '../pub-sub-manager'; - -export class RedisPubSubAdapter implements IPubSubAdapter { - publisher; - subscriber; - - constructor() { - this.publisher = createClient({ - url: process.env.PUB_SUB_REDIS_URL || 'redis://redis:6379', - }); - this.subscriber = this.publisher.duplicate(); - } - - async connect() { - await this.publisher.connect(); - await this.subscriber.connect(); - } - - async close() { - await this.publisher.disconnect(); - await this.subscriber.disconnect(); - } - - async subscribe(channel, callback) { - return this.subscriber.subscribe(channel, callback, true); - } - - async unsubscribe(channel, callback) { - return this.subscriber.unsubscribe(channel, callback, true); - } - - async publish(channel, message) { - return this.publisher.publish(channel, message); - } - - onMessage(callback) {} -} const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -59,25 +21,24 @@ describe('pub-sub-manager', () => { return 'Plugin1'; } - onMessage() { + async onMessage() { ++count; } async beforeLoad() { - this.app.pubSubManager.setAdapter(new RedisPubSubAdapter()); await this.app.pubSubManager.subscribe( 'chan1nel', (message) => { ++count; console.log(`Channel1 subscriber collected message: ${message}`); }, - { debounce: 1000 }, + { debounce: 500 }, ); } } const appOpts = { pubSubManager: { - name: 'app1', + basename: 'pubsub1', }, plugins: [Plugin1, 'nocobase'], }; @@ -89,23 +50,19 @@ describe('pub-sub-manager', () => { ...appOpts, name: 'app1_' + uid(), }); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await sleep(2000); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + node1.pubSubManager.publish('chan1nel', `channel1_message_1`); + await sleep(1000); expect(count).toBe(1); await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); - await sleep(2000); expect(count).toBe(2); + await node1.pm.get(Plugin1).sendMessage('plugin send message'); + expect(count).toBe(3); await node1.destroy(); await node2.destroy(); }); diff --git a/packages/core/server/src/index.ts b/packages/core/server/src/index.ts index b711345f31..8e8e31395a 100644 --- a/packages/core/server/src/index.ts +++ b/packages/core/server/src/index.ts @@ -7,13 +7,14 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +export * from './app-supervisor'; export * from './application'; export { Application as default } from './application'; +export * from './gateway'; export * as middlewares from './middlewares'; export * from './migration'; export * from './plugin'; export * from './plugin-manager'; -export * from './gateway'; -export * from './app-supervisor'; +export * from './pub-sub-manager'; export * from './sync-manager'; export const OFFICIAL_PLUGIN_PREFIX = '@nocobase/plugin-'; diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index dca582a795..f2e88e8dc6 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -148,7 +148,7 @@ export abstract class Plugin implements PluginInterface { this.app.syncManager.publish(this.name, message); } - onMessage(message) {} + async onMessage(message) {} async sendMessage(message) { if (!this.name) { return; diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index eba30bc9fc..e9baee4608 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -36,12 +36,14 @@ export class PubSubManager { if (!plugin.name) { return; } - await this.subscribe(plugin.name, plugin.onMessage.bind(plugin), { debounce: 1000 }); + await this.subscribe(plugin.name, plugin.onMessage.bind(plugin), { + debounce: Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000), + }); }); } - get prefix() { - return this.options.name || this.app.name; + get basename() { + return this.options.basename ? `${this.options.basename}.` : ''; } setAdapter(adapter: IPubSubAdapter) { @@ -56,7 +58,7 @@ export class PubSubManager { // subscribe 要在 connect 之后 for (const [channel, callbacks] of this.subscribes) { for (const [, fn] of callbacks) { - await this.adapter.subscribe(`${this.prefix}.${channel}`, fn); + await this.adapter.subscribe(`${this.basename}${channel}`, fn); } } } @@ -70,14 +72,13 @@ export class PubSubManager { async subscribe(channel: string, callback, options: PubSubManagerCallbackOptions = {}) { const { skipSelf = true, debounce = 0 } = options; - const fn = (wrappedMessage) => { + const debounceCallback = this.debounce((wrappedMessage) => { const { publisherId, message } = JSON.parse(wrappedMessage); if (skipSelf && publisherId === this.publisherId) { return; } callback(message); - }; - const debounceCallback = debounce ? _.debounce(fn, debounce) : fn; + }, debounce); if (!this.subscribes.has(channel)) { const map = new Map(); this.subscribes.set(channel, map); @@ -95,7 +96,7 @@ export class PubSubManager { if (!this.adapter || !fn) { return; } - return this.adapter.unsubscribe(`${this.prefix}.${channel}`, fn); + return this.adapter.unsubscribe(`${this.basename}${channel}`, fn); } async publish(channel, message) { @@ -108,7 +109,14 @@ export class PubSubManager { message: message, }); - return this.adapter.publish(`${this.prefix}.${channel}`, wrappedMessage); + return this.adapter.publish(`${this.basename}${channel}`, wrappedMessage); + } + + protected debounce(func, wait: number) { + if (wait) { + return _.debounce(func, wait); + } + return func; } onMessage(callback, options: PubSubManagerCallbackOptions = {}) { @@ -116,13 +124,9 @@ export class PubSubManager { return; } const { skipSelf = true, debounce = 0 } = options; - const debounceCallback = debounce - ? _.debounce((channel, message) => { - callback(channel, message); - }, debounce) - : callback; + const debounceCallback = this.debounce(callback, debounce); return this.adapter.onMessage((channel: string, wrappedMessage) => { - if (!channel.startsWith(`${this.prefix}.`)) { + if (!channel.startsWith(`${this.basename}`)) { return; } const { publisherId, message } = JSON.parse(wrappedMessage); diff --git a/packages/core/test/src/server/memory-pub-sub-adapter.ts b/packages/core/test/src/server/memory-pub-sub-adapter.ts new file mode 100644 index 0000000000..8210f59672 --- /dev/null +++ b/packages/core/test/src/server/memory-pub-sub-adapter.ts @@ -0,0 +1,70 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { IPubSubAdapter } from '@nocobase/server'; +import { AsyncEmitter, applyMixins, uid } from '@nocobase/utils'; +import { EventEmitter } from 'events'; +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +class TestEventEmitter extends EventEmitter { + declare emitAsync: (event: string | symbol, ...args: any[]) => Promise; +} + +applyMixins(TestEventEmitter, [AsyncEmitter]); + +export class MemoryPubSubAdapter implements IPubSubAdapter { + protected emitter: TestEventEmitter; + + protected connected = false; + + static instances = new Map(); + + static create(name?: string) { + if (!name) { + name = uid(); + } + if (!this.instances.has(name)) { + this.instances.set(name, new MemoryPubSubAdapter()); + } + return this.instances.get(name); + } + + constructor() { + this.emitter = new TestEventEmitter(); + } + + async connect() { + this.connected = true; + } + + async close() { + this.connected = false; + } + + async subscribe(channel, callback) { + this.emitter.on(channel, callback); + } + + async unsubscribe(channel, callback) { + this.emitter.off(channel, callback); + } + + async publish(channel, message) { + if (!this.connected) { + return; + } + await this.emitter.emitAsync(channel, message); + await this.emitter.emitAsync('__publish__', channel, message); + await sleep(Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000)); + } + + onMessage(callback) { + this.emitter.on('__publish__', callback); + } +} diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 91dec44647..e36ee3c8e6 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -12,6 +12,7 @@ import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager import jwt from 'jsonwebtoken'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; +import { MemoryPubSubAdapter } from './memory-pub-sub-adapter'; interface ActionParams { filterByTk?: any; @@ -233,6 +234,8 @@ export function mockServer(options: ApplicationOptions = {}) { ...options, }); + app.pubSubManager.setAdapter(MemoryPubSubAdapter.create(options.pubSubManager.basename)); + return app; } From 023ee34ebfb0beac947f5e422f9a5b60c596e724 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 24 Jul 2024 13:58:31 +0800 Subject: [PATCH 08/70] fix: test error --- packages/core/server/src/__tests__/pub-sub-manager.test.ts | 6 +++--- packages/core/server/src/plugin.ts | 5 ++--- packages/core/server/src/pub-sub-manager.ts | 4 ++-- packages/core/test/src/server/mock-server.ts | 4 +++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 6afdacaa61..34fd2726a5 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -21,7 +21,7 @@ describe('pub-sub-manager', () => { return 'Plugin1'; } - async onMessage() { + async handleSyncMessage() { ++count; } @@ -59,9 +59,9 @@ describe('pub-sub-manager', () => { node1.pubSubManager.publish('chan1nel', `channel1_message_1`); await sleep(1000); expect(count).toBe(1); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await node1.pm.get(Plugin1).sendSyncMessage('plugin send message'); expect(count).toBe(2); - await node1.pm.get(Plugin1).sendMessage('plugin send message'); + await node1.pm.get(Plugin1).sendSyncMessage('plugin send message'); expect(count).toBe(3); await node1.destroy(); await node2.destroy(); diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index f2e88e8dc6..a0814ae9af 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -148,12 +148,11 @@ export abstract class Plugin implements PluginInterface { this.app.syncManager.publish(this.name, message); } - async onMessage(message) {} - async sendMessage(message) { + async handleSyncMessage(message) {} + async sendSyncMessage(message) { if (!this.name) { return; } - console.log('sendMessage', this.name); await this.app.pubSubManager.publish(this.name, message); } diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index e9baee4608..e47e5aa48c 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -36,14 +36,14 @@ export class PubSubManager { if (!plugin.name) { return; } - await this.subscribe(plugin.name, plugin.onMessage.bind(plugin), { + await this.subscribe(plugin.name, plugin.handleSyncMessage.bind(plugin), { debounce: Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000), }); }); } get basename() { - return this.options.basename ? `${this.options.basename}.` : ''; + return this.options?.basename ? `${this.options.basename}.` : ''; } setAdapter(adapter: IPubSubAdapter) { diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index e36ee3c8e6..86c3dbcb58 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -234,7 +234,9 @@ export function mockServer(options: ApplicationOptions = {}) { ...options, }); - app.pubSubManager.setAdapter(MemoryPubSubAdapter.create(options.pubSubManager.basename)); + if (options.pubSubManager) { + app.pubSubManager.setAdapter(MemoryPubSubAdapter.create(options.pubSubManager?.basename)); + } return app; } From 58ef2213b97243ca4eddb1beb1fe33c6f0635087 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 09:14:34 +0800 Subject: [PATCH 09/70] feat: test cases --- .../src/__tests__/pub-sub-manager.test.ts | 228 +++++++++++++----- packages/core/server/src/application.ts | 2 +- packages/core/server/src/pub-sub-manager.ts | 80 ++++-- packages/core/test/src/server/index.ts | 5 +- .../test/src/server/memory-pub-sub-adapter.ts | 2 +- 5 files changed, 240 insertions(+), 77 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 34fd2726a5..7132ea4a15 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -7,63 +7,183 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { createMockServer, MockServer } from '@nocobase/test'; -import { uid } from '@nocobase/utils'; -import Plugin from '../plugin'; +import { MemoryPubSubAdapter, sleep } from '@nocobase/test'; +import { PubSubManager } from '../pub-sub-manager'; -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +describe('connect', () => { + let pubSubManager: PubSubManager; -describe('pub-sub-manager', () => { - test('case1', async () => { - let count = 0; - class Plugin1 extends Plugin { - get name() { - return 'Plugin1'; - } + beforeEach(async () => { + pubSubManager = new PubSubManager({ basename: 'pubsub1' }); + pubSubManager.setAdapter(new MemoryPubSubAdapter()); + }); - async handleSyncMessage() { - ++count; - } + afterEach(async () => { + await pubSubManager.close(); + }); - async beforeLoad() { - await this.app.pubSubManager.subscribe( - 'chan1nel', - (message) => { - ++count; - console.log(`Channel1 subscriber collected message: ${message}`); - }, - { debounce: 500 }, - ); - } - } - const appOpts = { - pubSubManager: { - basename: 'pubsub1', - }, - plugins: [Plugin1, 'nocobase'], - }; - const node1: MockServer = await createMockServer({ - ...appOpts, - name: 'app1_' + uid(), - }); - const node2: MockServer = await createMockServer({ - ...appOpts, - name: 'app1_' + uid(), - }); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - node1.pubSubManager.publish('chan1nel', `channel1_message_1`); - await sleep(1000); - expect(count).toBe(1); - await node1.pm.get(Plugin1).sendSyncMessage('plugin send message'); - expect(count).toBe(2); - await node1.pm.get(Plugin1).sendSyncMessage('plugin send message'); - expect(count).toBe(3); - await node1.destroy(); - await node2.destroy(); + test('not connected', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).not.toHaveBeenCalled(); + }); + + test('closed', async () => { + const mockListener = vi.fn(); + await pubSubManager.connect(); + await pubSubManager.subscribe('test1', mockListener); + await pubSubManager.close(); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).not.toHaveBeenCalled(); + }); + + test('subscribe before connect', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.connect(); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + }); + + test('subscribe after connect', async () => { + await pubSubManager.connect(); + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + }); +}); + +describe('skipSelf, unsubscribe, debounce', () => { + let pubSubManager: PubSubManager; + + beforeEach(async () => { + pubSubManager = new PubSubManager({ basename: 'pubsub1' }); + pubSubManager.setAdapter(new MemoryPubSubAdapter()); + await pubSubManager.connect(); + }); + + afterEach(async () => { + await pubSubManager.close(); + }); + + test('skipSelf: true', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).not.toHaveBeenCalled(); + }); + + test('skipSelf: false', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + }); + + test('debounce', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener, { skipSelf: false, debounce: 1000 }); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test1', 'message2'); + await sleep(2000); + expect(mockListener).toBeCalledTimes(2); + }); + + test('debounce', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribeAll(mockListener, { skipSelf: false, debounce: 1000 }); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message1'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test1', 'message2'); + pubSubManager.publish('test2', 'message2'); + pubSubManager.publish('test2', 'message2'); + await sleep(2000); + expect(mockListener).toBeCalledTimes(3); + }); + + test('unsubscribe', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + await pubSubManager.unsubscribe('test1', mockListener); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toBeCalledTimes(1); + }); + + test('subscribeAll + skipSelf: true', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribeAll(mockListener); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).not.toHaveBeenCalled(); + }); + + test('subscribeAll + skipSelf: false', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribeAll(mockListener, { skipSelf: false }); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('test1', 'message1'); + }); +}); + +describe('Pub/Sub', () => { + let publisher: PubSubManager; + let subscriber: PubSubManager; + + beforeEach(async () => { + const pubsub = new MemoryPubSubAdapter(); + publisher = new PubSubManager({ basename: 'pubsub1' }); + publisher.setAdapter(pubsub); + await publisher.connect(); + subscriber = new PubSubManager({ basename: 'pubsub1' }); + subscriber.setAdapter(pubsub); + await subscriber.connect(); + }); + + afterEach(async () => { + await publisher.close(); + await subscriber.close(); + }); + + test('subscribe publish', async () => { + const mockListener = vi.fn(); + await subscriber.subscribe('test1', mockListener); + await publisher.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + }); + + test('subscribe twice', async () => { + const mockListener = vi.fn(); + await subscriber.subscribe('test1', mockListener); + await subscriber.subscribe('test1', mockListener); + await publisher.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); }); }); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index e2bb8416c3..88e62f5a07 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -1129,7 +1129,7 @@ export class Application exten this._cli = this.createCLI(); this._i18n = createI18n(options); this.syncManager = new SyncManager(this); - this.pubSubManager = new PubSubManager(this, options.pubSubManager); + this.pubSubManager = PubSubManager.create(this, options.pubSubManager); this.context.db = this.db; /** diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index e47e5aa48c..398c9b14f5 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -8,9 +8,14 @@ */ import { uid } from '@nocobase/utils'; +import crypto from 'crypto'; import _ from 'lodash'; import Application from './application'; +export interface PubSubManagerOptions { + basename?: string; +} + export interface PubSubManagerCallbackOptions { skipSelf?: boolean; debounce?: number; @@ -18,28 +23,32 @@ export interface PubSubManagerCallbackOptions { export class PubSubManager { adapter: IPubSubAdapter; + messageHanders = new Map(); subscribes = new Map(); publisherId: string; + connected: boolean; - constructor( - protected app: Application, - protected options: any = {}, - ) { - this.publisherId = uid(); + static create(app: Application, options: PubSubManagerOptions) { + const pubSubManager = new PubSubManager(options); app.on('afterStart', async () => { - await this.connect(); + await pubSubManager.connect(); }); app.on('afterStop', async () => { - await this.close(); + await pubSubManager.close(); }); app.on('beforeLoadPlugin', async (plugin) => { if (!plugin.name) { return; } - await this.subscribe(plugin.name, plugin.handleSyncMessage.bind(plugin), { + await pubSubManager.subscribe(plugin.name, plugin.handleSyncMessage.bind(plugin), { debounce: Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000), }); }); + return pubSubManager; + } + + constructor(protected options: PubSubManagerOptions = {}) { + this.publisherId = uid(); } get basename() { @@ -54,6 +63,7 @@ export class PubSubManager { if (!this.adapter) { return; } + this.connected = true; await this.adapter.connect(); // subscribe 要在 connect 之后 for (const [channel, callbacks] of this.subscribes) { @@ -67,24 +77,46 @@ export class PubSubManager { if (!this.adapter) { return; } + this.connected = false; return await this.adapter.close(); } + getMessageHash(message) { + return crypto.createHash('sha256').update(JSON.stringify(message)).digest('hex'); + } + async subscribe(channel: string, callback, options: PubSubManagerCallbackOptions = {}) { const { skipSelf = true, debounce = 0 } = options; - const debounceCallback = this.debounce((wrappedMessage) => { + const wrappedCallback = async (wrappedMessage) => { const { publisherId, message } = JSON.parse(wrappedMessage); if (skipSelf && publisherId === this.publisherId) { return; } - callback(message); - }, debounce); + if (!debounce) { + await callback(message); + return; + } + const messageHash = '__subscribe__' + channel + this.getMessageHash(message); + if (!this.messageHanders.has(messageHash)) { + this.messageHanders.set(messageHash, this.debounce(callback, debounce)); + } + const handleMessage = this.messageHanders.get(messageHash); + await handleMessage(message); + this.messageHanders.delete(messageHash); + }; if (!this.subscribes.has(channel)) { const map = new Map(); this.subscribes.set(channel, map); } - const map = this.subscribes.get(channel); - map.set(callback, debounceCallback); + const map: Map = this.subscribes.get(channel); + const previous = map.get(callback); + if (previous) { + await this.adapter.unsubscribe(`${this.basename}${channel}`, previous); + } + map.set(callback, wrappedCallback); + if (this.connected) { + await this.adapter.subscribe(`${this.basename}${channel}`, wrappedCallback); + } } async unsubscribe(channel, callback) { @@ -119,21 +151,31 @@ export class PubSubManager { return func; } - onMessage(callback, options: PubSubManagerCallbackOptions = {}) { + async subscribeAll(callback, options: PubSubManagerCallbackOptions = {}) { if (!this.adapter) { return; } const { skipSelf = true, debounce = 0 } = options; - const debounceCallback = this.debounce(callback, debounce); - return this.adapter.onMessage((channel: string, wrappedMessage) => { - if (!channel.startsWith(`${this.basename}`)) { + return this.adapter.subscribeAll(async (channel: string, wrappedMessage) => { + if (!channel.startsWith(this.basename)) { return; } const { publisherId, message } = JSON.parse(wrappedMessage); if (skipSelf && publisherId === this.publisherId) { return; } - debounceCallback(channel, message); + const realChannel = channel.substring(this.basename.length); + if (!debounce) { + await callback(realChannel, message); + return; + } + const messageHash = '__subscribe_all__' + realChannel + this.getMessageHash(message); + if (!this.messageHanders.has(messageHash)) { + this.messageHanders.set(messageHash, this.debounce(callback, debounce)); + } + const handleMessage = this.messageHanders.get(messageHash); + await handleMessage(realChannel, message); + this.messageHanders.delete(messageHash); }); } } @@ -144,5 +186,5 @@ export interface IPubSubAdapter { subscribe(channel: string, callback): Promise; unsubscribe(channel: string, callback): Promise; publish(channel: string, message): Promise; - onMessage(callback): void; + subscribeAll(callback): Promise; } diff --git a/packages/core/test/src/server/index.ts b/packages/core/test/src/server/index.ts index 8b0cb5a86c..329e132dfa 100644 --- a/packages/core/test/src/server/index.ts +++ b/packages/core/test/src/server/index.ts @@ -10,10 +10,11 @@ import { describe } from 'vitest'; import ws from 'ws'; -export { mockDatabase, MockDatabase } from '@nocobase/database'; +export { MockDatabase, mockDatabase } from '@nocobase/database'; export { default as supertest } from 'supertest'; -export * from './mock-server'; +export * from './memory-pub-sub-adapter'; export * from './mock-cluster'; +export * from './mock-server'; export const pgOnly: () => any = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip); export const isPg = () => process.env.DB_DIALECT == 'postgres'; diff --git a/packages/core/test/src/server/memory-pub-sub-adapter.ts b/packages/core/test/src/server/memory-pub-sub-adapter.ts index 8210f59672..2ccdcba557 100644 --- a/packages/core/test/src/server/memory-pub-sub-adapter.ts +++ b/packages/core/test/src/server/memory-pub-sub-adapter.ts @@ -64,7 +64,7 @@ export class MemoryPubSubAdapter implements IPubSubAdapter { await sleep(Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000)); } - onMessage(callback) { + async subscribeAll(callback) { this.emitter.on('__publish__', callback); } } From ad39ff21eee403bc8aa3fe4a0ad59b42c86d4a94 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 15:34:51 +0800 Subject: [PATCH 10/70] feat: test cases --- .../src/__tests__/pub-sub-manager.test.ts | 109 ++++++++++++++---- .../__tests__/sync-message-manager.test.ts | 45 ++++++++ packages/core/server/src/application.ts | 13 ++- packages/core/server/src/plugin.ts | 4 +- packages/core/server/src/pub-sub-manager.ts | 51 ++++---- .../core/server/src/sync-message-manager.ts | 49 ++++++++ .../test/src/server/memory-pub-sub-adapter.ts | 12 +- packages/core/test/src/server/mock-server.ts | 38 +++++- 8 files changed, 263 insertions(+), 58 deletions(-) create mode 100644 packages/core/server/src/__tests__/sync-message-manager.test.ts create mode 100644 packages/core/server/src/sync-message-manager.ts diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 7132ea4a15..565ea2725c 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { MemoryPubSubAdapter, sleep } from '@nocobase/test'; +import { MemoryPubSubAdapter, MockServer, createMockServer, sleep } from '@nocobase/test'; import { PubSubManager } from '../pub-sub-manager'; describe('connect', () => { @@ -40,7 +40,7 @@ describe('connect', () => { test('subscribe before connect', async () => { const mockListener = vi.fn(); - await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.subscribe('test1', mockListener); await pubSubManager.connect(); await pubSubManager.publish('test1', 'message1'); expect(mockListener).toHaveBeenCalled(); @@ -51,7 +51,7 @@ describe('connect', () => { test('subscribe after connect', async () => { await pubSubManager.connect(); const mockListener = vi.fn(); - await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.subscribe('test1', mockListener); await pubSubManager.publish('test1', 'message1'); expect(mockListener).toHaveBeenCalled(); expect(mockListener).toBeCalledTimes(1); @@ -72,25 +72,25 @@ describe('skipSelf, unsubscribe, debounce', () => { await pubSubManager.close(); }); - test('skipSelf: true', async () => { - const mockListener = vi.fn(); - await pubSubManager.subscribe('test1', mockListener); - await pubSubManager.publish('test1', 'message1'); - expect(mockListener).not.toHaveBeenCalled(); - }); - test('skipSelf: false', async () => { const mockListener = vi.fn(); - await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.subscribe('test1', mockListener); await pubSubManager.publish('test1', 'message1'); expect(mockListener).toHaveBeenCalled(); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); }); + test('skipSelf: true', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener); + await pubSubManager.publish('test1', 'message1', { skipSelf: true }); + expect(mockListener).not.toHaveBeenCalled(); + }); + test('debounce', async () => { const mockListener = vi.fn(); - await pubSubManager.subscribe('test1', mockListener, { skipSelf: false, debounce: 1000 }); + await pubSubManager.subscribe('test1', mockListener, { debounce: 1000 }); pubSubManager.publish('test1', 'message1'); pubSubManager.publish('test1', 'message1'); pubSubManager.publish('test1', 'message1'); @@ -109,7 +109,7 @@ describe('skipSelf, unsubscribe, debounce', () => { test('debounce', async () => { const mockListener = vi.fn(); - await pubSubManager.subscribeAll(mockListener, { skipSelf: false, debounce: 1000 }); + await pubSubManager.subscribeAll(mockListener, { debounce: 1000 }); pubSubManager.publish('test1', 'message1'); pubSubManager.publish('test1', 'message1'); pubSubManager.publish('test1', 'message2'); @@ -120,9 +120,34 @@ describe('skipSelf, unsubscribe, debounce', () => { expect(mockListener).toBeCalledTimes(3); }); + test('message format', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener); + await pubSubManager.publish('test1', 1); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith(1); + const msg2 = ['message1']; + await pubSubManager.publish('test1', msg2); + expect(mockListener).toBeCalledTimes(2); + expect(mockListener).toHaveBeenCalledWith(msg2); + const msg3 = { type: 'test' }; + await pubSubManager.publish('test1', msg3); + expect(mockListener).toBeCalledTimes(3); + expect(mockListener).toHaveBeenCalledWith(msg3); + await pubSubManager.publish('test1', true); + expect(mockListener).toBeCalledTimes(4); + expect(mockListener).toHaveBeenCalledWith(true); + await pubSubManager.publish('test1', false); + expect(mockListener).toBeCalledTimes(5); + expect(mockListener).toHaveBeenCalledWith(false); + await pubSubManager.publish('test1', null); + expect(mockListener).toBeCalledTimes(6); + expect(mockListener).toHaveBeenCalledWith(null); + }); + test('unsubscribe', async () => { const mockListener = vi.fn(); - await pubSubManager.subscribe('test1', mockListener, { skipSelf: false }); + await pubSubManager.subscribe('test1', mockListener); await pubSubManager.publish('test1', 'message1'); expect(mockListener).toHaveBeenCalled(); expect(mockListener).toBeCalledTimes(1); @@ -136,17 +161,17 @@ describe('skipSelf, unsubscribe, debounce', () => { const mockListener = vi.fn(); await pubSubManager.subscribeAll(mockListener); await pubSubManager.publish('test1', 'message1'); - expect(mockListener).not.toHaveBeenCalled(); - }); - - test('subscribeAll + skipSelf: false', async () => { - const mockListener = vi.fn(); - await pubSubManager.subscribeAll(mockListener, { skipSelf: false }); - await pubSubManager.publish('test1', 'message1'); expect(mockListener).toHaveBeenCalled(); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('test1', 'message1'); }); + + test('publish + skipSelf: false', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribeAll(mockListener); + await pubSubManager.publish('test1', 'message1', { skipSelf: true }); + expect(mockListener).not.toHaveBeenCalled(); + }); }); describe('Pub/Sub', () => { @@ -186,4 +211,46 @@ describe('Pub/Sub', () => { expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); }); + + test('publish only self', async () => { + const mockListener = vi.fn(); + await subscriber.subscribe('test1', mockListener); + await publisher.subscribe('test1', mockListener); + await publisher.publish('test1', 'message1', { onlySelf: true }); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + }); +}); + +describe('app.pubSubManager', () => { + let app: MockServer; + let pubSubManager: PubSubManager; + + beforeEach(async () => { + app = await createMockServer({ + pubSubManager: { + basename: 'app1', + }, + }); + pubSubManager = app.pubSubManager; + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('adapter', async () => { + expect(pubSubManager.connected).toBe(true); + expect(pubSubManager.adapter).toBeInstanceOf(MemoryPubSubAdapter); + }); + + test('subscribe + publish', async () => { + const mockListener = vi.fn(); + await pubSubManager.subscribe('test1', mockListener); + await pubSubManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + }); }); diff --git a/packages/core/server/src/__tests__/sync-message-manager.test.ts b/packages/core/server/src/__tests__/sync-message-manager.test.ts new file mode 100644 index 0000000000..b26db4c324 --- /dev/null +++ b/packages/core/server/src/__tests__/sync-message-manager.test.ts @@ -0,0 +1,45 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Plugin } from '@nocobase/server'; +import { createMultiMockServer } from '@nocobase/test'; + +describe('sync-message-manager', () => { + test('subscribe + publish', async () => { + const [node1, node2] = await createMultiMockServer({ basename: 'base1' }); + const mockListener = vi.fn(); + await node1.syncMessageManager.subscribe('test1', mockListener); + await node2.syncMessageManager.subscribe('test1', mockListener); + await node2.syncMessageManager.publish('test1', 'message1'); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + await node1.destroy(); + await node2.destroy(); + }); + + test('plugin.handleSyncMessage', async () => { + const mockListener = vi.fn(); + class MyPlugin extends Plugin { + get name() { + return 'test1'; + } + async handleSyncMessage(message) { + mockListener(message); + } + } + const [node1, node2] = await createMultiMockServer({ basename: 'base1', plugins: [MyPlugin] }); + await node1.pm.get(MyPlugin).sendSyncMessage('message1'); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + await node2.pm.get(MyPlugin).sendSyncMessage('message2'); + expect(mockListener).toBeCalledTimes(2); + expect(mockListener).toHaveBeenCalledWith('message2'); + }); +}); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 88e62f5a07..9e97e1b1a0 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -60,8 +60,9 @@ import { dataTemplate } from './middlewares/data-template'; import validateFilterParams from './middlewares/validate-filter-params'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; -import { PubSubManager } from './pub-sub-manager'; +import { PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; import { SyncManager } from './sync-manager'; +import { SyncMessageManager } from './sync-message-manager'; import packageJson from '../package.json'; @@ -99,7 +100,8 @@ export interface ApplicationOptions { */ resourcer?: ResourceManagerOptions; resourceManager?: ResourceManagerOptions; - pubSubManager?: any; + pubSubManager?: PubSubManagerOptions; + syncMessageManager?: any; bodyParser?: any; cors?: any; dataWrapping?: boolean; @@ -230,6 +232,7 @@ export class Application exten */ public syncManager: SyncManager; public pubSubManager: PubSubManager; + public syncMessageManager: SyncMessageManager; public requestLogger: Logger; private sqlLogger: Logger; protected _logger: SystemLogger; @@ -1129,7 +1132,11 @@ export class Application exten this._cli = this.createCLI(); this._i18n = createI18n(options); this.syncManager = new SyncManager(this); - this.pubSubManager = PubSubManager.create(this, options.pubSubManager); + this.pubSubManager = PubSubManager.create(this, { + basename: this.name, + ...options.pubSubManager, + }); + this.syncMessageManager = new SyncMessageManager(this, options.syncMessageManager); this.context.db = this.db; /** diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index a0814ae9af..917e5585b7 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -151,9 +151,9 @@ export abstract class Plugin implements PluginInterface { async handleSyncMessage(message) {} async sendSyncMessage(message) { if (!this.name) { - return; + throw new Error(`plugin name invalid`); } - await this.app.pubSubManager.publish(this.name, message); + await this.app.syncMessageManager.publish(this.name, message); } /** diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index 398c9b14f5..f95f8d6eab 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -16,8 +16,12 @@ export interface PubSubManagerOptions { basename?: string; } -export interface PubSubManagerCallbackOptions { +export interface PubSubManagerPublishOptions { skipSelf?: boolean; + onlySelf?: boolean; +} + +export interface PubSubManagerSubscribeOptions { debounce?: number; } @@ -36,14 +40,6 @@ export class PubSubManager { app.on('afterStop', async () => { await pubSubManager.close(); }); - app.on('beforeLoadPlugin', async (plugin) => { - if (!plugin.name) { - return; - } - await pubSubManager.subscribe(plugin.name, plugin.handleSyncMessage.bind(plugin), { - debounce: Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000), - }); - }); return pubSubManager; } @@ -85,11 +81,13 @@ export class PubSubManager { return crypto.createHash('sha256').update(JSON.stringify(message)).digest('hex'); } - async subscribe(channel: string, callback, options: PubSubManagerCallbackOptions = {}) { - const { skipSelf = true, debounce = 0 } = options; + async subscribe(channel: string, callback, options: PubSubManagerSubscribeOptions = {}) { + const { debounce = 0 } = options; const wrappedCallback = async (wrappedMessage) => { - const { publisherId, message } = JSON.parse(wrappedMessage); - if (skipSelf && publisherId === this.publisherId) { + const { onlySelf, skipSelf, publisherId, message } = JSON.parse(wrappedMessage); + if (onlySelf && publisherId !== this.publisherId) { + return; + } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { return; } if (!debounce) { @@ -131,37 +129,33 @@ export class PubSubManager { return this.adapter.unsubscribe(`${this.basename}${channel}`, fn); } - async publish(channel, message) { + async publish(channel, message, options?: PubSubManagerPublishOptions) { if (!this.adapter) { return; } const wrappedMessage = JSON.stringify({ publisherId: this.publisherId, + ...options, message: message, }); return this.adapter.publish(`${this.basename}${channel}`, wrappedMessage); } - protected debounce(func, wait: number) { - if (wait) { - return _.debounce(func, wait); - } - return func; - } - - async subscribeAll(callback, options: PubSubManagerCallbackOptions = {}) { + async subscribeAll(callback, options: PubSubManagerSubscribeOptions = {}) { if (!this.adapter) { return; } - const { skipSelf = true, debounce = 0 } = options; + const { debounce = 0 } = options; return this.adapter.subscribeAll(async (channel: string, wrappedMessage) => { if (!channel.startsWith(this.basename)) { return; } - const { publisherId, message } = JSON.parse(wrappedMessage); - if (skipSelf && publisherId === this.publisherId) { + const { onlySelf, skipSelf, publisherId, message } = JSON.parse(wrappedMessage); + if (onlySelf && publisherId !== this.publisherId) { + return; + } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { return; } const realChannel = channel.substring(this.basename.length); @@ -178,6 +172,13 @@ export class PubSubManager { this.messageHanders.delete(messageHash); }); } + + protected debounce(func, wait: number) { + if (wait) { + return _.debounce(func, wait); + } + return func; + } } export interface IPubSubAdapter { diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts new file mode 100644 index 0000000000..c0f4ebc524 --- /dev/null +++ b/packages/core/server/src/sync-message-manager.ts @@ -0,0 +1,49 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import Application from './application'; +import { PubSubManager, PubSubManagerPublishOptions } from './pub-sub-manager'; + +export class SyncMessageManager { + protected versionManager: SyncMessageVersionManager; + protected pubSubManager: PubSubManager; + + constructor( + protected app: Application, + protected options: any = {}, + ) { + this.versionManager = new SyncMessageVersionManager(); + app.on('beforeLoadPlugin', async (plugin) => { + if (!plugin.name) { + return; + } + await this.subscribe(plugin.name, plugin.handleSyncMessage.bind(plugin)); + }); + } + + get debounce() { + return this.options.debounce || 1000; + } + + async publish(channel: string, message, options?: PubSubManagerPublishOptions) { + await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { skipSelf: true, ...options }); + } + + async subscribe(channel: string, callback) { + await this.app.pubSubManager.subscribe(`${this.app.name}.sync.${channel}`, callback, { debounce: this.debounce }); + } + + async sync() { + // TODO + } +} + +export class SyncMessageVersionManager { + // TODO +} diff --git a/packages/core/test/src/server/memory-pub-sub-adapter.ts b/packages/core/test/src/server/memory-pub-sub-adapter.ts index 2ccdcba557..1d2151196b 100644 --- a/packages/core/test/src/server/memory-pub-sub-adapter.ts +++ b/packages/core/test/src/server/memory-pub-sub-adapter.ts @@ -25,17 +25,17 @@ export class MemoryPubSubAdapter implements IPubSubAdapter { static instances = new Map(); - static create(name?: string) { + static create(name?: string, options?: any) { if (!name) { name = uid(); } if (!this.instances.has(name)) { - this.instances.set(name, new MemoryPubSubAdapter()); + this.instances.set(name, new MemoryPubSubAdapter(options)); } return this.instances.get(name); } - constructor() { + constructor(protected options: any = {}) { this.emitter = new TestEventEmitter(); } @@ -56,12 +56,16 @@ export class MemoryPubSubAdapter implements IPubSubAdapter { } async publish(channel, message) { + console.log(this.connected, { channel, message }); if (!this.connected) { return; } await this.emitter.emitAsync(channel, message); await this.emitter.emitAsync('__publish__', channel, message); - await sleep(Number(process.env.PUB_SUB_DEFAULT_DEBOUNCE || 1000)); + // 用于处理延迟问题 + if (this.options.debounce) { + await sleep(Number(this.options.debounce)); + } } async subscribeAll(callback) { diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 86c3dbcb58..e0e51315ba 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -10,6 +10,7 @@ import { mockDatabase } from '@nocobase/database'; import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server'; import jwt from 'jsonwebtoken'; +import _ from 'lodash'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; import { MemoryPubSubAdapter } from './memory-pub-sub-adapter'; @@ -231,12 +232,19 @@ export function mockServer(options: ApplicationOptions = {}) { const app = new MockServer({ acl: false, + syncMessageManager: { + debounce: 1000, + }, ...options, }); - if (options.pubSubManager) { - app.pubSubManager.setAdapter(MemoryPubSubAdapter.create(options.pubSubManager?.basename)); - } + const basename = app.options.pubSubManager?.basename || app.name; + + app.pubSubManager.setAdapter( + MemoryPubSubAdapter.create(basename, { + debounce: 1000, + }), + ); return app; } @@ -249,6 +257,30 @@ export async function startMockServer(options: ApplicationOptions = {}) { type BeforeInstallFn = (app) => Promise; +export async function createMultiMockServer( + options: ApplicationOptions & { + number?: number; + version?: string; + basename?: string; + beforeInstall?: BeforeInstallFn; + skipInstall?: boolean; + skipStart?: boolean; + } = {}, +) { + const instances: MockServer[] = []; + for (const i of _.range(0, options.number || 2)) { + const app: MockServer = await createMockServer({ + ...options, + skipSupervisor: true, + pubSubManager: { + basename: options.basename, + }, + }); + instances.push(app); + } + return instances; +} + export async function createMockServer( options: ApplicationOptions & { version?: string; From 96f20e3b2f4c3d21632c457a44aeeab93e77623f Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 16:51:39 +0800 Subject: [PATCH 11/70] fix: improve code --- .../src/__tests__/sync-message-manager.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/core/server/src/__tests__/sync-message-manager.test.ts b/packages/core/server/src/__tests__/sync-message-manager.test.ts index b26db4c324..c3fd9f8af3 100644 --- a/packages/core/server/src/__tests__/sync-message-manager.test.ts +++ b/packages/core/server/src/__tests__/sync-message-manager.test.ts @@ -9,6 +9,7 @@ import { Plugin } from '@nocobase/server'; import { createMultiMockServer } from '@nocobase/test'; +import { uid } from '@nocobase/utils'; describe('sync-message-manager', () => { test('subscribe + publish', async () => { @@ -34,12 +35,18 @@ describe('sync-message-manager', () => { mockListener(message); } } - const [node1, node2] = await createMultiMockServer({ basename: 'base1', plugins: [MyPlugin] }); - await node1.pm.get(MyPlugin).sendSyncMessage('message1'); + const [app1, app2] = await createMultiMockServer({ + basename: uid(), + number: 2, // 创建几个 app 实例 + plugins: [MyPlugin], + }); + await app1.pm.get(MyPlugin).sendSyncMessage('message1'); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); - await node2.pm.get(MyPlugin).sendSyncMessage('message2'); + await app2.pm.get(MyPlugin).sendSyncMessage('message2'); expect(mockListener).toBeCalledTimes(2); expect(mockListener).toHaveBeenCalledWith('message2'); + await app1.destroy(); + await app2.destroy(); }); }); From 2a19484e8f12b11688a1e563bd1c45240d7e763d Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 20:24:58 +0800 Subject: [PATCH 12/70] fix: improve code --- .../src/__tests__/pub-sub-manager.test.ts | 10 +- .../__tests__/sync-message-manager.test.ts | 49 +++++++- packages/core/server/src/application.ts | 2 +- packages/core/server/src/plugin.ts | 8 +- packages/core/server/src/pub-sub-manager.ts | 111 ++++++++++-------- .../core/server/src/sync-message-manager.ts | 34 +++++- .../test/src/server/memory-pub-sub-adapter.ts | 2 +- packages/core/test/src/server/mock-server.ts | 4 +- 8 files changed, 157 insertions(+), 63 deletions(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 565ea2725c..c9ce16b365 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -14,7 +14,7 @@ describe('connect', () => { let pubSubManager: PubSubManager; beforeEach(async () => { - pubSubManager = new PubSubManager({ basename: 'pubsub1' }); + pubSubManager = new PubSubManager({ channelPrefix: 'pubsub1' }); pubSubManager.setAdapter(new MemoryPubSubAdapter()); }); @@ -63,7 +63,7 @@ describe('skipSelf, unsubscribe, debounce', () => { let pubSubManager: PubSubManager; beforeEach(async () => { - pubSubManager = new PubSubManager({ basename: 'pubsub1' }); + pubSubManager = new PubSubManager({ channelPrefix: 'pubsub1' }); pubSubManager.setAdapter(new MemoryPubSubAdapter()); await pubSubManager.connect(); }); @@ -180,10 +180,10 @@ describe('Pub/Sub', () => { beforeEach(async () => { const pubsub = new MemoryPubSubAdapter(); - publisher = new PubSubManager({ basename: 'pubsub1' }); + publisher = new PubSubManager({ channelPrefix: 'pubsub1' }); publisher.setAdapter(pubsub); await publisher.connect(); - subscriber = new PubSubManager({ basename: 'pubsub1' }); + subscriber = new PubSubManager({ channelPrefix: 'pubsub1' }); subscriber.setAdapter(pubsub); await subscriber.connect(); }); @@ -230,7 +230,7 @@ describe('app.pubSubManager', () => { beforeEach(async () => { app = await createMockServer({ pubSubManager: { - basename: 'app1', + channelPrefix: 'app1', }, }); pubSubManager = app.pubSubManager; diff --git a/packages/core/server/src/__tests__/sync-message-manager.test.ts b/packages/core/server/src/__tests__/sync-message-manager.test.ts index c3fd9f8af3..3c43567514 100644 --- a/packages/core/server/src/__tests__/sync-message-manager.test.ts +++ b/packages/core/server/src/__tests__/sync-message-manager.test.ts @@ -8,7 +8,7 @@ */ import { Plugin } from '@nocobase/server'; -import { createMultiMockServer } from '@nocobase/test'; +import { createMultiMockServer, sleep } from '@nocobase/test'; import { uid } from '@nocobase/utils'; describe('sync-message-manager', () => { @@ -25,6 +25,23 @@ describe('sync-message-manager', () => { await node2.destroy(); }); + test('transaction', async () => { + const [node1, node2] = await createMultiMockServer({ basename: 'base1' }); + const mockListener = vi.fn(); + await node1.syncMessageManager.subscribe('test1', mockListener); + const transaction = await node2.db.sequelize.transaction(); + node2.syncMessageManager.publish('test1', 'message1', { transaction }); + await sleep(1000); + expect(mockListener).not.toHaveBeenCalled(); + await transaction.commit(); + await sleep(1100); + expect(mockListener).toHaveBeenCalled(); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + await node1.destroy(); + await node2.destroy(); + }); + test('plugin.handleSyncMessage', async () => { const mockListener = vi.fn(); class MyPlugin extends Plugin { @@ -49,4 +66,34 @@ describe('sync-message-manager', () => { await app1.destroy(); await app2.destroy(); }); + + test('plugin.handleSyncMessage + transaction', async () => { + const mockListener = vi.fn(); + class MyPlugin extends Plugin { + get name() { + return 'test1'; + } + async handleSyncMessage(message) { + mockListener(message); + } + } + const [app1, app2] = await createMultiMockServer({ + basename: uid(), + number: 2, // 创建几个 app 实例 + plugins: [MyPlugin], + }); + const transaction = await app1.db.sequelize.transaction(); + app1.pm.get(MyPlugin).sendSyncMessage('message1', { transaction }); + await sleep(1000); + expect(mockListener).not.toHaveBeenCalled(); + await transaction.commit(); + await sleep(1100); + expect(mockListener).toBeCalledTimes(1); + expect(mockListener).toHaveBeenCalledWith('message1'); + await app2.pm.get(MyPlugin).sendSyncMessage('message2'); + expect(mockListener).toBeCalledTimes(2); + expect(mockListener).toHaveBeenCalledWith('message2'); + await app1.destroy(); + await app2.destroy(); + }); }); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 9e97e1b1a0..5f929e87cd 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -1133,7 +1133,7 @@ export class Application exten this._i18n = createI18n(options); this.syncManager = new SyncManager(this); this.pubSubManager = PubSubManager.create(this, { - basename: this.name, + channelPrefix: this.name, ...options.pubSubManager, }); this.syncMessageManager = new SyncMessageManager(this, options.syncMessageManager); diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index 917e5585b7..144a4de07b 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -9,7 +9,7 @@ /* istanbul ignore file -- @preserve */ -import { Model } from '@nocobase/database'; +import { Model, Transactionable } from '@nocobase/database'; import { LoggerOptions } from '@nocobase/logger'; import { fsExists } from '@nocobase/utils'; import fs from 'fs'; @@ -148,12 +148,12 @@ export abstract class Plugin implements PluginInterface { this.app.syncManager.publish(this.name, message); } - async handleSyncMessage(message) {} - async sendSyncMessage(message) { + async handleSyncMessage(message: any) {} + async sendSyncMessage(message: any, options?: Transactionable) { if (!this.name) { throw new Error(`plugin name invalid`); } - await this.app.syncMessageManager.publish(this.name, message); + await this.app.syncMessageManager.publish(this.name, message, options); } /** diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index f95f8d6eab..7a81a9ec31 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -13,7 +13,7 @@ import _ from 'lodash'; import Application from './application'; export interface PubSubManagerOptions { - basename?: string; + channelPrefix?: string; } export interface PubSubManagerPublishOptions { @@ -30,7 +30,6 @@ export class PubSubManager { messageHanders = new Map(); subscribes = new Map(); publisherId: string; - connected: boolean; static create(app: Application, options: PubSubManagerOptions) { const pubSubManager = new PubSubManager(options); @@ -47,8 +46,12 @@ export class PubSubManager { this.publisherId = uid(); } - get basename() { - return this.options?.basename ? `${this.options.basename}.` : ''; + get channelPrefix() { + return this.options?.channelPrefix ? `${this.options.channelPrefix}.` : ''; + } + + get connected() { + return this.adapter?.connected || false; } setAdapter(adapter: IPubSubAdapter) { @@ -59,12 +62,10 @@ export class PubSubManager { if (!this.adapter) { return; } - this.connected = true; await this.adapter.connect(); - // subscribe 要在 connect 之后 for (const [channel, callbacks] of this.subscribes) { for (const [, fn] of callbacks) { - await this.adapter.subscribe(`${this.basename}${channel}`, fn); + await this.adapter.subscribe(`${this.channelPrefix}${channel}`, fn); } } } @@ -73,34 +74,26 @@ export class PubSubManager { if (!this.adapter) { return; } - this.connected = false; return await this.adapter.close(); } - getMessageHash(message) { - return crypto.createHash('sha256').update(JSON.stringify(message)).digest('hex'); + async getMessageHash(message) { + const encoder = new TextEncoder(); + const data = encoder.encode(JSON.stringify(message)); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + return hashHex; } async subscribe(channel: string, callback, options: PubSubManagerSubscribeOptions = {}) { const { debounce = 0 } = options; const wrappedCallback = async (wrappedMessage) => { - const { onlySelf, skipSelf, publisherId, message } = JSON.parse(wrappedMessage); - if (onlySelf && publisherId !== this.publisherId) { - return; - } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { + const json = JSON.parse(wrappedMessage); + if (!this.verifyMessage(json)) { return; } - if (!debounce) { - await callback(message); - return; - } - const messageHash = '__subscribe__' + channel + this.getMessageHash(message); - if (!this.messageHanders.has(messageHash)) { - this.messageHanders.set(messageHash, this.debounce(callback, debounce)); - } - const handleMessage = this.messageHanders.get(messageHash); - await handleMessage(message); - this.messageHanders.delete(messageHash); + await this.handleMessage({ channel, message: json.message, debounce, callback }); }; if (!this.subscribes.has(channel)) { const map = new Map(); @@ -109,11 +102,11 @@ export class PubSubManager { const map: Map = this.subscribes.get(channel); const previous = map.get(callback); if (previous) { - await this.adapter.unsubscribe(`${this.basename}${channel}`, previous); + await this.adapter.unsubscribe(`${this.channelPrefix}${channel}`, previous); } map.set(callback, wrappedCallback); if (this.connected) { - await this.adapter.subscribe(`${this.basename}${channel}`, wrappedCallback); + await this.adapter.subscribe(`${this.channelPrefix}${channel}`, wrappedCallback); } } @@ -122,11 +115,12 @@ export class PubSubManager { let fn = null; if (map) { fn = map.get(callback); + map.delete(callback); } if (!this.adapter || !fn) { return; } - return this.adapter.unsubscribe(`${this.basename}${channel}`, fn); + return this.adapter.unsubscribe(`${this.channelPrefix}${channel}`, fn); } async publish(channel, message, options?: PubSubManagerPublishOptions) { @@ -140,7 +134,7 @@ export class PubSubManager { message: message, }); - return this.adapter.publish(`${this.basename}${channel}`, wrappedMessage); + return this.adapter.publish(`${this.channelPrefix}${channel}`, wrappedMessage); } async subscribeAll(callback, options: PubSubManagerSubscribeOptions = {}) { @@ -149,30 +143,54 @@ export class PubSubManager { } const { debounce = 0 } = options; return this.adapter.subscribeAll(async (channel: string, wrappedMessage) => { - if (!channel.startsWith(this.basename)) { + if (!channel.startsWith(this.channelPrefix)) { return; } - const { onlySelf, skipSelf, publisherId, message } = JSON.parse(wrappedMessage); - if (onlySelf && publisherId !== this.publisherId) { - return; - } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { + const json = JSON.parse(wrappedMessage); + if (!this.verifyMessage(json)) { return; } - const realChannel = channel.substring(this.basename.length); - if (!debounce) { - await callback(realChannel, message); - return; - } - const messageHash = '__subscribe_all__' + realChannel + this.getMessageHash(message); - if (!this.messageHanders.has(messageHash)) { - this.messageHanders.set(messageHash, this.debounce(callback, debounce)); - } - const handleMessage = this.messageHanders.get(messageHash); - await handleMessage(realChannel, message); - this.messageHanders.delete(messageHash); + const realChannel = channel.substring(this.channelPrefix.length); + await this.handleMessage({ + callback, + debounce, + subscribeAll: true, + channel: realChannel, + message: json.message, + }); }); } + protected async handleMessage({ channel, message, callback, debounce, subscribeAll = false }) { + const args = subscribeAll ? [channel, message] : [message]; + if (!debounce) { + await callback(...args); + return; + } + const prefix = subscribeAll ? '__subscribe_all__' : '__subscribe__'; + const messageHash = prefix + channel + (await this.getMessageHash(message)); + if (!this.messageHanders.has(messageHash)) { + this.messageHanders.set(messageHash, this.debounce(callback, debounce)); + } + const handleMessage = this.messageHanders.get(messageHash); + try { + const args = subscribeAll ? [channel, message] : [message]; + await handleMessage(...args); + } catch (error) { + this.messageHanders.delete(messageHash); + throw error; + } + } + + protected verifyMessage({ onlySelf, skipSelf, publisherId }) { + if (onlySelf && publisherId !== this.publisherId) { + return; + } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { + return; + } + return true; + } + protected debounce(func, wait: number) { if (wait) { return _.debounce(func, wait); @@ -182,6 +200,7 @@ export class PubSubManager { } export interface IPubSubAdapter { + connected?: boolean; connect(): Promise; close(): Promise; subscribe(channel: string, callback): Promise; diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts index c0f4ebc524..e92840513d 100644 --- a/packages/core/server/src/sync-message-manager.ts +++ b/packages/core/server/src/sync-message-manager.ts @@ -7,6 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { Transactionable } from '@nocobase/database'; import Application from './application'; import { PubSubManager, PubSubManagerPublishOptions } from './pub-sub-manager'; @@ -31,12 +32,39 @@ export class SyncMessageManager { return this.options.debounce || 1000; } - async publish(channel: string, message, options?: PubSubManagerPublishOptions) { - await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { skipSelf: true, ...options }); + async publish(channel: string, message, options?: PubSubManagerPublishOptions & Transactionable) { + const { transaction, ...others } = options || {}; + if (transaction) { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error('publish timeout')); + }, 5000); + transaction.afterCommit(async () => { + try { + const r = await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { + skipSelf: true, + ...others, + }); + clearTimeout(timer); + resolve(r); + } catch (error) { + clearTimeout(timer); + reject(error); + } + }); + }); + } else { + return await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { + skipSelf: true, + ...options, + }); + } } async subscribe(channel: string, callback) { - await this.app.pubSubManager.subscribe(`${this.app.name}.sync.${channel}`, callback, { debounce: this.debounce }); + return await this.app.pubSubManager.subscribe(`${this.app.name}.sync.${channel}`, callback, { + debounce: this.debounce, + }); } async sync() { diff --git a/packages/core/test/src/server/memory-pub-sub-adapter.ts b/packages/core/test/src/server/memory-pub-sub-adapter.ts index 1d2151196b..f9cf0b4993 100644 --- a/packages/core/test/src/server/memory-pub-sub-adapter.ts +++ b/packages/core/test/src/server/memory-pub-sub-adapter.ts @@ -21,7 +21,7 @@ applyMixins(TestEventEmitter, [AsyncEmitter]); export class MemoryPubSubAdapter implements IPubSubAdapter { protected emitter: TestEventEmitter; - protected connected = false; + connected = false; static instances = new Map(); diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index e0e51315ba..452ab94a01 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -238,7 +238,7 @@ export function mockServer(options: ApplicationOptions = {}) { ...options, }); - const basename = app.options.pubSubManager?.basename || app.name; + const basename = app.options.pubSubManager?.channelPrefix || app.name; app.pubSubManager.setAdapter( MemoryPubSubAdapter.create(basename, { @@ -273,7 +273,7 @@ export async function createMultiMockServer( ...options, skipSupervisor: true, pubSubManager: { - basename: options.basename, + channelPrefix: options.basename, }, }); instances.push(app); From d13b08be29329f42cfcad09c8ef32a85ffb97fc7 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 21:23:21 +0800 Subject: [PATCH 13/70] feat: improve code --- .../__tests__/sync-message-manager.test.ts | 28 ++++++++----------- packages/core/test/src/server/mock-server.ts | 20 ++++++++++--- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/core/server/src/__tests__/sync-message-manager.test.ts b/packages/core/server/src/__tests__/sync-message-manager.test.ts index 3c43567514..31a35921c1 100644 --- a/packages/core/server/src/__tests__/sync-message-manager.test.ts +++ b/packages/core/server/src/__tests__/sync-message-manager.test.ts @@ -8,12 +8,12 @@ */ import { Plugin } from '@nocobase/server'; -import { createMultiMockServer, sleep } from '@nocobase/test'; -import { uid } from '@nocobase/utils'; +import { createMockCluster, sleep } from '@nocobase/test'; describe('sync-message-manager', () => { test('subscribe + publish', async () => { - const [node1, node2] = await createMultiMockServer({ basename: 'base1' }); + const cluster = await createMockCluster(); + const [node1, node2] = cluster.instances; const mockListener = vi.fn(); await node1.syncMessageManager.subscribe('test1', mockListener); await node2.syncMessageManager.subscribe('test1', mockListener); @@ -21,12 +21,12 @@ describe('sync-message-manager', () => { expect(mockListener).toHaveBeenCalled(); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); - await node1.destroy(); - await node2.destroy(); + await cluster.destroy(); }); test('transaction', async () => { - const [node1, node2] = await createMultiMockServer({ basename: 'base1' }); + const cluster = await createMockCluster(); + const [node1, node2] = cluster.instances; const mockListener = vi.fn(); await node1.syncMessageManager.subscribe('test1', mockListener); const transaction = await node2.db.sequelize.transaction(); @@ -38,8 +38,7 @@ describe('sync-message-manager', () => { expect(mockListener).toHaveBeenCalled(); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); - await node1.destroy(); - await node2.destroy(); + await cluster.destroy(); }); test('plugin.handleSyncMessage', async () => { @@ -52,19 +51,17 @@ describe('sync-message-manager', () => { mockListener(message); } } - const [app1, app2] = await createMultiMockServer({ - basename: uid(), - number: 2, // 创建几个 app 实例 + const cluster = await createMockCluster({ plugins: [MyPlugin], }); + const [app1, app2] = cluster.instances; await app1.pm.get(MyPlugin).sendSyncMessage('message1'); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); await app2.pm.get(MyPlugin).sendSyncMessage('message2'); expect(mockListener).toBeCalledTimes(2); expect(mockListener).toHaveBeenCalledWith('message2'); - await app1.destroy(); - await app2.destroy(); + await cluster.destroy(); }); test('plugin.handleSyncMessage + transaction', async () => { @@ -77,11 +74,10 @@ describe('sync-message-manager', () => { mockListener(message); } } - const [app1, app2] = await createMultiMockServer({ - basename: uid(), - number: 2, // 创建几个 app 实例 + const cluster = await createMockCluster({ plugins: [MyPlugin], }); + const [app1, app2] = cluster.instances; const transaction = await app1.db.sequelize.transaction(); app1.pm.get(MyPlugin).sendSyncMessage('message1', { transaction }); await sleep(1000); diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 452ab94a01..244c135aab 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -9,6 +9,7 @@ import { mockDatabase } from '@nocobase/database'; import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server'; +import { uid } from '@nocobase/utils'; import jwt from 'jsonwebtoken'; import _ from 'lodash'; import qs from 'qs'; @@ -257,28 +258,39 @@ export async function startMockServer(options: ApplicationOptions = {}) { type BeforeInstallFn = (app) => Promise; -export async function createMultiMockServer( +export async function createMockCluster( options: ApplicationOptions & { number?: number; version?: string; - basename?: string; + name?: string; + appName?: string; beforeInstall?: BeforeInstallFn; skipInstall?: boolean; skipStart?: boolean; } = {}, ) { const instances: MockServer[] = []; + const clusterName = options.name || `cluster_${uid()}`; + const appName = options.appName || `app_${uid()}`; for (const i of _.range(0, options.number || 2)) { const app: MockServer = await createMockServer({ ...options, skipSupervisor: true, + name: clusterName + '_' + appName, pubSubManager: { - channelPrefix: options.basename, + channelPrefix: clusterName, }, }); instances.push(app); } - return instances; + return { + instances, + async destroy() { + for (const instance of instances) { + await instance.destroy(); + } + }, + }; } export async function createMockServer( From 56cd2336949f962b1cb89203b11d475b62a7a994 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 21:26:04 +0800 Subject: [PATCH 14/70] fix: improve code --- .../server/src/__tests__/sync-message-manager.test.ts | 8 ++++---- packages/core/test/src/server/mock-server.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/server/src/__tests__/sync-message-manager.test.ts b/packages/core/server/src/__tests__/sync-message-manager.test.ts index 31a35921c1..0fef6d7afd 100644 --- a/packages/core/server/src/__tests__/sync-message-manager.test.ts +++ b/packages/core/server/src/__tests__/sync-message-manager.test.ts @@ -13,7 +13,7 @@ import { createMockCluster, sleep } from '@nocobase/test'; describe('sync-message-manager', () => { test('subscribe + publish', async () => { const cluster = await createMockCluster(); - const [node1, node2] = cluster.instances; + const [node1, node2] = cluster.nodes; const mockListener = vi.fn(); await node1.syncMessageManager.subscribe('test1', mockListener); await node2.syncMessageManager.subscribe('test1', mockListener); @@ -26,7 +26,7 @@ describe('sync-message-manager', () => { test('transaction', async () => { const cluster = await createMockCluster(); - const [node1, node2] = cluster.instances; + const [node1, node2] = cluster.nodes; const mockListener = vi.fn(); await node1.syncMessageManager.subscribe('test1', mockListener); const transaction = await node2.db.sequelize.transaction(); @@ -54,7 +54,7 @@ describe('sync-message-manager', () => { const cluster = await createMockCluster({ plugins: [MyPlugin], }); - const [app1, app2] = cluster.instances; + const [app1, app2] = cluster.nodes; await app1.pm.get(MyPlugin).sendSyncMessage('message1'); expect(mockListener).toBeCalledTimes(1); expect(mockListener).toHaveBeenCalledWith('message1'); @@ -77,7 +77,7 @@ describe('sync-message-manager', () => { const cluster = await createMockCluster({ plugins: [MyPlugin], }); - const [app1, app2] = cluster.instances; + const [app1, app2] = cluster.nodes; const transaction = await app1.db.sequelize.transaction(); app1.pm.get(MyPlugin).sendSyncMessage('message1', { transaction }); await sleep(1000); diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 244c135aab..fd563f50a0 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -269,7 +269,7 @@ export async function createMockCluster( skipStart?: boolean; } = {}, ) { - const instances: MockServer[] = []; + const nodes: MockServer[] = []; const clusterName = options.name || `cluster_${uid()}`; const appName = options.appName || `app_${uid()}`; for (const i of _.range(0, options.number || 2)) { @@ -281,13 +281,13 @@ export async function createMockCluster( channelPrefix: clusterName, }, }); - instances.push(app); + nodes.push(app); } return { - instances, + nodes, async destroy() { - for (const instance of instances) { - await instance.destroy(); + for (const node of nodes) { + await node.destroy(); } }, }; From 9cc5a3699bffa4fcd7c5f30aab44c4085904ff96 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 21:31:17 +0800 Subject: [PATCH 15/70] fix: test case --- .../core/server/src/__tests__/sync-message-manager.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/server/src/__tests__/sync-message-manager.test.ts b/packages/core/server/src/__tests__/sync-message-manager.test.ts index 0fef6d7afd..6d1426c2a1 100644 --- a/packages/core/server/src/__tests__/sync-message-manager.test.ts +++ b/packages/core/server/src/__tests__/sync-message-manager.test.ts @@ -89,7 +89,6 @@ describe('sync-message-manager', () => { await app2.pm.get(MyPlugin).sendSyncMessage('message2'); expect(mockListener).toBeCalledTimes(2); expect(mockListener).toHaveBeenCalledWith('message2'); - await app1.destroy(); - await app2.destroy(); + await cluster.destroy(); }); }); From a97240a10b19d4383261ecde286a82f78c1347af Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 21:33:52 +0800 Subject: [PATCH 16/70] fix: typo --- packages/core/server/src/pub-sub-manager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index 7a81a9ec31..80b6f4b90e 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -27,7 +27,7 @@ export interface PubSubManagerSubscribeOptions { export class PubSubManager { adapter: IPubSubAdapter; - messageHanders = new Map(); + messageHandlers = new Map(); subscribes = new Map(); publisherId: string; @@ -169,15 +169,15 @@ export class PubSubManager { } const prefix = subscribeAll ? '__subscribe_all__' : '__subscribe__'; const messageHash = prefix + channel + (await this.getMessageHash(message)); - if (!this.messageHanders.has(messageHash)) { - this.messageHanders.set(messageHash, this.debounce(callback, debounce)); + if (!this.messageHandlers.has(messageHash)) { + this.messageHandlers.set(messageHash, this.debounce(callback, debounce)); } - const handleMessage = this.messageHanders.get(messageHash); + const handleMessage = this.messageHandlers.get(messageHash); try { const args = subscribeAll ? [channel, message] : [message]; await handleMessage(...args); } catch (error) { - this.messageHanders.delete(messageHash); + this.messageHandlers.delete(messageHash); throw error; } } From e9679b6f86d43a4c7f910e3721de826ba9b91185 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 25 Jul 2024 21:40:12 +0800 Subject: [PATCH 17/70] fix: createPubSubManager --- packages/core/server/src/application.ts | 4 ++-- packages/core/server/src/pub-sub-manager.ts | 22 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 5f929e87cd..da22ab1659 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -60,7 +60,7 @@ import { dataTemplate } from './middlewares/data-template'; import validateFilterParams from './middlewares/validate-filter-params'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; -import { PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; +import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; import { SyncManager } from './sync-manager'; import { SyncMessageManager } from './sync-message-manager'; @@ -1132,7 +1132,7 @@ export class Application exten this._cli = this.createCLI(); this._i18n = createI18n(options); this.syncManager = new SyncManager(this); - this.pubSubManager = PubSubManager.create(this, { + this.pubSubManager = createPubSubManager(this, { channelPrefix: this.name, ...options.pubSubManager, }); diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index 80b6f4b90e..a76e0864d2 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -25,23 +25,23 @@ export interface PubSubManagerSubscribeOptions { debounce?: number; } +export const createPubSubManager = (app: Application, options: PubSubManagerOptions) => { + const pubSubManager = new PubSubManager(options); + app.on('afterStart', async () => { + await pubSubManager.connect(); + }); + app.on('afterStop', async () => { + await pubSubManager.close(); + }); + return pubSubManager; +}; + export class PubSubManager { adapter: IPubSubAdapter; messageHandlers = new Map(); subscribes = new Map(); publisherId: string; - static create(app: Application, options: PubSubManagerOptions) { - const pubSubManager = new PubSubManager(options); - app.on('afterStart', async () => { - await pubSubManager.connect(); - }); - app.on('afterStop', async () => { - await pubSubManager.close(); - }); - return pubSubManager; - } - constructor(protected options: PubSubManagerOptions = {}) { this.publisherId = uid(); } From 2897e7ceef4b1547042121b09d3add7de676b842 Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 26 Jul 2024 17:34:10 +0800 Subject: [PATCH 18/70] fix: delete messageHandlers --- packages/core/server/src/pub-sub-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index a76e0864d2..ddfe3dd57e 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -176,6 +176,7 @@ export class PubSubManager { try { const args = subscribeAll ? [channel, message] : [message]; await handleMessage(...args); + this.messageHandlers.delete(messageHash); } catch (error) { this.messageHandlers.delete(messageHash); throw error; From 2f9f7c7392bd77cae2de3c27408cf900c51f3234 Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 26 Jul 2024 22:16:23 +0800 Subject: [PATCH 19/70] fix: test case --- .../core/server/src/__tests__/pub-sub-manager.test.ts | 8 ++++++++ packages/core/server/src/pub-sub-manager.ts | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index c9ce16b365..5304e83c1e 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -103,7 +103,12 @@ describe('skipSelf, unsubscribe, debounce', () => { pubSubManager.publish('test1', 'message2'); pubSubManager.publish('test1', 'message2'); pubSubManager.publish('test1', 'message2'); + await sleep(500); + //@ts-ignore + expect(pubSubManager['messageHandlers'].size).toBe(2); await sleep(2000); + //@ts-ignore + expect(pubSubManager['messageHandlers'].size).toBe(0); expect(mockListener).toBeCalledTimes(2); }); @@ -116,7 +121,10 @@ describe('skipSelf, unsubscribe, debounce', () => { pubSubManager.publish('test1', 'message2'); pubSubManager.publish('test2', 'message2'); pubSubManager.publish('test2', 'message2'); + await sleep(500); + expect(pubSubManager['messageHandlers'].size).toBe(3); await sleep(2000); + expect(pubSubManager['messageHandlers'].size).toBe(0); expect(mockListener).toBeCalledTimes(3); }); diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts index ddfe3dd57e..ce4960bc42 100644 --- a/packages/core/server/src/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager.ts @@ -176,7 +176,9 @@ export class PubSubManager { try { const args = subscribeAll ? [channel, message] : [message]; await handleMessage(...args); - this.messageHandlers.delete(messageHash); + setTimeout(() => { + this.messageHandlers.delete(messageHash); + }, debounce); } catch (error) { this.messageHandlers.delete(messageHash); throw error; From 90ba426766e0fc355a94820bd3c746a9a6ba7e65 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 31 Jul 2024 13:28:47 +0800 Subject: [PATCH 20/70] feat: improve code --- .../src/__tests__/pub-sub-manager.test.ts | 41 +--- packages/core/server/src/pub-sub-manager.ts | 213 ------------------ .../src/pub-sub-manager/handler-manager.ts | 118 ++++++++++ .../core/server/src/pub-sub-manager/index.ts | 13 ++ .../src/pub-sub-manager/pub-sub-manager.ts | 104 +++++++++ .../core/server/src/pub-sub-manager/types.ts | 32 +++ .../test/src/server/memory-pub-sub-adapter.ts | 4 + 7 files changed, 274 insertions(+), 251 deletions(-) delete mode 100644 packages/core/server/src/pub-sub-manager.ts create mode 100644 packages/core/server/src/pub-sub-manager/handler-manager.ts create mode 100644 packages/core/server/src/pub-sub-manager/index.ts create mode 100644 packages/core/server/src/pub-sub-manager/pub-sub-manager.ts create mode 100644 packages/core/server/src/pub-sub-manager/types.ts diff --git a/packages/core/server/src/__tests__/pub-sub-manager.test.ts b/packages/core/server/src/__tests__/pub-sub-manager.test.ts index 5304e83c1e..cce18ab041 100644 --- a/packages/core/server/src/__tests__/pub-sub-manager.test.ts +++ b/packages/core/server/src/__tests__/pub-sub-manager.test.ts @@ -104,30 +104,12 @@ describe('skipSelf, unsubscribe, debounce', () => { pubSubManager.publish('test1', 'message2'); pubSubManager.publish('test1', 'message2'); await sleep(500); - //@ts-ignore - expect(pubSubManager['messageHandlers'].size).toBe(2); + expect(pubSubManager['handlerManager']['uniqueMessageHandlers'].size).toBe(2); await sleep(2000); - //@ts-ignore - expect(pubSubManager['messageHandlers'].size).toBe(0); + expect(pubSubManager['handlerManager']['uniqueMessageHandlers'].size).toBe(0); expect(mockListener).toBeCalledTimes(2); }); - test('debounce', async () => { - const mockListener = vi.fn(); - await pubSubManager.subscribeAll(mockListener, { debounce: 1000 }); - pubSubManager.publish('test1', 'message1'); - pubSubManager.publish('test1', 'message1'); - pubSubManager.publish('test1', 'message2'); - pubSubManager.publish('test1', 'message2'); - pubSubManager.publish('test2', 'message2'); - pubSubManager.publish('test2', 'message2'); - await sleep(500); - expect(pubSubManager['messageHandlers'].size).toBe(3); - await sleep(2000); - expect(pubSubManager['messageHandlers'].size).toBe(0); - expect(mockListener).toBeCalledTimes(3); - }); - test('message format', async () => { const mockListener = vi.fn(); await pubSubManager.subscribe('test1', mockListener); @@ -164,22 +146,6 @@ describe('skipSelf, unsubscribe, debounce', () => { await pubSubManager.publish('test1', 'message1'); expect(mockListener).toBeCalledTimes(1); }); - - test('subscribeAll + skipSelf: true', async () => { - const mockListener = vi.fn(); - await pubSubManager.subscribeAll(mockListener); - await pubSubManager.publish('test1', 'message1'); - expect(mockListener).toHaveBeenCalled(); - expect(mockListener).toBeCalledTimes(1); - expect(mockListener).toHaveBeenCalledWith('test1', 'message1'); - }); - - test('publish + skipSelf: false', async () => { - const mockListener = vi.fn(); - await pubSubManager.subscribeAll(mockListener); - await pubSubManager.publish('test1', 'message1', { skipSelf: true }); - expect(mockListener).not.toHaveBeenCalled(); - }); }); describe('Pub/Sub', () => { @@ -249,8 +215,7 @@ describe('app.pubSubManager', () => { }); test('adapter', async () => { - expect(pubSubManager.connected).toBe(true); - expect(pubSubManager.adapter).toBeInstanceOf(MemoryPubSubAdapter); + expect(await pubSubManager.isConnected()).toBe(true); }); test('subscribe + publish', async () => { diff --git a/packages/core/server/src/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager.ts deleted file mode 100644 index ce4960bc42..0000000000 --- a/packages/core/server/src/pub-sub-manager.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { uid } from '@nocobase/utils'; -import crypto from 'crypto'; -import _ from 'lodash'; -import Application from './application'; - -export interface PubSubManagerOptions { - channelPrefix?: string; -} - -export interface PubSubManagerPublishOptions { - skipSelf?: boolean; - onlySelf?: boolean; -} - -export interface PubSubManagerSubscribeOptions { - debounce?: number; -} - -export const createPubSubManager = (app: Application, options: PubSubManagerOptions) => { - const pubSubManager = new PubSubManager(options); - app.on('afterStart', async () => { - await pubSubManager.connect(); - }); - app.on('afterStop', async () => { - await pubSubManager.close(); - }); - return pubSubManager; -}; - -export class PubSubManager { - adapter: IPubSubAdapter; - messageHandlers = new Map(); - subscribes = new Map(); - publisherId: string; - - constructor(protected options: PubSubManagerOptions = {}) { - this.publisherId = uid(); - } - - get channelPrefix() { - return this.options?.channelPrefix ? `${this.options.channelPrefix}.` : ''; - } - - get connected() { - return this.adapter?.connected || false; - } - - setAdapter(adapter: IPubSubAdapter) { - this.adapter = adapter; - } - - async connect() { - if (!this.adapter) { - return; - } - await this.adapter.connect(); - for (const [channel, callbacks] of this.subscribes) { - for (const [, fn] of callbacks) { - await this.adapter.subscribe(`${this.channelPrefix}${channel}`, fn); - } - } - } - - async close() { - if (!this.adapter) { - return; - } - return await this.adapter.close(); - } - - async getMessageHash(message) { - const encoder = new TextEncoder(); - const data = encoder.encode(JSON.stringify(message)); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); - return hashHex; - } - - async subscribe(channel: string, callback, options: PubSubManagerSubscribeOptions = {}) { - const { debounce = 0 } = options; - const wrappedCallback = async (wrappedMessage) => { - const json = JSON.parse(wrappedMessage); - if (!this.verifyMessage(json)) { - return; - } - await this.handleMessage({ channel, message: json.message, debounce, callback }); - }; - if (!this.subscribes.has(channel)) { - const map = new Map(); - this.subscribes.set(channel, map); - } - const map: Map = this.subscribes.get(channel); - const previous = map.get(callback); - if (previous) { - await this.adapter.unsubscribe(`${this.channelPrefix}${channel}`, previous); - } - map.set(callback, wrappedCallback); - if (this.connected) { - await this.adapter.subscribe(`${this.channelPrefix}${channel}`, wrappedCallback); - } - } - - async unsubscribe(channel, callback) { - const map: Map = this.subscribes.get(channel); - let fn = null; - if (map) { - fn = map.get(callback); - map.delete(callback); - } - if (!this.adapter || !fn) { - return; - } - return this.adapter.unsubscribe(`${this.channelPrefix}${channel}`, fn); - } - - async publish(channel, message, options?: PubSubManagerPublishOptions) { - if (!this.adapter) { - return; - } - - const wrappedMessage = JSON.stringify({ - publisherId: this.publisherId, - ...options, - message: message, - }); - - return this.adapter.publish(`${this.channelPrefix}${channel}`, wrappedMessage); - } - - async subscribeAll(callback, options: PubSubManagerSubscribeOptions = {}) { - if (!this.adapter) { - return; - } - const { debounce = 0 } = options; - return this.adapter.subscribeAll(async (channel: string, wrappedMessage) => { - if (!channel.startsWith(this.channelPrefix)) { - return; - } - const json = JSON.parse(wrappedMessage); - if (!this.verifyMessage(json)) { - return; - } - const realChannel = channel.substring(this.channelPrefix.length); - await this.handleMessage({ - callback, - debounce, - subscribeAll: true, - channel: realChannel, - message: json.message, - }); - }); - } - - protected async handleMessage({ channel, message, callback, debounce, subscribeAll = false }) { - const args = subscribeAll ? [channel, message] : [message]; - if (!debounce) { - await callback(...args); - return; - } - const prefix = subscribeAll ? '__subscribe_all__' : '__subscribe__'; - const messageHash = prefix + channel + (await this.getMessageHash(message)); - if (!this.messageHandlers.has(messageHash)) { - this.messageHandlers.set(messageHash, this.debounce(callback, debounce)); - } - const handleMessage = this.messageHandlers.get(messageHash); - try { - const args = subscribeAll ? [channel, message] : [message]; - await handleMessage(...args); - setTimeout(() => { - this.messageHandlers.delete(messageHash); - }, debounce); - } catch (error) { - this.messageHandlers.delete(messageHash); - throw error; - } - } - - protected verifyMessage({ onlySelf, skipSelf, publisherId }) { - if (onlySelf && publisherId !== this.publisherId) { - return; - } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { - return; - } - return true; - } - - protected debounce(func, wait: number) { - if (wait) { - return _.debounce(func, wait); - } - return func; - } -} - -export interface IPubSubAdapter { - connected?: boolean; - connect(): Promise; - close(): Promise; - subscribe(channel: string, callback): Promise; - unsubscribe(channel: string, callback): Promise; - publish(channel: string, message): Promise; - subscribeAll(callback): Promise; -} diff --git a/packages/core/server/src/pub-sub-manager/handler-manager.ts b/packages/core/server/src/pub-sub-manager/handler-manager.ts new file mode 100644 index 0000000000..efeb6d54b8 --- /dev/null +++ b/packages/core/server/src/pub-sub-manager/handler-manager.ts @@ -0,0 +1,118 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import _ from 'lodash'; +import { type PubSubManagerSubscribeOptions } from './types'; + +export class HandlerManager { + headlers: Map; + uniqueMessageHandlers: Map; + + constructor(protected publisherId: string) { + this.reset(); + } + + protected async getMessageHash(message) { + const encoder = new TextEncoder(); + const data = encoder.encode(JSON.stringify(message)); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + return hashHex; + } + + protected verifyMessage({ onlySelf, skipSelf, publisherId }) { + if (onlySelf && publisherId !== this.publisherId) { + return; + } else if (!onlySelf && skipSelf && publisherId === this.publisherId) { + return; + } + return true; + } + + protected debounce(func, wait: number) { + if (wait) { + return _.debounce(func, wait); + } + return func; + } + + async handleMessage({ channel, message, callback, debounce }) { + if (!debounce) { + await callback(message); + return; + } + const messageHash = channel + (await this.getMessageHash(message)); + if (!this.uniqueMessageHandlers.has(messageHash)) { + this.uniqueMessageHandlers.set(messageHash, this.debounce(callback, debounce)); + } + const handler = this.uniqueMessageHandlers.get(messageHash); + try { + await handler(message); + setTimeout(() => { + this.uniqueMessageHandlers.delete(messageHash); + }, debounce); + } catch (error) { + this.uniqueMessageHandlers.delete(messageHash); + throw error; + } + } + + wrapper(channel, callback, options) { + const { debounce = 0 } = options; + return async (wrappedMessage) => { + const json = JSON.parse(wrappedMessage); + if (!this.verifyMessage(json)) { + return; + } + await this.handleMessage({ channel, message: json.message, debounce, callback }); + }; + } + + set(channel: string, callback, options: PubSubManagerSubscribeOptions) { + if (!this.headlers.has(channel)) { + this.headlers.set(channel, new Map()); + } + const headlerMap = this.headlers.get(channel); + const headler = this.wrapper(channel, callback, options); + headlerMap.set(callback, headler); + return headler; + } + + get(channel: string, callback) { + const headlerMap = this.headlers.get(channel); + if (!headlerMap) { + return; + } + return headlerMap.get(callback); + } + + delete(channel: string, callback) { + const headlerMap = this.headlers.get(channel); + if (!headlerMap) { + return; + } + const headler = headlerMap.get(callback); + headlerMap.delete(callback); + return headler; + } + + reset() { + this.headlers = new Map(); + this.uniqueMessageHandlers = new Map(); + } + + async each(callback) { + for (const [channel, headlerMap] of this.headlers) { + for (const headler of headlerMap.values()) { + await callback(channel, headler); + } + } + } +} diff --git a/packages/core/server/src/pub-sub-manager/index.ts b/packages/core/server/src/pub-sub-manager/index.ts new file mode 100644 index 0000000000..d19a1603bb --- /dev/null +++ b/packages/core/server/src/pub-sub-manager/index.ts @@ -0,0 +1,13 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export * from './handler-manager'; +export * from './pub-sub-manager'; + +export * from './types'; diff --git a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts new file mode 100644 index 0000000000..5006aa4bf4 --- /dev/null +++ b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts @@ -0,0 +1,104 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { uid } from '@nocobase/utils'; +import Application from '../application'; +import { HandlerManager } from './handler-manager'; +import { + type IPubSubAdapter, + type PubSubManagerOptions, + type PubSubManagerPublishOptions, + type PubSubManagerSubscribeOptions, +} from './types'; + +export const createPubSubManager = (app: Application, options: PubSubManagerOptions) => { + const pubSubManager = new PubSubManager(options); + app.on('afterStart', async () => { + await pubSubManager.connect(); + }); + app.on('afterStop', async () => { + await pubSubManager.close(); + }); + return pubSubManager; +}; + +export class PubSubManager { + protected publisherId: string; + protected adapter: IPubSubAdapter; + protected handlerManager: HandlerManager; + + constructor(protected options: PubSubManagerOptions = {}) { + this.publisherId = uid(); + this.handlerManager = new HandlerManager(this.publisherId); + } + + get channelPrefix() { + return this.options?.channelPrefix ? `${this.options.channelPrefix}.` : ''; + } + + setAdapter(adapter: IPubSubAdapter) { + this.adapter = adapter; + } + + async isConnected() { + return this.adapter?.isConnected(); + } + + async connect() { + if (!this.adapter) { + return; + } + await this.adapter.connect(); + // 如果没连接前添加的订阅,连接后需要把订阅添加上 + await this.handlerManager.each(async (channel, headler) => { + await this.adapter.subscribe(`${this.channelPrefix}${channel}`, headler); + }); + } + + async close() { + if (!this.adapter) { + return; + } + return await this.adapter.close(); + } + + async subscribe(channel: string, callback, options: PubSubManagerSubscribeOptions = {}) { + // 先退订,防止重复订阅 + await this.unsubscribe(channel, callback); + const handler = this.handlerManager.set(channel, callback, options); + // 连接之后才能订阅 + if (await this.adapter.isConnected()) { + await this.adapter.subscribe(`${this.channelPrefix}${channel}`, handler); + } + } + + async unsubscribe(channel, callback) { + const handler = this.handlerManager.delete(channel, callback); + + if (!this.adapter || !handler) { + return; + } + + return this.adapter.unsubscribe(`${this.channelPrefix}${channel}`, handler); + } + + async publish(channel, message, options?: PubSubManagerPublishOptions) { + if (!this.adapter) { + return; + } + + const wrappedMessage = JSON.stringify({ + publisherId: this.publisherId, + ...options, + message: message, + }); + + return this.adapter.publish(`${this.channelPrefix}${channel}`, wrappedMessage); + } +} diff --git a/packages/core/server/src/pub-sub-manager/types.ts b/packages/core/server/src/pub-sub-manager/types.ts new file mode 100644 index 0000000000..0fca8cfb66 --- /dev/null +++ b/packages/core/server/src/pub-sub-manager/types.ts @@ -0,0 +1,32 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export interface PubSubManagerOptions { + channelPrefix?: string; +} + +export interface PubSubManagerPublishOptions { + skipSelf?: boolean; + onlySelf?: boolean; +} + +export interface PubSubManagerSubscribeOptions { + debounce?: number; +} + +export type PubSubCallback = (message: any) => Promise; + +export interface IPubSubAdapter { + isConnected(): Promise; + connect(): Promise; + close(): Promise; + subscribe(channel: string, callback: PubSubCallback): Promise; + unsubscribe(channel: string, callback: PubSubCallback): Promise; + publish(channel: string, message: any): Promise; +} diff --git a/packages/core/test/src/server/memory-pub-sub-adapter.ts b/packages/core/test/src/server/memory-pub-sub-adapter.ts index f9cf0b4993..0320dc5630 100644 --- a/packages/core/test/src/server/memory-pub-sub-adapter.ts +++ b/packages/core/test/src/server/memory-pub-sub-adapter.ts @@ -47,6 +47,10 @@ export class MemoryPubSubAdapter implements IPubSubAdapter { this.connected = false; } + async isConnected() { + return this.connected; + } + async subscribe(channel, callback) { this.emitter.on(channel, callback); } From 20d4385c18508dc3b2ecbb0faf777fe6e32de46a Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 31 Jul 2024 14:40:05 +0800 Subject: [PATCH 21/70] fix: test error --- .../src/pub-sub-manager/handler-manager.ts | 16 ++++++++-------- .../src/pub-sub-manager/pub-sub-manager.ts | 11 ++++++----- packages/core/server/src/sync-message-manager.ts | 8 ++++++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/core/server/src/pub-sub-manager/handler-manager.ts b/packages/core/server/src/pub-sub-manager/handler-manager.ts index efeb6d54b8..63992eec43 100644 --- a/packages/core/server/src/pub-sub-manager/handler-manager.ts +++ b/packages/core/server/src/pub-sub-manager/handler-manager.ts @@ -11,7 +11,7 @@ import _ from 'lodash'; import { type PubSubManagerSubscribeOptions } from './types'; export class HandlerManager { - headlers: Map; + handlers: Map; uniqueMessageHandlers: Map; constructor(protected publisherId: string) { @@ -76,17 +76,17 @@ export class HandlerManager { } set(channel: string, callback, options: PubSubManagerSubscribeOptions) { - if (!this.headlers.has(channel)) { - this.headlers.set(channel, new Map()); + if (!this.handlers.has(channel)) { + this.handlers.set(channel, new Map()); } - const headlerMap = this.headlers.get(channel); + const headlerMap = this.handlers.get(channel); const headler = this.wrapper(channel, callback, options); headlerMap.set(callback, headler); return headler; } get(channel: string, callback) { - const headlerMap = this.headlers.get(channel); + const headlerMap = this.handlers.get(channel); if (!headlerMap) { return; } @@ -94,7 +94,7 @@ export class HandlerManager { } delete(channel: string, callback) { - const headlerMap = this.headlers.get(channel); + const headlerMap = this.handlers.get(channel); if (!headlerMap) { return; } @@ -104,12 +104,12 @@ export class HandlerManager { } reset() { - this.headlers = new Map(); + this.handlers = new Map(); this.uniqueMessageHandlers = new Map(); } async each(callback) { - for (const [channel, headlerMap] of this.headlers) { + for (const [channel, headlerMap] of this.handlers) { for (const headler of headlerMap.values()) { await callback(channel, headler); } diff --git a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts index 5006aa4bf4..3f3cced021 100644 --- a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts @@ -11,6 +11,7 @@ import { uid } from '@nocobase/utils'; import Application from '../application'; import { HandlerManager } from './handler-manager'; import { + PubSubCallback, type IPubSubAdapter, type PubSubManagerOptions, type PubSubManagerPublishOptions, @@ -47,7 +48,7 @@ export class PubSubManager { } async isConnected() { - return this.adapter?.isConnected(); + return !!this.adapter?.isConnected(); } async connect() { @@ -68,17 +69,17 @@ export class PubSubManager { return await this.adapter.close(); } - async subscribe(channel: string, callback, options: PubSubManagerSubscribeOptions = {}) { + async subscribe(channel: string, callback: PubSubCallback, options: PubSubManagerSubscribeOptions = {}) { // 先退订,防止重复订阅 await this.unsubscribe(channel, callback); const handler = this.handlerManager.set(channel, callback, options); // 连接之后才能订阅 - if (await this.adapter.isConnected()) { + if (await this.isConnected()) { await this.adapter.subscribe(`${this.channelPrefix}${channel}`, handler); } } - async unsubscribe(channel, callback) { + async unsubscribe(channel: string, callback: PubSubCallback) { const handler = this.handlerManager.delete(channel, callback); if (!this.adapter || !handler) { @@ -88,7 +89,7 @@ export class PubSubManager { return this.adapter.unsubscribe(`${this.channelPrefix}${channel}`, handler); } - async publish(channel, message, options?: PubSubManagerPublishOptions) { + async publish(channel: string, message: any, options?: PubSubManagerPublishOptions) { if (!this.adapter) { return; } diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts index e92840513d..298e7822a2 100644 --- a/packages/core/server/src/sync-message-manager.ts +++ b/packages/core/server/src/sync-message-manager.ts @@ -9,7 +9,7 @@ import { Transactionable } from '@nocobase/database'; import Application from './application'; -import { PubSubManager, PubSubManagerPublishOptions } from './pub-sub-manager'; +import { PubSubCallback, PubSubManager, PubSubManagerPublishOptions } from './pub-sub-manager'; export class SyncMessageManager { protected versionManager: SyncMessageVersionManager; @@ -61,12 +61,16 @@ export class SyncMessageManager { } } - async subscribe(channel: string, callback) { + async subscribe(channel: string, callback: PubSubCallback) { return await this.app.pubSubManager.subscribe(`${this.app.name}.sync.${channel}`, callback, { debounce: this.debounce, }); } + async unsubscribe(channel: string, callback: PubSubCallback) { + return this.app.pubSubManager.unsubscribe(`${this.app.name}.sync.${channel}`, callback); + } + async sync() { // TODO } From eb56a7be0e39efda16840092780d3bef8157a581 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 31 Jul 2024 15:13:00 +0800 Subject: [PATCH 22/70] fix: test error --- packages/core/server/src/pub-sub-manager/handler-manager.ts | 3 +++ packages/core/server/src/pub-sub-manager/pub-sub-manager.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/server/src/pub-sub-manager/handler-manager.ts b/packages/core/server/src/pub-sub-manager/handler-manager.ts index 63992eec43..b2b972df83 100644 --- a/packages/core/server/src/pub-sub-manager/handler-manager.ts +++ b/packages/core/server/src/pub-sub-manager/handler-manager.ts @@ -94,6 +94,9 @@ export class HandlerManager { } delete(channel: string, callback) { + if (!callback) { + return; + } const headlerMap = this.handlers.get(channel); if (!headlerMap) { return; diff --git a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts index 3f3cced021..7c14a64361 100644 --- a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts @@ -48,7 +48,10 @@ export class PubSubManager { } async isConnected() { - return !!this.adapter?.isConnected(); + if (this.adapter) { + return this.adapter.isConnected(); + } + return false; } async connect() { @@ -74,6 +77,7 @@ export class PubSubManager { await this.unsubscribe(channel, callback); const handler = this.handlerManager.set(channel, callback, options); // 连接之后才能订阅 + if (await this.isConnected()) { await this.adapter.subscribe(`${this.channelPrefix}${channel}`, handler); } From b61895d30925ef76dcb06ff638ec111cffdc065e Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 31 Jul 2024 14:39:50 +0000 Subject: [PATCH 23/70] refactor(server): adapt to new api and fix test --- packages/core/server/src/application.ts | 3 - packages/core/server/src/plugin.ts | 14 ---- packages/core/test/src/server/mock-server.ts | 48 ++++++------- .../src/server/__tests__/cluster.test.ts | 53 ++++++++++++++ .../plugin-file-manager/src/server/server.ts | 12 ++-- .../plugin-workflow-test/src/server/index.ts | 59 +++++++++------ .../plugin-workflow/src/server/Plugin.ts | 22 +++--- .../src/server/__tests__/Plugin.test.ts | 2 +- .../src/server/__tests__/cluster.test.ts | 72 +++++++++++++++++++ 9 files changed, 205 insertions(+), 80 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts create mode 100644 packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index da22ab1659..36f83924e9 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -61,7 +61,6 @@ import validateFilterParams from './middlewares/validate-filter-params'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; -import { SyncManager } from './sync-manager'; import { SyncMessageManager } from './sync-message-manager'; import packageJson from '../package.json'; @@ -230,7 +229,6 @@ export class Application exten /** * @internal */ - public syncManager: SyncManager; public pubSubManager: PubSubManager; public syncMessageManager: SyncMessageManager; public requestLogger: Logger; @@ -1131,7 +1129,6 @@ export class Application exten this._cli = this.createCLI(); this._i18n = createI18n(options); - this.syncManager = new SyncManager(this); this.pubSubManager = createPubSubManager(this, { channelPrefix: this.name, ...options.pubSubManager, diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index 144a4de07b..15fd544b94 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -134,20 +134,6 @@ export abstract class Plugin implements PluginInterface { async afterRemove() {} - /** - * Fired when a sync message is received. - * @experimental - */ - onSync(message: SyncMessageData = {}): Promise | void {} - - /** - * Publish a sync message. - * @experimental - */ - sync(message?: SyncMessageData) { - this.app.syncManager.publish(this.name, message); - } - async handleSyncMessage(message: any) {} async sendSyncMessage(message: any, options?: Transactionable) { if (!this.name) { diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index fd563f50a0..1a3b0fd736 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -234,7 +234,7 @@ export function mockServer(options: ApplicationOptions = {}) { const app = new MockServer({ acl: false, syncMessageManager: { - debounce: 1000, + debounce: 500, }, ...options, }); @@ -243,7 +243,7 @@ export function mockServer(options: ApplicationOptions = {}) { app.pubSubManager.setAdapter( MemoryPubSubAdapter.create(basename, { - debounce: 1000, + debounce: 500, }), ); @@ -258,25 +258,32 @@ export async function startMockServer(options: ApplicationOptions = {}) { type BeforeInstallFn = (app) => Promise; -export async function createMockCluster( - options: ApplicationOptions & { - number?: number; - version?: string; - name?: string; - appName?: string; - beforeInstall?: BeforeInstallFn; - skipInstall?: boolean; - skipStart?: boolean; - } = {}, -) { +export type MockServerOptions = ApplicationOptions & { + version?: string; + beforeInstall?: BeforeInstallFn; + skipInstall?: boolean; + skipStart?: boolean; +}; + +export type MockClusterOptions = MockServerOptions & { + number?: number; + clusterName?: string; + appName?: string; +}; + +export async function createMockCluster({ + number = 2, + clusterName = `cluster_${uid()}`, + appName = `app_${uid()}`, + ...options +}: MockClusterOptions = {}) { const nodes: MockServer[] = []; - const clusterName = options.name || `cluster_${uid()}`; - const appName = options.appName || `app_${uid()}`; - for (const i of _.range(0, options.number || 2)) { + for (const i of _.range(0, number || 2)) { const app: MockServer = await createMockServer({ ...options, skipSupervisor: true, name: clusterName + '_' + appName, + skipInstall: Boolean(i), pubSubManager: { channelPrefix: clusterName, }, @@ -293,14 +300,7 @@ export async function createMockCluster( }; } -export async function createMockServer( - options: ApplicationOptions & { - version?: string; - beforeInstall?: BeforeInstallFn; - skipInstall?: boolean; - skipStart?: boolean; - } = {}, -) { +export async function createMockServer(options: MockServerOptions = {}) { const { version, beforeInstall, skipInstall, skipStart, ...others } = options; const app: any = mockServer(others); if (!skipInstall) { diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts new file mode 100644 index 0000000000..67927ac3ff --- /dev/null +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts @@ -0,0 +1,53 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createMockCluster, sleep } from '@nocobase/test'; + +import Plugin from '..'; + +describe('file-manager > cluster', () => { + let cluster; + + beforeEach(async () => { + cluster = await createMockCluster({ + plugins: [[Plugin, { name: 'file-manager' }]], + }); + }); + + afterEach(() => cluster.destroy()); + + describe('sync message', () => { + it('storage cache should be sync between every nodes', async () => { + const [app1, app2] = cluster.nodes; + + const StorageRepo = app1.db.getRepository('storages'); + + const s1 = await StorageRepo.findOne({ + filter: { + default: true, + }, + }); + + const p1 = app1.pm.get(Plugin) as Plugin; + const p2 = app2.pm.get(Plugin) as Plugin; + + expect(p1.storagesCache.get(s1.id)).toEqual(s1.toJSON()); + expect(p2.storagesCache.get(s1.id)).toEqual(s1.toJSON()); + + await s1.update({ + path: 'a', + }); + expect(p1.storagesCache.get(s1.id).path).toEqual('a'); + + await sleep(550); + + expect(p2.storagesCache.get(s1.id).path).toEqual('a'); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts index b18acbfbe1..66f9c082e0 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts @@ -152,7 +152,7 @@ export default class PluginFileManagerServer extends Plugin { } } - async onSync(message) { + async handleSyncMessage(message) { if (message.type === 'storageChange') { const storage = await this.db.getRepository('storages').findOne({ filterByTk: message.storageId, @@ -162,7 +162,7 @@ export default class PluginFileManagerServer extends Plugin { } } if (message.type === 'storageRemove') { - const id = Number.parseInt(message.storageId, 10); + const id = message.storageId; this.storagesCache.delete(id); } } @@ -192,16 +192,16 @@ export default class PluginFileManagerServer extends Plugin { const Storage = this.db.getModel('storages'); Storage.afterSave((m) => { this.storagesCache.set(m.id, m.toJSON()); - this.sync({ + this.sendSyncMessage({ type: 'storageChange', - storageId: `${m.id}`, + storageId: m.id, }); }); Storage.afterDestroy((m) => { this.storagesCache.delete(m.id); - this.sync({ + this.sendSyncMessage({ type: 'storageRemove', - storageId: `${m.id}`, + storageId: m.id, }); }); diff --git a/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts b/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts index 71fcd04fba..070b41c79d 100644 --- a/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts @@ -10,42 +10,38 @@ import path from 'path'; import { ApplicationOptions, Plugin } from '@nocobase/server'; -import { MockServer, createMockServer, mockDatabase } from '@nocobase/test'; +import { MockClusterOptions, MockServer, createMockCluster, createMockServer, mockDatabase } from '@nocobase/test'; import functions from './functions'; import triggers from './triggers'; import instructions from './instructions'; import { SequelizeDataSource } from '@nocobase/data-source-manager'; import { uid } from '@nocobase/utils'; +export { sleep } from '@nocobase/test'; -export interface MockServerOptions extends ApplicationOptions { +interface WorkflowMockServerOptions extends ApplicationOptions { collectionsPath?: string; } -// async function createMockServer(options: MockServerOptions) { -// const app = mockServer(options); -// await app.cleanDb(); -// await app.runCommand('start', '--quickstart'); -// return app; -// } - -export function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); +interface WorkflowMockClusterOptions extends MockClusterOptions { + collectionsPath?: string; } -export async function getApp(options: MockServerOptions = {}): Promise { - const { plugins = [], collectionsPath, ...others } = options; - class TestCollectionPlugin extends Plugin { - async load() { - if (collectionsPath) { - await this.db.import({ directory: collectionsPath }); - } +class TestCollectionPlugin extends Plugin { + async load() { + if (this.options.collectionsPath) { + await this.db.import({ directory: this.options.collectionsPath }); } } +} + +export async function getApp({ + plugins = [], + collectionsPath, + ...options +}: WorkflowMockServerOptions = {}): Promise { const app = await createMockServer({ - ...others, + ...options, plugins: [ [ 'workflow', @@ -56,7 +52,7 @@ export async function getApp(options: MockServerOptions = {}): Promise { const logger = this.getLogger(workflow.id); + if (!workflow.enabled) { + logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`); + return; + } if (!this.ready) { logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`); logger.debug(`ignored event data:`, context); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts index c1140bdea9..616dfea33b 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/Plugin.test.ts @@ -510,7 +510,7 @@ describe('workflow > Plugin', () => { }); }); - describe('sync', () => { + describe('sync trigger', () => { it('sync on trigger class', async () => { const w1 = await WorkflowModel.create({ enabled: true, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts new file mode 100644 index 0000000000..675a8df9ef --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts @@ -0,0 +1,72 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { sleep } from '@nocobase/test'; +import { getCluster } from '@nocobase/plugin-workflow-test'; + +import Plugin, { Processor } from '..'; +import { EXECUTION_STATUS } from '../constants'; + +describe('workflow > cluster', () => { + let cluster; + + beforeEach(async () => { + cluster = await getCluster({ + number: 3, + }); + }); + + afterEach(() => cluster.destroy()); + + describe('sync message', () => { + it('enabled status of workflow should be sync in every nodes', async () => { + const [app1, app2, app3] = cluster.nodes; + + const WorkflowRepo = app1.db.getRepository('workflows'); + + const w1 = await WorkflowRepo.create({ + values: { + type: 'syncTrigger', + enabled: true, + }, + }); + + const p1 = app1.pm.get(Plugin) as Plugin; + + const pro1 = (await p1.trigger(w1, {})) as Processor; + expect(pro1.execution.status).toBe(EXECUTION_STATUS.RESOLVED); + + await sleep(500); + + const p2 = app2.pm.get(Plugin) as Plugin; + const w2 = p2.enabledCache.get(w1.id); + expect(w2).toBeDefined(); + const pro2 = (await p2.trigger(w2, {})) as Processor; + expect(pro2.execution.status).toBe(EXECUTION_STATUS.RESOLVED); + + const p3 = app3.pm.get(Plugin) as Plugin; + const w3 = p3.enabledCache.get(w1.id); + expect(w3).toBeDefined(); + const pro3 = (await p3.trigger(w3, {})) as Processor; + expect(pro3.execution.status).toBe(EXECUTION_STATUS.RESOLVED); + + const executions = await w1.getExecutions(); + expect(executions.length).toBe(3); + + await w1.update({ + enabled: false, + }); + + await sleep(550); + + expect(p2.enabledCache.get(w1.id)).toBeUndefined(); + expect(p3.enabledCache.get(w1.id)).toBeUndefined(); + }); + }); +}); From 288823dab1946eabee306f9c3ed801d909947209 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 31 Jul 2024 15:15:07 +0000 Subject: [PATCH 24/70] fix(plugin-data-source-main): fix changed api --- .../@nocobase/plugin-data-source-main/src/server/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index bcd7320ab2..ab61bee1e5 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -41,7 +41,7 @@ export class PluginDataSourceMainServer extends Plugin { this.loadFilter = filter; } - async onSync(message) { + async handleSyncMessage(message) { const { type, collectionName } = message; if (type === 'newCollection') { const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({ @@ -92,7 +92,7 @@ export class PluginDataSourceMainServer extends Plugin { transaction, }); - this.app.syncManager.publish(this.name, { + this.sendSyncMessage({ type: 'newCollection', collectionName: model.get('name'), }); From b927115230ea022f83f9f061efebe5c03ae8a592 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 1 Aug 2024 08:07:38 +0800 Subject: [PATCH 25/70] fix: test error --- packages/core/test/src/server/mock-server.ts | 14 +++++----- .../src/server/__tests__/cluster.test.ts | 4 +-- .../plugin-file-manager/src/server/server.ts | 26 ++++++++++++------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 1a3b0fd736..c99b89b85c 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -239,13 +239,15 @@ export function mockServer(options: ApplicationOptions = {}) { ...options, }); - const basename = app.options.pubSubManager?.channelPrefix || app.name; + const basename = app.options.pubSubManager?.channelPrefix; - app.pubSubManager.setAdapter( - MemoryPubSubAdapter.create(basename, { - debounce: 500, - }), - ); + if (basename) { + app.pubSubManager.setAdapter( + MemoryPubSubAdapter.create(basename, { + debounce: 500, + }), + ); + } return app; } diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts index 67927ac3ff..f273a34097 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/cluster.test.ts @@ -16,7 +16,7 @@ describe('file-manager > cluster', () => { beforeEach(async () => { cluster = await createMockCluster({ - plugins: [[Plugin, { name: 'file-manager' }]], + plugins: ['file-manager'], }); }); @@ -44,9 +44,7 @@ describe('file-manager > cluster', () => { path: 'a', }); expect(p1.storagesCache.get(s1.id).path).toEqual('a'); - await sleep(550); - expect(p2.storagesCache.get(s1.id).path).toEqual('a'); }); }); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts index 66f9c082e0..e356211ae7 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts @@ -190,19 +190,25 @@ export default class PluginFileManagerServer extends Plugin { }); const Storage = this.db.getModel('storages'); - Storage.afterSave((m) => { + Storage.afterSave((m, { transaction }) => { this.storagesCache.set(m.id, m.toJSON()); - this.sendSyncMessage({ - type: 'storageChange', - storageId: m.id, - }); + this.sendSyncMessage( + { + type: 'storageChange', + storageId: m.id, + }, + { transaction }, + ); }); - Storage.afterDestroy((m) => { + Storage.afterDestroy((m, { transaction }) => { this.storagesCache.delete(m.id); - this.sendSyncMessage({ - type: 'storageRemove', - storageId: m.id, - }); + this.sendSyncMessage( + { + type: 'storageRemove', + storageId: m.id, + }, + { transaction }, + ); }); this.app.acl.registerSnippet({ From a16df8aa2fdf5cc9e7c3d3c20cb1cd26ee2ba402 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 1 Aug 2024 08:30:09 +0800 Subject: [PATCH 26/70] fix: remove sync-manager test case --- .../server/src/__tests__/sync-manager.test.ts | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 packages/core/server/src/__tests__/sync-manager.test.ts diff --git a/packages/core/server/src/__tests__/sync-manager.test.ts b/packages/core/server/src/__tests__/sync-manager.test.ts deleted file mode 100644 index 044a1bcdae..0000000000 --- a/packages/core/server/src/__tests__/sync-manager.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { Application } from '../application'; -import { SyncAdapter } from '../sync-manager'; - -class MockAdapter extends SyncAdapter { - private _ready: boolean; - - constructor({ ready = false, ...options } = {}) { - super(options); - this._ready = ready; - } - - get ready() { - return this._ready; - } - - publish(data: Record): void { - return; - } -} - -describe('sync manager', () => { - let app: Application; - - beforeEach(() => { - app = new Application({ - database: { - dialect: 'sqlite', - storage: ':memory:', - }, - resourcer: { - prefix: '/api', - }, - acl: false, - dataWrapping: false, - registerActions: false, - }); - }); - - afterEach(async () => { - return app.destroy(); - }); - - it('sync manager should be initialized with application', async () => { - expect(app.syncManager).toBeDefined(); - }); - - it('init adapter', async () => { - const mockAdapter1 = new MockAdapter(); - app.syncManager.init(mockAdapter1); - expect(() => app.syncManager.init(mockAdapter1)).toThrowError(); - }); -}); From 773172c030848de603aa3e37ae7eb59039a0bfa3 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 00:28:11 +0000 Subject: [PATCH 27/70] chore(server): remove legacy code --- packages/core/server/src/sync-manager.ts | 127 ----------------------- 1 file changed, 127 deletions(-) delete mode 100644 packages/core/server/src/sync-manager.ts diff --git a/packages/core/server/src/sync-manager.ts b/packages/core/server/src/sync-manager.ts deleted file mode 100644 index e8fc42df5b..0000000000 --- a/packages/core/server/src/sync-manager.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { isEqual, uniqWith } from 'lodash'; -import { randomUUID } from 'node:crypto'; -import EventEmitter from 'node:events'; -import Application from './application'; - -export abstract class SyncAdapter extends EventEmitter { - abstract get ready(): boolean; - public abstract publish(data: SyncMessage): void | Promise; -} - -export type SyncMessageData = Record; - -export type SyncEventCallback = (message: SyncMessageData) => void; - -export type SyncMessage = { - namespace: string; - nodeId: string; - appName: string; -} & SyncMessageData; - -/** - * @experimental - */ -export class SyncManager { - private nodeId: string; - private eventEmitter = new EventEmitter(); - private adapter: SyncAdapter = null; - private incomingBuffer: SyncMessageData[] = []; - private outgoingBuffer: [string, SyncMessageData][] = []; - private flushTimer: NodeJS.Timeout = null; - - public get available() { - return this.adapter ? this.adapter.ready : false; - } - - constructor(private app: Application) { - this.nodeId = `${process.env.NODE_ID || randomUUID()}-${process.pid}`; - } - - private onMessage(namespace, message) { - this.app.logger.info(`emit sync event in namespace ${namespace}`); - this.eventEmitter.emit(namespace, message); - const pluginInstance = this.app.pm.get(namespace); - pluginInstance.onSync(message); - } - - private onSync = (messages: SyncMessage[]) => { - this.app.logger.info('sync messages received, save into buffer:', messages); - - if (this.flushTimer) { - clearTimeout(this.flushTimer); - this.flushTimer = null; - } - - this.incomingBuffer = uniqWith( - this.incomingBuffer.concat( - messages - .filter((item) => item.nodeId !== this.nodeId && item.appName === this.app.name) - .map(({ nodeId, appName, ...message }) => message), - ), - isEqual, - ); - - this.flushTimer = setTimeout(() => { - this.incomingBuffer.forEach(({ namespace, ...message }) => { - this.onMessage(namespace, message); - }); - this.incomingBuffer = []; - }, 1000); - }; - - private onReady = () => { - while (this.outgoingBuffer.length) { - const [namespace, data] = this.outgoingBuffer.shift(); - this.publish(namespace, data); - } - }; - - public init(adapter: SyncAdapter) { - if (this.adapter) { - throw new Error('sync adapter is already exists'); - } - - if (!adapter) { - return; - } - - this.adapter = adapter; - this.adapter.on('message', this.onSync); - this.adapter.on('ready', this.onReady); - } - - public subscribe(namespace: string, callback: SyncEventCallback) { - this.eventEmitter.on(namespace, callback); - } - - public unsubscribe(namespace: string, callback: SyncEventCallback) { - this.eventEmitter.off(namespace, callback); - } - - /** - * Publish a message to the sync manager - */ - public publish(namespace: string, data: SyncMessageData = {}) { - if (!this.adapter) { - return; - } - if (!this.adapter.ready) { - this.outgoingBuffer.push([namespace, data]); - this.app.logger.warn(`sync adapter is not ready for now, message will be send when it is ready`); - return; - } - this.app.logger.info(`publishing sync message from #${this.nodeId} (${this.app.name}) in namespace ${namespace}:`, { - data, - }); - return this.adapter.publish({ ...data, nodeId: this.nodeId, appName: this.app.name, namespace }); - } -} From df2841acb37ebe79e71e0c5e1ed5bc0c66c253f9 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 00:50:29 +0000 Subject: [PATCH 28/70] fix(plugin-workflow): fix send sync message with transaction --- .../plugin-workflow/src/server/Plugin.ts | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts index d805742ba4..0753cd062e 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts @@ -12,7 +12,7 @@ import { randomUUID } from 'crypto'; import LRUCache from 'lru-cache'; -import { Op, Transactionable } from '@nocobase/database'; +import { Op, Transaction, Transactionable } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; import { Registry } from '@nocobase/utils'; @@ -64,7 +64,7 @@ export default class PluginWorkflowServer extends Plugin { private meter = null; private checker: NodeJS.Timeout = null; - private onBeforeSave = async (instance: WorkflowModel, options) => { + private onBeforeSave = async (instance: WorkflowModel, { transaction }) => { const Model = instance.constructor; if (instance.enabled) { @@ -74,7 +74,7 @@ export default class PluginWorkflowServer extends Plugin { where: { key: instance.key, }, - transaction: options.transaction, + transaction, }); if (!count) { instance.set('current', true); @@ -93,7 +93,7 @@ export default class PluginWorkflowServer extends Plugin { [Op.ne]: instance.id, }, }, - transaction: options.transaction, + transaction, }); if (previous) { @@ -101,12 +101,12 @@ export default class PluginWorkflowServer extends Plugin { await previous.update( { enabled: false, current: null }, { - transaction: options.transaction, + transaction, hooks: false, }, ); - this.toggle(previous, false); + this.toggle(previous, false, transaction); } }; @@ -122,12 +122,12 @@ export default class PluginWorkflowServer extends Plugin { }); } if (workflow) { - this.toggle(workflow, true, true); + this.toggle(workflow, true); } } else { const workflow = this.enabledCache.get(message.workflowId); if (workflow) { - this.toggle(workflow, false, true); + this.toggle(workflow, false); } } } @@ -268,13 +268,15 @@ export default class PluginWorkflowServer extends Plugin { }); db.on('workflows.beforeSave', this.onBeforeSave); - db.on('workflows.afterCreate', (model: WorkflowModel) => { + db.on('workflows.afterCreate', (model: WorkflowModel, { transaction }) => { if (model.enabled) { - this.toggle(model); + this.toggle(model, true, transaction); } }); - db.on('workflows.afterUpdate', (model: WorkflowModel) => this.toggle(model)); - db.on('workflows.beforeDestroy', (model: WorkflowModel) => this.toggle(model, false)); + db.on('workflows.afterUpdate', (model: WorkflowModel, { transaction }) => + this.toggle(model, model.enabled, transaction), + ); + db.on('workflows.afterDestroy', (model: WorkflowModel, { transaction }) => this.toggle(model, false, transaction)); // [Life Cycle]: // * load all workflows in db @@ -320,7 +322,7 @@ export default class PluginWorkflowServer extends Plugin { }); } - private toggle(workflow: WorkflowModel, enable?: boolean, silent = false) { + private toggle(workflow: WorkflowModel, enable?: boolean, transaction?: Transaction) { const type = workflow.get('type'); const trigger = this.triggers.get(type); if (!trigger) { @@ -340,12 +342,15 @@ export default class PluginWorkflowServer extends Plugin { trigger.off(workflow); this.enabledCache.delete(workflow.id); } - if (!silent) { - this.sendSyncMessage({ - type: 'statusChange', - workflowId: workflow.id, - enabled: next, - }); + if (transaction) { + this.sendSyncMessage( + { + type: 'statusChange', + workflowId: workflow.id, + enabled: next, + }, + { transaction }, + ); } } From 69b95c5f7ea1c8b1c62b644895bf43a083159846 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 01:12:10 +0000 Subject: [PATCH 29/70] chore(server): remove legacy code --- packages/core/server/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/server/src/index.ts b/packages/core/server/src/index.ts index 8e8e31395a..bf674e8f23 100644 --- a/packages/core/server/src/index.ts +++ b/packages/core/server/src/index.ts @@ -16,5 +16,4 @@ export * from './migration'; export * from './plugin'; export * from './plugin-manager'; export * from './pub-sub-manager'; -export * from './sync-manager'; export const OFFICIAL_PLUGIN_PREFIX = '@nocobase/plugin-'; From fd8c9932d44cd89b07a9c0c0aa45e3c8ba059326 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 08:02:35 +0000 Subject: [PATCH 30/70] chore(server): remove legacy code --- packages/core/server/src/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index 15fd544b94..cc57119fc5 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -18,7 +18,6 @@ import { resolve } from 'path'; import { Application } from './application'; import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager'; import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils'; -import { SyncMessageData } from './sync-manager'; export interface PluginInterface { beforeLoad?: () => void; From 49211ab0138fbd9dd188eb0255e5ec898c214567 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 10:17:19 +0000 Subject: [PATCH 31/70] fix(plugin-workflow): fix test case --- .../plugin-workflow/src/server/__tests__/cluster.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts index 675a8df9ef..17f9c0b5bd 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts @@ -59,8 +59,11 @@ describe('workflow > cluster', () => { const executions = await w1.getExecutions(); expect(executions.length).toBe(3); - await w1.update({ - enabled: false, + await WorkflowRepo.update({ + filterByTk: w1.id, + values: { + enabled: false, + }, }); await sleep(550); From 98d58963ea339facc7af1b08ed462f8f478e07ef Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 10:22:39 +0000 Subject: [PATCH 32/70] fix(plugin-workflow): fix test case --- .../plugin-workflow/src/server/Plugin.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts index 0753cd062e..fa4092a869 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts @@ -106,7 +106,7 @@ export default class PluginWorkflowServer extends Plugin { }, ); - this.toggle(previous, false, transaction); + this.toggle(previous, false, { transaction }); } }; @@ -122,12 +122,12 @@ export default class PluginWorkflowServer extends Plugin { }); } if (workflow) { - this.toggle(workflow, true); + this.toggle(workflow, true, { silent: true }); } } else { const workflow = this.enabledCache.get(message.workflowId); if (workflow) { - this.toggle(workflow, false); + this.toggle(workflow, false, { silent: true }); } } } @@ -270,13 +270,15 @@ export default class PluginWorkflowServer extends Plugin { db.on('workflows.beforeSave', this.onBeforeSave); db.on('workflows.afterCreate', (model: WorkflowModel, { transaction }) => { if (model.enabled) { - this.toggle(model, true, transaction); + this.toggle(model, true, { transaction }); } }); db.on('workflows.afterUpdate', (model: WorkflowModel, { transaction }) => - this.toggle(model, model.enabled, transaction), + this.toggle(model, model.enabled, { transaction }), + ); + db.on('workflows.afterDestroy', (model: WorkflowModel, { transaction }) => + this.toggle(model, false, { transaction }), ); - db.on('workflows.afterDestroy', (model: WorkflowModel, { transaction }) => this.toggle(model, false, transaction)); // [Life Cycle]: // * load all workflows in db @@ -292,7 +294,7 @@ export default class PluginWorkflowServer extends Plugin { }); workflows.forEach((workflow: WorkflowModel) => { - this.toggle(workflow); + this.toggle(workflow, true, { silent: true }); }); this.checker = setInterval(() => { @@ -305,7 +307,7 @@ export default class PluginWorkflowServer extends Plugin { this.app.on('beforeStop', async () => { for (const workflow of this.enabledCache.values()) { - this.toggle(workflow, false); + this.toggle(workflow, false, { silent: true }); } this.ready = false; @@ -322,7 +324,11 @@ export default class PluginWorkflowServer extends Plugin { }); } - private toggle(workflow: WorkflowModel, enable?: boolean, transaction?: Transaction) { + private toggle( + workflow: WorkflowModel, + enable?: boolean, + { silent, transaction }: { silent?: boolean } & Transactionable = {}, + ) { const type = workflow.get('type'); const trigger = this.triggers.get(type); if (!trigger) { @@ -342,7 +348,7 @@ export default class PluginWorkflowServer extends Plugin { trigger.off(workflow); this.enabledCache.delete(workflow.id); } - if (transaction) { + if (!silent) { this.sendSyncMessage( { type: 'statusChange', From 1702ac67e340955a8a7325ea85b098313f6ebc55 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 11:09:04 +0000 Subject: [PATCH 33/70] test(server): test skip-install parameter in cluster --- packages/core/test/src/server/mock-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index c99b89b85c..73db8bec6b 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -285,7 +285,7 @@ export async function createMockCluster({ ...options, skipSupervisor: true, name: clusterName + '_' + appName, - skipInstall: Boolean(i), + // skipInstall: Boolean(i), pubSubManager: { channelPrefix: clusterName, }, From 3e19b27d735b568d7598a5b49b9f831aa08ddd8a Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 12:36:49 +0000 Subject: [PATCH 34/70] test(server): avoid multiple installation in cluster --- packages/core/test/src/server/mock-server.ts | 2 +- .../plugin-workflow/src/server/__tests__/cluster.test.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 73db8bec6b..c99b89b85c 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -285,7 +285,7 @@ export async function createMockCluster({ ...options, skipSupervisor: true, name: clusterName + '_' + appName, - // skipInstall: Boolean(i), + skipInstall: Boolean(i), pubSubManager: { channelPrefix: clusterName, }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts index 17f9c0b5bd..86adeee367 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/cluster.test.ts @@ -42,7 +42,7 @@ describe('workflow > cluster', () => { const pro1 = (await p1.trigger(w1, {})) as Processor; expect(pro1.execution.status).toBe(EXECUTION_STATUS.RESOLVED); - await sleep(500); + await sleep(550); const p2 = app2.pm.get(Plugin) as Plugin; const w2 = p2.enabledCache.get(w1.id); @@ -59,11 +59,8 @@ describe('workflow > cluster', () => { const executions = await w1.getExecutions(); expect(executions.length).toBe(3); - await WorkflowRepo.update({ - filterByTk: w1.id, - values: { - enabled: false, - }, + await w1.update({ + enabled: false, }); await sleep(550); From aa41ae4cc9a7e7682f5c3dc7fda54ae8310d0210 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 1 Aug 2024 12:59:16 +0000 Subject: [PATCH 35/70] test(server): installation in cluster --- packages/core/test/src/server/mock-server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index c99b89b85c..1c6b3bbc97 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -280,7 +280,7 @@ export async function createMockCluster({ ...options }: MockClusterOptions = {}) { const nodes: MockServer[] = []; - for (const i of _.range(0, number || 2)) { + for (let i = 0; i < number; i++) { const app: MockServer = await createMockServer({ ...options, skipSupervisor: true, @@ -290,6 +290,7 @@ export async function createMockCluster({ channelPrefix: clusterName, }, }); + console.log('-------------', await app.isInstalled()); nodes.push(app); } return { From 28cda22a791fc1e7835d02d4dd79e1e20d2ad671 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Fri, 2 Aug 2024 22:51:57 +0800 Subject: [PATCH 36/70] feat: sync collection using sync manager (#4920) * chore: sync collection message * chore: sync acl * fix: typo * chore: sync data source * chore: remove collection * fix: typo * fix: test * chore: sync sub app event * chore: sync collection test * chore: collection test * chore: test * chore: data source sync message * chore: sync multi app * chore: test * chore: test * chore: test * chore: test * chore: test --- .../application/schema-initializer/types.ts | 2 +- .../schema-initializer/withInitializer.tsx | 2 +- .../core/client/src/pm/PluginManagerLink.tsx | 5 +- packages/core/server/src/plugin.ts | 17 +- .../core/server/src/sync-message-manager.ts | 5 +- .../core/test/src/server/mock-data-source.ts | 25 ++ packages/core/test/src/server/mock-server.ts | 25 +- .../src/server/model/RoleResourceModel.ts | 2 +- .../src/server/__tests__/cluster.test.ts | 235 ++++++++++++++++++ .../src/server/server.ts | 67 ++++- .../src/server/__tests__/cluster.test.ts | 49 ++++ .../src/server/plugin.ts | 205 ++++++++++++++- .../demos/MobileRoutesProvider-basic.tsx | 54 ++-- .../client/demos/MobileTabBar.Link-schema.tsx | 4 +- .../demos/MobileTabBar.Link-settings.tsx | 4 +- .../client/demos/MobileTabBar.Page-schema.tsx | 8 +- .../MobileTabBar.Item/schema.ts | 2 +- .../src/server/server.ts | 50 +++- 18 files changed, 693 insertions(+), 68 deletions(-) create mode 100644 packages/core/test/src/server/mock-data-source.ts create mode 100644 packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts create mode 100644 packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts diff --git a/packages/core/client/src/application/schema-initializer/types.ts b/packages/core/client/src/application/schema-initializer/types.ts index 188ee33f08..8a51a083b5 100644 --- a/packages/core/client/src/application/schema-initializer/types.ts +++ b/packages/core/client/src/application/schema-initializer/types.ts @@ -110,7 +110,7 @@ export interface SchemaInitializerOptions { insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; designable?: boolean; wrap?: (s: ISchema, options?: any) => ISchema; - useWrap?: () => ((s: ISchema, options?: any) => ISchema); + useWrap?: () => (s: ISchema, options?: any) => ISchema; onSuccess?: (data: any) => void; insert?: InsertType; useInsert?: () => InsertType; diff --git a/packages/core/client/src/application/schema-initializer/withInitializer.tsx b/packages/core/client/src/application/schema-initializer/withInitializer.tsx index 997a571d5e..a20515aa21 100644 --- a/packages/core/client/src/application/schema-initializer/withInitializer.tsx +++ b/packages/core/client/src/application/schema-initializer/withInitializer.tsx @@ -108,7 +108,7 @@ export function withInitializer(C: ComponentType) { {...popoverProps} arrow={false} overlayClassName={overlayClassName} - open={visible} + open={visible} onOpenChange={setVisible} content={wrapSSR(
{ return { key: setting.name, icon: setting.icon, - label: setting.link ?
window.open(setting.link)}>{compile(setting.title)}
: + label: setting.link ? ( +
window.open(setting.link)}>{compile(setting.title)}
+ ) : ( {compile(setting.title)} + ), }; }); }, [app, t]); diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index cc57119fc5..a77a3c02c1 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -16,7 +16,7 @@ import fs from 'fs'; import type { TFuncKey, TOptions } from 'i18next'; import { resolve } from 'path'; import { Application } from './application'; -import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager'; +import { getExposeChangelogUrl, getExposeReadmeUrl, InstallOptions } from './plugin-manager'; import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils'; export interface PluginInterface { @@ -138,6 +138,7 @@ export abstract class Plugin implements PluginInterface { if (!this.name) { throw new Error(`plugin name invalid`); } + await this.app.syncMessageManager.publish(this.name, message, options); } @@ -172,13 +173,6 @@ export abstract class Plugin implements PluginInterface { }); } - private async getPluginBasePath() { - if (!this.options.packageName) { - return; - } - return getPluginBasePath(this.options.packageName); - } - /** * @internal */ @@ -245,6 +239,13 @@ export abstract class Plugin implements PluginInterface { return results; } + + private async getPluginBasePath() { + if (!this.options.packageName) { + return; + } + return getPluginBasePath(this.options.packageName); + } } export default Plugin; diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts index 298e7822a2..4769e1dc66 100644 --- a/packages/core/server/src/sync-message-manager.ts +++ b/packages/core/server/src/sync-message-manager.ts @@ -39,17 +39,18 @@ export class SyncMessageManager { const timer = setTimeout(() => { reject(new Error('publish timeout')); }, 5000); + transaction.afterCommit(async () => { try { const r = await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { skipSelf: true, ...others, }); - clearTimeout(timer); resolve(r); } catch (error) { - clearTimeout(timer); reject(error); + } finally { + clearTimeout(timer); } }); }); diff --git a/packages/core/test/src/server/mock-data-source.ts b/packages/core/test/src/server/mock-data-source.ts new file mode 100644 index 0000000000..9e8ca8d773 --- /dev/null +++ b/packages/core/test/src/server/mock-data-source.ts @@ -0,0 +1,25 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CollectionManager, DataSource } from '@nocobase/data-source-manager'; +import { waitSecond } from '@nocobase/test'; + +export class MockDataSource extends DataSource { + static testConnection(options?: any): Promise { + return Promise.resolve(true); + } + + async load(): Promise { + await waitSecond(1000); + } + + createCollectionManager(options?: any): any { + return new CollectionManager(options); + } +} diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 1c6b3bbc97..465ab4d705 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -11,10 +11,10 @@ import { mockDatabase } from '@nocobase/database'; import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server'; import { uid } from '@nocobase/utils'; import jwt from 'jsonwebtoken'; -import _ from 'lodash'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; import { MemoryPubSubAdapter } from './memory-pub-sub-adapter'; +import { MockDataSource } from './mock-data-source'; interface ActionParams { filterByTk?: any; @@ -77,6 +77,10 @@ interface ExtendedAgent extends SuperAgentTest { } export class MockServer extends Application { + registerMockDataSource() { + this.dataSourceManager.factory.register('mock', MockDataSource); + } + async loadAndInstall(options: any = {}) { await this.load({ method: 'install' }); @@ -231,13 +235,15 @@ export function mockServer(options: ApplicationOptions = {}) { PluginManager.findPackagePatched = true; } - const app = new MockServer({ + const mockServerOptions = { acl: false, syncMessageManager: { debounce: 500, }, ...options, - }); + }; + + const app = new MockServer(mockServerOptions); const basename = app.options.pubSubManager?.channelPrefix; @@ -280,16 +286,27 @@ export async function createMockCluster({ ...options }: MockClusterOptions = {}) { const nodes: MockServer[] = []; + let dbOptions; + for (let i = 0; i < number; i++) { + if (dbOptions) { + options['database'] = { + ...dbOptions, + }; + } + const app: MockServer = await createMockServer({ ...options, skipSupervisor: true, name: clusterName + '_' + appName, - skipInstall: Boolean(i), pubSubManager: { channelPrefix: clusterName, }, }); + + if (!dbOptions) { + dbOptions = app.db.options; + } console.log('-------------', await app.isInstalled()); nodes.push(app); } diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts b/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts index 2c48fcc161..5559e899d4 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/server/model/RoleResourceModel.ts @@ -17,7 +17,7 @@ export class RoleResourceModel extends Model { role.revokeResource(resourceName); } - async writeToACL(options: { acl: ACL; transaction: any }) { + async writeToACL(options: { acl: ACL; transaction?: any }) { const { acl } = options; const resourceName = this.get('name') as string; const roleName = this.get('roleName') as string; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts new file mode 100644 index 0000000000..ce64564fc2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts @@ -0,0 +1,235 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createMockCluster, sleep } from '@nocobase/test'; + +describe('cluster', () => { + let cluster; + beforeEach(async () => { + cluster = await createMockCluster({ + plugins: ['error-handler', 'data-source-main', 'ui-schema-storage'], + acl: false, + }); + }); + + afterEach(async () => { + await cluster.destroy(); + }); + + describe('sync collection', () => { + test('create collection', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + }); + + test('update collection', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + description: 'test collection', + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('collections').update({ + filterByTk: 'tests', + values: { + description: 'new test collection', + }, + context: {}, + }); + + await sleep(2000); + + expect(testsCollection.options.description).toBe('new test collection'); + }); + + test('destroy collection', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('collections').destroy({ + filterByTk: 'tests', + context: {}, + }); + + await sleep(2000); + + expect(app2.db.getCollection('tests')).toBeFalsy(); + }); + }); + + describe('sync fields', () => { + test('create field', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('fields').create({ + values: { + name: 'age', + type: 'integer', + collectionName: 'tests', + }, + context: {}, + }); + + await sleep(2000); + + const ageField = testsCollection.getField('age'); + expect(ageField).toBeTruthy(); + }); + + test('update field', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('fields').create({ + values: { + name: 'age', + type: 'integer', + collectionName: 'tests', + }, + context: {}, + }); + + await sleep(2000); + + await app1.db.getRepository('collections.fields', 'tests').update({ + filterByTk: 'age', + values: { + description: 'age field', + }, + context: {}, + }); + + await sleep(2000); + + const ageField = testsCollection.getField('age'); + expect(ageField).toBeTruthy(); + + expect(ageField.options.description).toBe('age field'); + }); + + test('destroy field', async () => { + const [app1, app2] = cluster.nodes; + + await app1.db.getRepository('collections').create({ + values: { + name: 'tests', + fields: [ + { + name: 'name', + type: 'string', + }, + { + name: 'age', + type: 'integer', + }, + ], + }, + context: {}, + }); + + await sleep(2000); + + const testsCollection = app2.db.getCollection('tests'); + expect(testsCollection).toBeTruthy(); + + await app1.db.getRepository('collections.fields', 'tests').destroy({ + filterByTk: 'age', + context: {}, + }); + + await sleep(2000); + + expect(testsCollection.getField('age')).toBeFalsy(); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index ab61bee1e5..acf95af08e 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -43,7 +43,8 @@ export class PluginDataSourceMainServer extends Plugin { async handleSyncMessage(message) { const { type, collectionName } = message; - if (type === 'newCollection') { + + if (type === 'syncCollection') { const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({ filter: { name: collectionName, @@ -52,6 +53,26 @@ export class PluginDataSourceMainServer extends Plugin { await collectionModel.load(); } + + if (type === 'removeField') { + const { collectionName, fieldName } = message; + const collection = this.app.db.getCollection(collectionName); + if (!collection) { + return; + } + + return collection.removeFieldFromDb(fieldName); + } + + if (type === 'removeCollection') { + const { collectionName } = message; + const collection = this.app.db.getCollection(collectionName); + if (!collection) { + return; + } + + collection.remove(); + } } async beforeLoad() { @@ -92,10 +113,15 @@ export class PluginDataSourceMainServer extends Plugin { transaction, }); - this.sendSyncMessage({ - type: 'newCollection', - collectionName: model.get('name'), - }); + this.sendSyncMessage( + { + type: 'syncCollection', + collectionName: model.get('name'), + }, + { + transaction, + }, + ); } }, ); @@ -113,6 +139,16 @@ export class PluginDataSourceMainServer extends Plugin { } await model.remove(removeOptions); + + this.sendSyncMessage( + { + type: 'removeCollection', + collectionName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); // 要在 beforeInitOptions 之前处理 @@ -262,6 +298,16 @@ export class PluginDataSourceMainServer extends Plugin { }; await collection.sync(syncOptions); + + this.sendSyncMessage( + { + type: 'syncCollection', + collectionName: model.get('collectionName'), + }, + { + transaction, + }, + ); } }); @@ -273,6 +319,17 @@ export class PluginDataSourceMainServer extends Plugin { this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => { await mutex.runExclusive(async () => { await model.remove(options); + + this.sendSyncMessage( + { + type: 'removeField', + collectionName: model.get('collectionName'), + fieldName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts new file mode 100644 index 0000000000..f5154cc062 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/__tests__/cluster.test.ts @@ -0,0 +1,49 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createMockCluster, waitSecond } from '@nocobase/test'; + +describe('cluster', () => { + let cluster; + beforeEach(async () => { + cluster = await createMockCluster({ + plugins: ['nocobase'], + acl: false, + }); + + for (const node of cluster.nodes) { + node.registerMockDataSource(); + } + }); + + afterEach(async () => { + await cluster.destroy(); + }); + + test('create data source', async () => { + const app1 = cluster.nodes[0]; + + await app1.db.getRepository('dataSources').create({ + values: { + key: 'mockInstance1', + type: 'mock', + displayName: 'Mock', + options: {}, + }, + }); + + await waitSecond(2000); + + const dataSource = app1.dataSourceManager.get('mockInstance1'); + expect(dataSource).toBeDefined(); + + const dataSourceInApp2 = cluster.nodes[1].dataSourceManager.get('mockInstance1'); + expect(dataSourceInApp2).toBeDefined(); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts index 8df2e0677f..8200b7886f 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/server/plugin.ts @@ -36,6 +36,96 @@ export class PluginDataSourceManagerServer extends Plugin { [dataSourceKey: string]: DataSourceState; } = {}; + async handleSyncMessage(message) { + const { type } = message; + if (type === 'syncRole') { + const { roleName, dataSourceKey } = message; + const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey); + + const dataSourceRole: DataSourcesRolesModel = await this.app.db.getRepository('dataSourcesRoles').findOne({ + filter: { + dataSourceKey, + roleName, + }, + }); + + await dataSourceRole.writeToAcl({ + acl: dataSource.acl, + }); + } + + if (type === 'syncRoleResource') { + const { roleName, dataSourceKey, resourceName } = message; + const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey); + + const dataSourceRoleResource: DataSourcesRolesResourcesModel = await this.app.db + .getRepository('dataSourcesRolesResources') + .findOne({ + filter: { + dataSourceKey, + roleName, + name: resourceName, + }, + }); + + await dataSourceRoleResource.writeToACL({ + acl: dataSource.acl, + }); + } + if (type === 'loadDataSource') { + const { dataSourceKey } = message; + const dataSourceModel = await this.app.db.getRepository('dataSources').findOne({ + filter: { + key: dataSourceKey, + }, + }); + + if (!dataSourceModel) { + return; + } + + await dataSourceModel.loadIntoApplication({ + app: this.app, + }); + } + + if (type === 'loadDataSourceField') { + const { key } = message; + const fieldModel = await this.app.db.getRepository('dataSourcesFields').findOne({ + filter: { + key, + }, + }); + + fieldModel.load({ + app: this.app, + }); + } + if (type === 'removeDataSourceCollection') { + const { dataSourceKey, collectionName } = message; + const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey); + dataSource.collectionManager.removeCollection(collectionName); + } + + if (type === 'removeDataSourceField') { + const { key } = message; + const fieldModel = await this.app.db.getRepository('dataSourcesFields').findOne({ + filter: { + key, + }, + }); + + fieldModel.unload({ + app: this.app, + }); + } + + if (type === 'removeDataSource') { + const { dataSourceKey } = message; + this.app.dataSourceManager.dataSources.delete(dataSourceKey); + } + } + async beforeLoad() { this.app.db.registerModels({ DataSourcesCollectionModel, @@ -100,6 +190,16 @@ export class PluginDataSourceManagerServer extends Plugin { model.loadIntoApplication({ app: this.app, }); + + this.sendSyncMessage( + { + type: 'loadDataSource', + dataSourceKey: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); } }); @@ -264,6 +364,7 @@ export class PluginDataSourceManagerServer extends Plugin { } }); + const self = this; this.app.actions({ async ['dataSources:listEnabled'](ctx, next) { const dataSources = await ctx.db.getRepository('dataSources').find({ @@ -302,6 +403,7 @@ export class PluginDataSourceManagerServer extends Plugin { async ['dataSources:refresh'](ctx, next) { const { filterByTk, clientStatus } = ctx.action.params; + const dataSourceModel: DataSourceModel = await ctx.db.getRepository('dataSources').findOne({ filter: { key: filterByTk, @@ -317,6 +419,11 @@ export class PluginDataSourceManagerServer extends Plugin { dataSourceModel.loadIntoApplication({ app: ctx.app, }); + + ctx.app.syncMessageManager.publish(self.name, { + type: 'loadDataSource', + dataSourceKey: dataSourceModel.get('key'), + }); } ctx.body = { @@ -352,21 +459,52 @@ export class PluginDataSourceManagerServer extends Plugin { } }); - this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel) => { + this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel, options) => { const dataSource = this.app.dataSourceManager.dataSources.get(model.get('dataSourceKey')); dataSource.collectionManager.removeCollection(model.get('name')); + + this.sendSyncMessage( + { + type: 'removeDataSourceCollection', + dataSourceKey: model.get('dataSourceKey'), + collectionName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); - this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel) => { + this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel, options) => { model.load({ app: this.app, }); + + this.sendSyncMessage( + { + type: 'loadDataSourceField', + key: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); }); - this.app.db.on('dataSourcesFields.afterDestroy', async (model: DataSourcesFieldModel) => { + this.app.db.on('dataSourcesFields.afterDestroy', async (model: DataSourcesFieldModel, options) => { model.unload({ app: this.app, }); + + this.sendSyncMessage( + { + type: 'removeDataSourceField', + key: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); }); this.app.db.on( @@ -379,8 +517,18 @@ export class PluginDataSourceManagerServer extends Plugin { }, ); - this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel) => { + this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel, options) => { this.app.dataSourceManager.dataSources.delete(model.get('key')); + + this.sendSyncMessage( + { + type: 'removeDataSource', + dataSourceKey: model.get('key'), + }, + { + transaction: options.transaction, + }, + ); }); this.app.on('afterStart', async (app: Application) => { @@ -412,6 +560,19 @@ export class PluginDataSourceManagerServer extends Plugin { acl: dataSource.acl, transaction: transaction, }); + + // sync roles resources between nodes + this.sendSyncMessage( + { + type: 'syncRoleResource', + roleName: model.get('roleName'), + dataSourceKey: model.get('dataSourceKey'), + resourceName: model.get('name'), + }, + { + transaction, + }, + ); }, ); @@ -429,6 +590,18 @@ export class PluginDataSourceManagerServer extends Plugin { acl: dataSource.acl, transaction: transaction, }); + + this.sendSyncMessage( + { + type: 'syncRoleResource', + roleName: resource.get('roleName'), + dataSourceKey: resource.get('dataSourceKey'), + resourceName: resource.get('name'), + }, + { + transaction, + }, + ); }, ); @@ -440,6 +613,18 @@ export class PluginDataSourceManagerServer extends Plugin { if (role) { role.revokeResource(model.get('name')); } + + this.sendSyncMessage( + { + type: 'syncRoleResource', + roleName, + dataSourceKey: model.get('dataSourceKey'), + resourceName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); this.app.db.on('dataSourcesRoles.afterSave', async (model: DataSourcesRolesModel, options) => { @@ -462,6 +647,18 @@ export class PluginDataSourceManagerServer extends Plugin { hooks: false, transaction, }); + + // sync role between nodes + this.sendSyncMessage( + { + type: 'syncRole', + roleName: model.get('roleName'), + dataSourceKey: model.get('dataSourceKey'), + }, + { + transaction, + }, + ); }); this.app.on('acl:writeResources', async ({ roleName, transaction }) => { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx index 44d0d4d88b..f37d1cbbcf 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileRoutesProvider-basic.tsx @@ -46,36 +46,34 @@ const app = mockApp({ 'mobileRoutes:list': { data: [ { - "id": 10, - "createdAt": "2024-07-08T13:22:33.763Z", - "updatedAt": "2024-07-08T13:22:33.763Z", - "parentId": null, - "title": "Test1", - "icon": "AppstoreOutlined", - "schemaUid": "test", - "type": "page", - "options": null, - "sort": 1, - "createdById": 1, - "updatedById": 1, + id: 10, + createdAt: '2024-07-08T13:22:33.763Z', + updatedAt: '2024-07-08T13:22:33.763Z', + parentId: null, + title: 'Test1', + icon: 'AppstoreOutlined', + schemaUid: 'test', + type: 'page', + options: null, + sort: 1, + createdById: 1, + updatedById: 1, }, { - "id": 13, - "createdAt": "2024-07-08T13:23:01.929Z", - "updatedAt": "2024-07-08T13:23:12.433Z", - "parentId": null, - "title": "Test2", - "icon": "aliwangwangoutlined", - "schemaUid": null, - "type": "link", - "options": { - "schemaUid": null, - "url": "https://github.com", - "params": [ - {} - ] - } - } + id: 13, + createdAt: '2024-07-08T13:23:01.929Z', + updatedAt: '2024-07-08T13:23:12.433Z', + parentId: null, + title: 'Test2', + icon: 'aliwangwangoutlined', + schemaUid: null, + type: 'link', + options: { + schemaUid: null, + url: 'https://github.com', + params: [{}], + }, + }, ], }, }, diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx index e7d4ce01bf..b2012ac28a 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-schema.tsx @@ -15,8 +15,8 @@ const Demo = () => { title: 'Link', icon: 'AppstoreOutlined', options: { - url: 'https://github.com' - } + url: 'https://github.com', + }, }), )} /> diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx index b3d74786c4..31a50f695f 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Link-settings.tsx @@ -16,8 +16,8 @@ const schema = getMobileTabBarItemSchema({ title: 'Link', icon: 'AppstoreOutlined', options: { - url: 'https://github.com' - } + url: 'https://github.com', + }, }); const Demo = () => { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx index fad8c60625..febb8124bb 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/MobileTabBar.Page-schema.tsx @@ -14,13 +14,7 @@ const schema = getMobileTabBarItemSchema({ }); const Demo = () => { - return ( - - ); + return ; }; class MyPlugin extends Plugin { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts index 6a9691873e..22c9d4222e 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile-layout/mobile-tab-bar/MobileTabBar.Item/schema.ts @@ -18,5 +18,5 @@ export function getMobileTabBarItemSchema(routeItem: MobileRouteItem) { schemaUid: routeItem.schemaUid, ...(routeItem.options || {}), }, - } + }; } diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts index 1252cf0109..a5fd542b2d 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/server.ts @@ -143,6 +143,34 @@ export class PluginMultiAppManagerServer extends Plugin { return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator'])); } + async handleSyncMessage(message) { + const { type } = message; + + if (type === 'startApp') { + const { appName } = message; + const model = await this.app.db.getRepository('applications').findOne({ + filter: { + name: appName, + }, + }); + + if (!model) { + return; + } + + const subApp = model.registerToSupervisor(this.app, { + appOptionsFactory: this.appOptionsFactory, + }); + + subApp.runCommand('start', '--quickstart'); + } + + if (type === 'removeApp') { + const { appName } = message; + await AppSupervisor.getInstance().removeApp(appName); + } + } + setSubAppUpgradeHandler(handler: SubAppUpgradeHandler) { this.subAppUpgradeHandler = handler; } @@ -186,6 +214,16 @@ export class PluginMultiAppManagerServer extends Plugin { context: options.context, }); + this.sendSyncMessage( + { + type: 'startApp', + appName: name, + }, + { + transaction, + }, + ); + const startPromise = subApp.runCommand('start', '--quickstart'); if (options?.context?.waitSubAppInstall) { @@ -194,8 +232,18 @@ export class PluginMultiAppManagerServer extends Plugin { }, ); - this.db.on('applications.afterDestroy', async (model: ApplicationModel) => { + this.db.on('applications.afterDestroy', async (model: ApplicationModel, options) => { await AppSupervisor.getInstance().removeApp(model.get('name') as string); + + this.sendSyncMessage( + { + type: 'removeApp', + appName: model.get('name'), + }, + { + transaction: options.transaction, + }, + ); }); const self = this; From 9edaa9183a417e742a3aa2d1dcd06ec6acc13a11 Mon Sep 17 00:00:00 2001 From: Chareice Date: Sun, 4 Aug 2024 08:45:51 +0800 Subject: [PATCH 37/70] chore: error message --- packages/core/server/src/sync-message-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts index 4769e1dc66..600cf7a377 100644 --- a/packages/core/server/src/sync-message-manager.ts +++ b/packages/core/server/src/sync-message-manager.ts @@ -37,7 +37,7 @@ export class SyncMessageManager { if (transaction) { return await new Promise((resolve, reject) => { const timer = setTimeout(() => { - reject(new Error('publish timeout')); + reject(new Error(`Publish message to ${channel} timeout, message: ${JSON.stringify(message)}`)); }, 5000); transaction.afterCommit(async () => { From 447c54df9f17ffdfa5a92af1940c987f3ff2893b Mon Sep 17 00:00:00 2001 From: mytharcher Date: Fri, 2 Aug 2024 02:02:19 +0000 Subject: [PATCH 38/70] fix(server): add type and remove log --- packages/core/test/src/server/mock-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 465ab4d705..503b04b520 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -307,7 +307,7 @@ export async function createMockCluster({ if (!dbOptions) { dbOptions = app.db.options; } - console.log('-------------', await app.isInstalled()); + nodes.push(app); } return { @@ -322,7 +322,7 @@ export async function createMockCluster({ export async function createMockServer(options: MockServerOptions = {}) { const { version, beforeInstall, skipInstall, skipStart, ...others } = options; - const app: any = mockServer(others); + const app: MockServer = mockServer(others); if (!skipInstall) { if (beforeInstall) { await beforeInstall(app); From a9cc8a17fa86875d14900cb41c5117c8f456ec86 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Sun, 4 Aug 2024 03:39:15 +0000 Subject: [PATCH 39/70] fix(server): not to publish when adpater is not connected --- packages/core/server/src/pub-sub-manager/pub-sub-manager.ts | 2 +- packages/core/server/src/pub-sub-manager/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts index 7c14a64361..bba69f9f23 100644 --- a/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts +++ b/packages/core/server/src/pub-sub-manager/pub-sub-manager.ts @@ -94,7 +94,7 @@ export class PubSubManager { } async publish(channel: string, message: any, options?: PubSubManagerPublishOptions) { - if (!this.adapter) { + if (!this.adapter?.isConnected()) { return; } diff --git a/packages/core/server/src/pub-sub-manager/types.ts b/packages/core/server/src/pub-sub-manager/types.ts index 0fca8cfb66..64240cb4ca 100644 --- a/packages/core/server/src/pub-sub-manager/types.ts +++ b/packages/core/server/src/pub-sub-manager/types.ts @@ -23,7 +23,7 @@ export interface PubSubManagerSubscribeOptions { export type PubSubCallback = (message: any) => Promise; export interface IPubSubAdapter { - isConnected(): Promise; + isConnected(): Promise | boolean; connect(): Promise; close(): Promise; subscribe(channel: string, callback: PubSubCallback): Promise; From 77af3154cfd7871c9a42f2b523eaae6de98d836d Mon Sep 17 00:00:00 2001 From: mytharcher Date: Sun, 4 Aug 2024 08:24:51 +0000 Subject: [PATCH 40/70] refactor(server): refine types --- packages/core/server/src/pub-sub-manager/types.ts | 2 +- packages/core/test/src/server/index.ts | 2 +- .../server/{mock-cluster.ts => mock-isolated-cluster.ts} | 6 +++--- packages/core/test/src/server/mock-server.ts | 7 ++++++- 4 files changed, 11 insertions(+), 6 deletions(-) rename packages/core/test/src/server/{mock-cluster.ts => mock-isolated-cluster.ts} (94%) diff --git a/packages/core/server/src/pub-sub-manager/types.ts b/packages/core/server/src/pub-sub-manager/types.ts index 64240cb4ca..4ed2acd3d3 100644 --- a/packages/core/server/src/pub-sub-manager/types.ts +++ b/packages/core/server/src/pub-sub-manager/types.ts @@ -28,5 +28,5 @@ export interface IPubSubAdapter { close(): Promise; subscribe(channel: string, callback: PubSubCallback): Promise; unsubscribe(channel: string, callback: PubSubCallback): Promise; - publish(channel: string, message: any): Promise; + publish(channel: string, message: string): Promise; } diff --git a/packages/core/test/src/server/index.ts b/packages/core/test/src/server/index.ts index 329e132dfa..d290c44e15 100644 --- a/packages/core/test/src/server/index.ts +++ b/packages/core/test/src/server/index.ts @@ -13,7 +13,7 @@ import ws from 'ws'; export { MockDatabase, mockDatabase } from '@nocobase/database'; export { default as supertest } from 'supertest'; export * from './memory-pub-sub-adapter'; -export * from './mock-cluster'; +export * from './mock-isolated-cluster'; export * from './mock-server'; export const pgOnly: () => any = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip); diff --git a/packages/core/test/src/server/mock-cluster.ts b/packages/core/test/src/server/mock-isolated-cluster.ts similarity index 94% rename from packages/core/test/src/server/mock-cluster.ts rename to packages/core/test/src/server/mock-isolated-cluster.ts index 4afdacff8a..b2b08ddcda 100644 --- a/packages/core/test/src/server/mock-cluster.ts +++ b/packages/core/test/src/server/mock-isolated-cluster.ts @@ -14,19 +14,19 @@ import { getPortPromise } from 'portfinder'; import { uid } from '@nocobase/utils'; import { createMockServer } from './mock-server'; -type ClusterOptions = { +type IsolatedClusterOptions = { script?: string; env?: Record; plugins?: string[]; instances?: number; }; -export class MockCluster { +export class MockIsolatedCluster { private script = `${process.env.APP_PACKAGE_ROOT}/src/index.ts`; private processes = []; private mockApp; - constructor(private options: ClusterOptions = {}) { + constructor(private options: IsolatedClusterOptions = {}) { if (options.script) { this.script = options.script; } diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 503b04b520..966504bc01 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -279,12 +279,17 @@ export type MockClusterOptions = MockServerOptions & { appName?: string; }; +export type MockCluster = { + nodes: MockServer[]; + destroy: () => Promise; +}; + export async function createMockCluster({ number = 2, clusterName = `cluster_${uid()}`, appName = `app_${uid()}`, ...options -}: MockClusterOptions = {}) { +}: MockClusterOptions = {}): Promise { const nodes: MockServer[] = []; let dbOptions; From 349fb319c43216d8ff03d527c624010a329d39e3 Mon Sep 17 00:00:00 2001 From: Chareice Date: Sun, 4 Aug 2024 17:31:12 +0800 Subject: [PATCH 41/70] chore: timeout --- packages/core/server/src/sync-message-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/server/src/sync-message-manager.ts b/packages/core/server/src/sync-message-manager.ts index 600cf7a377..6b3735baa6 100644 --- a/packages/core/server/src/sync-message-manager.ts +++ b/packages/core/server/src/sync-message-manager.ts @@ -38,7 +38,7 @@ export class SyncMessageManager { return await new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Publish message to ${channel} timeout, message: ${JSON.stringify(message)}`)); - }, 5000); + }, 50000); transaction.afterCommit(async () => { try { From fc0cab3cb5cf3381d9734c7827c9c6e69c713920 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 7 Aug 2024 15:45:24 +0000 Subject: [PATCH 42/70] fix(server): fix pubSubManager options --- packages/core/server/src/application.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 36f83924e9..6a5d9c022c 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -1129,10 +1129,7 @@ export class Application exten this._cli = this.createCLI(); this._i18n = createI18n(options); - this.pubSubManager = createPubSubManager(this, { - channelPrefix: this.name, - ...options.pubSubManager, - }); + this.pubSubManager = createPubSubManager(this, options.pubSubManager); this.syncMessageManager = new SyncMessageManager(this, options.syncMessageManager); this.context.db = this.db; From 3fc617b7cc5df4cec3c366834012f193c3a393c0 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Sat, 10 Aug 2024 09:01:12 +0000 Subject: [PATCH 43/70] test(ci): test ci checkout --- .github/workflows/manual-build-pro-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/manual-build-pro-image.yml b/.github/workflows/manual-build-pro-image.yml index fbddf5f7a4..dbb657bc12 100644 --- a/.github/workflows/manual-build-pro-image.yml +++ b/.github/workflows/manual-build-pro-image.yml @@ -26,7 +26,7 @@ jobs: - 4873:4873 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.base_branch }} ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} @@ -38,7 +38,7 @@ jobs: - name: Echo PR branch run: echo "${{ steps.set_pro_pr_branch.outputs.pr_branch }}" - name: Checkout pro-plugins - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nocobase/pro-plugins path: packages/pro-plugins From 71a1ddf69e74505dcd8e8258b49b4c86953ef254 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Mon, 12 Aug 2024 01:56:32 +0000 Subject: [PATCH 44/70] feat(server): add lock manager to server --- packages/core/database/package.json | 2 +- packages/core/server/package.json | 1 + .../server/src/__tests__/lock-manager.test.ts | 132 ++++++++++++++++++ packages/core/server/src/application.ts | 8 ++ packages/core/server/src/index.ts | 3 + packages/core/server/src/lock-manager.ts | 127 +++++++++++++++++ .../plugins/@nocobase/plugin-acl/package.json | 2 +- .../plugin-action-export/package.json | 2 +- .../plugin-data-source-main/package.json | 2 +- .../plugin-multi-app-manager/package.json | 2 +- 10 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 packages/core/server/src/__tests__/lock-manager.test.ts create mode 100644 packages/core/server/src/lock-manager.ts diff --git a/packages/core/database/package.json b/packages/core/database/package.json index acd51010c6..a79e2faf69 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -8,7 +8,7 @@ "dependencies": { "@nocobase/logger": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha", - "async-mutex": "^0.3.2", + "async-mutex": "^0.5.0", "chalk": "^4.1.1", "cron-parser": "4.4.0", "dayjs": "^1.11.8", diff --git a/packages/core/server/package.json b/packages/core/server/package.json index d353adeb32..79ac42b769 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -26,6 +26,7 @@ "@types/ini": "^1.3.31", "@types/koa-send": "^4.1.3", "@types/multer": "^1.4.5", + "async-mutex": "^0.5.0", "axios": "^0.26.1", "chalk": "^4.1.1", "commander": "^9.2.0", diff --git a/packages/core/server/src/__tests__/lock-manager.test.ts b/packages/core/server/src/__tests__/lock-manager.test.ts new file mode 100644 index 0000000000..0092803cec --- /dev/null +++ b/packages/core/server/src/__tests__/lock-manager.test.ts @@ -0,0 +1,132 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Mutex, withTimeout } from 'async-mutex'; + +import { Application } from '../application'; +import { LocalLock } from '../lock-manager'; + +function sleep(ms = 1000) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +describe('lock manager', () => { + describe.skip('mutex example', () => { + it('mutex', async () => { + const order = []; + const lock = new Mutex(); + const release1 = await lock.acquire(); + order.push(1); + expect(release1).toBeDefined(); + expect(lock.isLocked()).toBe(true); + setTimeout(async () => { + order.push(2); + await lock.release(); + order.push(3); + expect(lock.isLocked()).toBe(true); + }, 200); + order.push(4); + const release2 = await lock.acquire(); + order.push(5); + expect(lock.isLocked()).toBe(true); + await release2(); + order.push(6); + expect(lock.isLocked()).toBe(false); + expect(order).toEqual([1, 4, 2, 3, 5, 6]); + }); + + it.skip('with timeout', async () => { + const lock = withTimeout(new Mutex(), 200); + const r1 = await lock.acquire(); + expect(lock.isLocked()).toBe(true); + const l2 = lock.acquire(); + await sleep(100); + expect(lock.isLocked()).toBe(true); + setTimeout(async () => { + expect(lock.isLocked()).toBe(false); + const r2 = await l2; + expect(lock.isLocked()).toBe(true); + await r2(); + expect(lock.isLocked()).toBe(false); + }, 150); + await sleep(300); + }); + }); + + describe('local lock', () => { + let app: Application; + + beforeEach(() => { + app = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + resourcer: { + prefix: '/api', + }, + acl: false, + dataWrapping: false, + registerActions: false, + }); + }); + + afterEach(async () => { + return app.destroy(); + }); + + it('base api', async () => { + expect(app.lockManager).toBeDefined(); + expect(await app.lockManager.getLock('a')).toBeInstanceOf(LocalLock); + }); + + it('acquire and release', async () => { + const order = []; + const lock1 = await app.lockManager.getLock('test'); + expect(lock1).toBeDefined(); + order.push(1); + await lock1.acquire(); + order.push(2); + setTimeout(async () => { + order.push(3); + await lock1.release(); + order.push(4); + }, 200); + order.push(5); + await lock1.acquire(); + order.push(6); + await lock1.release(); + order.push(7); + expect(order).toEqual([1, 2, 5, 3, 4, 6, 7]); + }); + + it('runExclusive', async () => { + const order = []; + const lock = await app.lockManager.getLock('test'); + setTimeout(async () => { + await lock.runExclusive(async () => { + order.push(1); + await sleep(100); + order.push(2); + }); + }, 100); + order.push(3); + await lock.runExclusive(async () => { + order.push(4); + await sleep(500); + order.push(5); + }); + order.push(6); + await sleep(200); + expect(order).toEqual([3, 4, 5, 6, 1, 2]); + }); + }); +}); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 6a5d9c022c..0928d540d8 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -62,6 +62,7 @@ import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; import { SyncMessageManager } from './sync-message-manager'; +import { LockManager, LockManagerOptions } from './lock-manager'; import packageJson from '../package.json'; @@ -115,6 +116,7 @@ export interface ApplicationOptions { pmSock?: string; name?: string; authManager?: AuthManagerOptions; + lockManager?: LockManagerOptions; /** * @internal */ @@ -226,6 +228,8 @@ export class Application exten private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; private _actionCommand: Command; + public lockManager: LockManager; + /** * @internal */ @@ -1131,6 +1135,10 @@ export class Application exten this._i18n = createI18n(options); this.pubSubManager = createPubSubManager(this, options.pubSubManager); this.syncMessageManager = new SyncMessageManager(this, options.syncMessageManager); + this.lockManager = new LockManager({ + defaultAdapter: process.env.LOCK_ADAPTER_DEFAULT, + ...options.lockManager, + }); this.context.db = this.db; /** diff --git a/packages/core/server/src/index.ts b/packages/core/server/src/index.ts index bf674e8f23..a78e1dfc59 100644 --- a/packages/core/server/src/index.ts +++ b/packages/core/server/src/index.ts @@ -16,4 +16,7 @@ export * from './migration'; export * from './plugin'; export * from './plugin-manager'; export * from './pub-sub-manager'; +export * from './gateway'; +export * from './app-supervisor'; +export * from './lock-manager'; export const OFFICIAL_PLUGIN_PREFIX = '@nocobase/plugin-'; diff --git a/packages/core/server/src/lock-manager.ts b/packages/core/server/src/lock-manager.ts new file mode 100644 index 0000000000..e9a2bd24f7 --- /dev/null +++ b/packages/core/server/src/lock-manager.ts @@ -0,0 +1,127 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Registry } from '@nocobase/utils'; +import { Mutex, withTimeout, MutexInterface, E_CANCELED } from 'async-mutex'; + +export abstract class AbstractLockAdapter { + async connect() {} + abstract getLock(key: string, ttl: number): L | Promise; + abstract acquire(key: string, ttl: number): L | Promise; + abstract release(key: string): void | Promise; +} + +export abstract class AbstractLock { + abstract acquire(): Promise; + abstract release(): Promise; + abstract runExclusive(fn: () => Promise): Promise; +} + +export class LockAbortError extends Error { + constructor(message, options) { + super(message, options); + } +} + +export class LocalLock extends AbstractLock { + private lock: MutexInterface; + + constructor(ttl) { + super(); + this.lock = withTimeout(new Mutex(), ttl); + } + + async acquire() { + await this.lock.acquire(); + return this; + } + + async release() { + this.lock.release(); + } + + async runExclusive(fn: () => Promise): Promise { + try { + return this.lock.runExclusive(fn); + } catch (e) { + if (e === E_CANCELED) { + throw new LockAbortError('Lock aborted', { cause: E_CANCELED }); + } else { + throw e; + } + } + } +} + +class LocalLockAdapter extends AbstractLockAdapter { + private locks = new Map(); + + getLock(key: string, ttl: number): LocalLock { + let lock = this.locks.get(key); + if (!lock) { + lock = new LocalLock(ttl); + this.locks.set(key, lock); + } + return lock; + } + + async acquire(key: string, ttl?: number) { + const lockInstance = this.getLock(key, ttl); + return lockInstance.acquire(); + } + + async release(lockKey) { + if (this.locks.has(lockKey)) { + const lock = this.locks.get(lockKey); + await lock.release(); + } + } +} + +export interface LockAdapterConfig { + Client: new (...args: any[]) => C; + [key: string]: any; +} + +export interface LockManagerOptions { + defaultAdapter?: string; +} + +export class LockManager { + private registry = new Registry(); + private clients = new Map(); + + constructor(private options: LockManagerOptions = {}) { + this.registry.register('local', { + Client: LocalLockAdapter, + }); + } + + registerAdapter(name: string, adapterConfig: LockAdapterConfig) { + this.registry.register(name, adapterConfig); + } + + async getLock(key: string, ttl = 500): Promise { + const type = this.options.defaultAdapter || 'local'; + let client = this.clients.get(type); + if (!client) { + const adapter = this.registry.get(type); + if (!adapter) { + throw new Error(`Lock adapter "${type}" not registered`); + } + + const { Client, ...config } = adapter; + client = new Client(config); + await client.connect(); + this.clients.set(type, client); + } + + return client.getLock(key, ttl); + } +} diff --git a/packages/plugins/@nocobase/plugin-acl/package.json b/packages/plugins/@nocobase/plugin-acl/package.json index 2795a3b584..ed02957a8e 100644 --- a/packages/plugins/@nocobase/plugin-acl/package.json +++ b/packages/plugins/@nocobase/plugin-acl/package.json @@ -14,7 +14,7 @@ ], "devDependencies": { "@types/jsonwebtoken": "^8.5.8", - "async-mutex": "^0.3.2", + "async-mutex": "^0.5.0", "jsonwebtoken": "^8.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/plugins/@nocobase/plugin-action-export/package.json b/packages/plugins/@nocobase/plugin-action-export/package.json index 72d0672d28..85e9d7e1fc 100644 --- a/packages/plugins/@nocobase/plugin-action-export/package.json +++ b/packages/plugins/@nocobase/plugin-action-export/package.json @@ -14,7 +14,7 @@ "@formily/react": "2.x", "@formily/shared": "2.x", "@types/node-xlsx": "^0.15.1", - "async-mutex": "^0.3.2", + "async-mutex": "^0.5.0", "file-saver": "^2.0.5", "node-xlsx": "^0.16.1", "react": "^18.2.0", diff --git a/packages/plugins/@nocobase/plugin-data-source-main/package.json b/packages/plugins/@nocobase/plugin-data-source-main/package.json index 7b51d7eb01..d02d57b39e 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/package.json +++ b/packages/plugins/@nocobase/plugin-data-source-main/package.json @@ -11,7 +11,7 @@ "license": "AGPL-3.0", "devDependencies": { "@hapi/topo": "^6.0.0", - "async-mutex": "^0.3.2", + "async-mutex": "^0.5.0", "toposort": "^2.0.2" }, "peerDependencies": { diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/package.json b/packages/plugins/@nocobase/plugin-multi-app-manager/package.json index af14f04dfb..cc4a2e907b 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/package.json +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/package.json @@ -14,7 +14,7 @@ "@formily/shared": "2.x", "antd": "5.x", "antd-style": "3.x", - "async-mutex": "^0.3.2", + "async-mutex": "^0.5.0", "mysql2": "^3.11.0", "pg": "^8.7.3", "react": "18.x", From 04c25e4460e551f137c35df87428186e5824422b Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 13 Aug 2024 09:47:02 +0800 Subject: [PATCH 45/70] feat: update ci --- .github/workflows/build-docker-image.yml | 1 + .github/workflows/build-pro-image.yml | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 80eced7754..9b7f3f544a 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -7,6 +7,7 @@ concurrency: on: push: branches: + - 'develop' - 'main' - 'next' paths: diff --git a/.github/workflows/build-pro-image.yml b/.github/workflows/build-pro-image.yml index de5761a3a2..9a1b3204a7 100644 --- a/.github/workflows/build-pro-image.yml +++ b/.github/workflows/build-pro-image.yml @@ -7,6 +7,7 @@ concurrency: on: push: branches: + - 'develop' - 'main' - 'next' paths: @@ -33,7 +34,7 @@ jobs: uses: actions/checkout@v3 with: repository: nocobase/pro-plugins - ref: main + ref: next path: packages/pro-plugins fetch-depth: 0 ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} @@ -45,7 +46,7 @@ jobs: if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then git checkout ${{ github.event.pull_request.base.ref }} else - git checkout main + git checkout next fi fi - name: rm .git From 615384e3cf87fdf71bb644cb85c27649637a68e6 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Mon, 26 Aug 2024 17:57:21 +0800 Subject: [PATCH 46/70] feat: pub/sub manager (#4933) * feat: pub/sub manager * fix: test case * fix: test error * fix: test error * feat: skip self * feat: debounce * feat: improve code * fix: test error * feat: test cases * feat: test cases * fix: improve code * fix: improve code * feat: improve code * fix: improve code * fix: test case * fix: typo * fix: createPubSubManager * fix: delete messageHandlers * fix: test case * feat: improve code * fix: test error * fix: test error * refactor(server): adapt to new api and fix test * fix(plugin-data-source-main): fix changed api * fix: test error * fix: remove sync-manager test case * chore(server): remove legacy code * fix(plugin-workflow): fix send sync message with transaction * chore(server): remove legacy code * chore(server): remove legacy code * fix(plugin-workflow): fix test case * fix(plugin-workflow): fix test case * test(server): test skip-install parameter in cluster * test(server): avoid multiple installation in cluster * test(server): installation in cluster * feat: sync collection using sync manager (#4920) * chore: sync collection message * chore: sync acl * fix: typo * chore: sync data source * chore: remove collection * fix: typo * fix: test * chore: sync sub app event * chore: sync collection test * chore: collection test * chore: test * chore: data source sync message * chore: sync multi app * chore: test * chore: test * chore: test * chore: test * chore: test * chore: error message * fix(server): add type and remove log * fix(server): not to publish when adpater is not connected * refactor(server): refine types * chore: timeout * fix(server): fix pubSubManager options * test(ci): test ci checkout --------- Co-authored-by: mytharcher Co-authored-by: ChengLei Shao --- packages/core/server/src/application.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 0928d540d8..c3c7421d7d 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -64,8 +64,6 @@ import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub- import { SyncMessageManager } from './sync-message-manager'; import { LockManager, LockManagerOptions } from './lock-manager'; -import packageJson from '../package.json'; - export type PluginType = string | typeof Plugin; export type PluginConfiguration = PluginType | [PluginType, any]; From 47ced56eb6eb038b83f6377cff3e47742c7524ba Mon Sep 17 00:00:00 2001 From: mytharcher Date: Tue, 13 Aug 2024 16:00:02 +0000 Subject: [PATCH 47/70] refactor(server): refactor api and local lock --- .../server/src/__tests__/lock-manager.test.ts | 73 ++++++++++++---- packages/core/server/src/lock-manager.ts | 85 ++++++++++++------- 2 files changed, 110 insertions(+), 48 deletions(-) diff --git a/packages/core/server/src/__tests__/lock-manager.test.ts b/packages/core/server/src/__tests__/lock-manager.test.ts index 0092803cec..a07cea07dd 100644 --- a/packages/core/server/src/__tests__/lock-manager.test.ts +++ b/packages/core/server/src/__tests__/lock-manager.test.ts @@ -85,48 +85,89 @@ describe('lock manager', () => { it('base api', async () => { expect(app.lockManager).toBeDefined(); - expect(await app.lockManager.getLock('a')).toBeInstanceOf(LocalLock); }); it('acquire and release', async () => { const order = []; - const lock1 = await app.lockManager.getLock('test'); - expect(lock1).toBeDefined(); + const r1 = await app.lockManager.acquire('test'); order.push(1); - await lock1.acquire(); - order.push(2); setTimeout(async () => { + order.push(2); + await r1(); order.push(3); - await lock1.release(); - order.push(4); }, 200); + order.push(4); + const r2 = await app.lockManager.acquire('test'); order.push(5); - await lock1.acquire(); + await r2(); order.push(6); - await lock1.release(); - order.push(7); - expect(order).toEqual([1, 2, 5, 3, 4, 6, 7]); + expect(order).toEqual([1, 4, 2, 3, 5, 6]); + }); + + it('acquire and release with timeout', async () => { + const order = []; + const r1 = await app.lockManager.acquire('test', 200); + order.push(1); + setTimeout(async () => { + order.push(2); + await r1(); + order.push(3); + }, 400); + order.push(4); + const r2 = await app.lockManager.acquire('test', 200); + order.push(5); + await sleep(300); + await r2(); + order.push(6); + expect(order).toEqual([1, 4, 5, 2, 3, 6]); }); it('runExclusive', async () => { const order = []; - const lock = await app.lockManager.getLock('test'); setTimeout(async () => { - await lock.runExclusive(async () => { + await app.lockManager.runExclusive('test', async () => { order.push(1); await sleep(100); order.push(2); }); }, 100); order.push(3); - await lock.runExclusive(async () => { + await app.lockManager.runExclusive('test', async () => { order.push(4); - await sleep(500); + await sleep(400); order.push(5); }); order.push(6); await sleep(200); - expect(order).toEqual([3, 4, 5, 6, 1, 2]); + expect(order).toEqual([3, 4, 5, 1, 6, 2]); + }); + + it('runExclusive with timeout', async () => { + const order = []; + setTimeout(async () => { + await app.lockManager.runExclusive( + 'test', + async () => { + order.push(1); + await sleep(200); + order.push(2); + }, + 200, + ); + }, 100); + order.push(3); + await app.lockManager.runExclusive( + 'test', + async () => { + order.push(4); + await sleep(400); + order.push(5); + }, + 200, + ); + order.push(6); + await sleep(200); + expect(order).toEqual([3, 4, 5, 1, 6, 2]); }); }); }); diff --git a/packages/core/server/src/lock-manager.ts b/packages/core/server/src/lock-manager.ts index e9a2bd24f7..9cdf69cc46 100644 --- a/packages/core/server/src/lock-manager.ts +++ b/packages/core/server/src/lock-manager.ts @@ -10,17 +10,13 @@ import { Registry } from '@nocobase/utils'; import { Mutex, withTimeout, MutexInterface, E_CANCELED } from 'async-mutex'; -export abstract class AbstractLockAdapter { - async connect() {} - abstract getLock(key: string, ttl: number): L | Promise; - abstract acquire(key: string, ttl: number): L | Promise; - abstract release(key: string): void | Promise; -} +export type Releaser = () => void | Promise; -export abstract class AbstractLock { - abstract acquire(): Promise; - abstract release(): Promise; - abstract runExclusive(fn: () => Promise): Promise; +export abstract class AbstractLockAdapter { + async connect() {} + async close() {} + abstract acquire(key: string, ttl: number): Releaser | Promise; + abstract runExclusive(key: string, fn: () => Promise, ttl: number): Promise; } export class LockAbortError extends Error { @@ -29,25 +25,38 @@ export class LockAbortError extends Error { } } -export class LocalLock extends AbstractLock { +export class LocalLock { private lock: MutexInterface; - constructor(ttl) { - super(); - this.lock = withTimeout(new Mutex(), ttl); + constructor(private ttl: number) { + this.lock = new Mutex(); + } + + setTTL(ttl: number) { + this.ttl = ttl; } async acquire() { - await this.lock.acquire(); - return this; - } - - async release() { - this.lock.release(); + const release = (await this.lock.acquire()) as Releaser; + const timer = setTimeout(() => { + if (this.lock.isLocked()) { + release(); + } + }, this.ttl); + return () => { + release(); + clearTimeout(timer); + }; } async runExclusive(fn: () => Promise): Promise { + let timer; try { + timer = setTimeout(() => { + if (this.lock.isLocked()) { + this.lock.release(); + } + }); return this.lock.runExclusive(fn); } catch (e) { if (e === E_CANCELED) { @@ -55,32 +64,34 @@ export class LocalLock extends AbstractLock { } else { throw e; } + } finally { + clearTimeout(timer); } } } -class LocalLockAdapter extends AbstractLockAdapter { +class LocalLockAdapter extends AbstractLockAdapter { private locks = new Map(); - getLock(key: string, ttl: number): LocalLock { + private getLock(key: string, ttl: number): LocalLock { let lock = this.locks.get(key); if (!lock) { lock = new LocalLock(ttl); this.locks.set(key, lock); + } else { + lock.setTTL(ttl); } return lock; } - async acquire(key: string, ttl?: number) { - const lockInstance = this.getLock(key, ttl); - return lockInstance.acquire(); + async acquire(key: string, ttl: number) { + const lock = this.getLock(key, ttl); + return lock.acquire(); } - async release(lockKey) { - if (this.locks.has(lockKey)) { - const lock = this.locks.get(lockKey); - await lock.release(); - } + async runExclusive(key: string, fn: () => Promise, ttl: number): Promise { + const lock = this.getLock(key, ttl); + return lock.runExclusive(fn); } } @@ -107,7 +118,7 @@ export class LockManager { this.registry.register(name, adapterConfig); } - async getLock(key: string, ttl = 500): Promise { + private async getClient(): Promise { const type = this.options.defaultAdapter || 'local'; let client = this.clients.get(type); if (!client) { @@ -122,6 +133,16 @@ export class LockManager { this.clients.set(type, client); } - return client.getLock(key, ttl); + return client; + } + + public async acquire(key: string, ttl = 500) { + const client = await this.getClient(); + return client.acquire(key, ttl); + } + + public async runExclusive(key: string, fn: () => Promise, ttl = 500): Promise { + const client = await this.getClient(); + return client.runExclusive(key, fn, ttl); } } From 387e16f3bf14a489f2fbfa79808a22b36df1c79b Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 14 Aug 2024 15:46:10 +0000 Subject: [PATCH 48/70] refactor(server): change variable names and use singleton for local lock --- .../server/src/__tests__/lock-manager.test.ts | 1 - packages/core/server/src/lock-manager.ts | 102 +++++++++--------- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/packages/core/server/src/__tests__/lock-manager.test.ts b/packages/core/server/src/__tests__/lock-manager.test.ts index a07cea07dd..4b6781d7ec 100644 --- a/packages/core/server/src/__tests__/lock-manager.test.ts +++ b/packages/core/server/src/__tests__/lock-manager.test.ts @@ -10,7 +10,6 @@ import { Mutex, withTimeout } from 'async-mutex'; import { Application } from '../application'; -import { LocalLock } from '../lock-manager'; function sleep(ms = 1000) { return new Promise((resolve) => { diff --git a/packages/core/server/src/lock-manager.ts b/packages/core/server/src/lock-manager.ts index 9cdf69cc46..af64533863 100644 --- a/packages/core/server/src/lock-manager.ts +++ b/packages/core/server/src/lock-manager.ts @@ -8,7 +8,7 @@ */ import { Registry } from '@nocobase/utils'; -import { Mutex, withTimeout, MutexInterface, E_CANCELED } from 'async-mutex'; +import { Mutex, tryAcquire, MutexInterface, E_CANCELED } from 'async-mutex'; export type Releaser = () => void | Promise; @@ -17,6 +17,7 @@ export abstract class AbstractLockAdapter { async close() {} abstract acquire(key: string, ttl: number): Releaser | Promise; abstract runExclusive(key: string, fn: () => Promise, ttl: number): Promise; + // abstract tryAcquire(key: string, ttl: number): Releaser | Promise; } export class LockAbortError extends Error { @@ -25,39 +26,42 @@ export class LockAbortError extends Error { } } -export class LocalLock { - private lock: MutexInterface; +class LocalLockAdapter extends AbstractLockAdapter { + static locks = new Map(); - constructor(private ttl: number) { - this.lock = new Mutex(); + private getLock(key: string): MutexInterface { + let lock = (this.constructor).locks.get(key); + if (!lock) { + lock = new Mutex(); + (this.constructor).locks.set(key, lock); + } + return lock; } - setTTL(ttl: number) { - this.ttl = ttl; - } - - async acquire() { - const release = (await this.lock.acquire()) as Releaser; + async acquire(key: string, ttl: number) { + const lock = this.getLock(key); + const release = (await lock.acquire()) as Releaser; const timer = setTimeout(() => { - if (this.lock.isLocked()) { + if (lock.isLocked()) { release(); } - }, this.ttl); + }, ttl); return () => { release(); clearTimeout(timer); }; } - async runExclusive(fn: () => Promise): Promise { + async runExclusive(key: string, fn: () => Promise, ttl: number): Promise { + const lock = this.getLock(key); let timer; try { timer = setTimeout(() => { - if (this.lock.isLocked()) { - this.lock.release(); + if (lock.isLocked()) { + lock.release(); } - }); - return this.lock.runExclusive(fn); + }, ttl); + return lock.runExclusive(fn); } catch (e) { if (e === E_CANCELED) { throw new LockAbortError('Lock aborted', { cause: E_CANCELED }); @@ -68,36 +72,15 @@ export class LocalLock { clearTimeout(timer); } } -} - -class LocalLockAdapter extends AbstractLockAdapter { - private locks = new Map(); - - private getLock(key: string, ttl: number): LocalLock { - let lock = this.locks.get(key); - if (!lock) { - lock = new LocalLock(ttl); - this.locks.set(key, lock); - } else { - lock.setTTL(ttl); - } - return lock; - } - - async acquire(key: string, ttl: number) { - const lock = this.getLock(key, ttl); - return lock.acquire(); - } - - async runExclusive(key: string, fn: () => Promise, ttl: number): Promise { - const lock = this.getLock(key, ttl); - return lock.runExclusive(fn); - } + // async tryAcquire(key: string, ttl: number) { + // const lock = this.getLock(key); + // return lock.tryAcquire(ttl); + // } } export interface LockAdapterConfig { - Client: new (...args: any[]) => C; - [key: string]: any; + Adapter: new (...args: any[]) => C; + options?: Record; } export interface LockManagerOptions { @@ -106,11 +89,11 @@ export interface LockManagerOptions { export class LockManager { private registry = new Registry(); - private clients = new Map(); + private adapters = new Map(); constructor(private options: LockManagerOptions = {}) { this.registry.register('local', { - Client: LocalLockAdapter, + Adapter: LocalLockAdapter, }); } @@ -118,31 +101,42 @@ export class LockManager { this.registry.register(name, adapterConfig); } - private async getClient(): Promise { + private async getAdapter(): Promise { const type = this.options.defaultAdapter || 'local'; - let client = this.clients.get(type); + let client = this.adapters.get(type); if (!client) { const adapter = this.registry.get(type); if (!adapter) { throw new Error(`Lock adapter "${type}" not registered`); } - const { Client, ...config } = adapter; - client = new Client(config); + const { Adapter, options } = adapter; + client = new Adapter(options); await client.connect(); - this.clients.set(type, client); + this.adapters.set(type, client); } return client; } + public async close() { + for (const client of this.adapters.values()) { + await client.close(); + } + } + public async acquire(key: string, ttl = 500) { - const client = await this.getClient(); + const client = await this.getAdapter(); return client.acquire(key, ttl); } public async runExclusive(key: string, fn: () => Promise, ttl = 500): Promise { - const client = await this.getClient(); + const client = await this.getAdapter(); return client.runExclusive(key, fn, ttl); } + + // public async tryAcquire(key: string, ttl = 500) { + // const client = await this.getAdapter(); + // return client.tryAcquire(key, ttl); + // } } From dcb22c8ce3f75d2cc637e87b93c63f1b7c4f0dea Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 15 Aug 2024 15:39:51 +0800 Subject: [PATCH 49/70] fix: lockManager.close --- packages/core/server/src/application.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index c3c7421d7d..03fc74ae9a 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -533,6 +533,10 @@ export class Application exten await this.telemetry.shutdown(); } + if (this.lockManager) { + await this.lockManager.close(); + } + this.closeLogger(); const oldDb = this.db; From c17dfd36b8071d63ef2bfb6c9f574162bac4b189 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 15 Aug 2024 08:26:55 +0000 Subject: [PATCH 50/70] refactor(server): adjust types --- packages/core/server/src/lock-manager.ts | 53 ++++++++++++++++++------ 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/core/server/src/lock-manager.ts b/packages/core/server/src/lock-manager.ts index af64533863..39f5a073bb 100644 --- a/packages/core/server/src/lock-manager.ts +++ b/packages/core/server/src/lock-manager.ts @@ -12,12 +12,17 @@ import { Mutex, tryAcquire, MutexInterface, E_CANCELED } from 'async-mutex'; export type Releaser = () => void | Promise; -export abstract class AbstractLockAdapter { - async connect() {} - async close() {} - abstract acquire(key: string, ttl: number): Releaser | Promise; - abstract runExclusive(key: string, fn: () => Promise, ttl: number): Promise; - // abstract tryAcquire(key: string, ttl: number): Releaser | Promise; +export interface ILock { + acquire(ttl: number): Releaser | Promise; + runExclusive(fn: () => Promise, ttl: number): Promise; +} + +export interface ILockAdapter { + connect(): Promise; + close(): Promise; + acquire(key: string, ttl: number): Releaser | Promise; + runExclusive(key: string, fn: () => Promise, ttl: number): Promise; + // tryAcquire(key: string, timeout?: number): Promise; } export class LockAbortError extends Error { @@ -26,9 +31,18 @@ export class LockAbortError extends Error { } } -class LocalLockAdapter extends AbstractLockAdapter { +export class LockAcquireError extends Error { + constructor(message, options) { + super(message, options); + } +} + +class LocalLockAdapter implements ILockAdapter { static locks = new Map(); + async connect() {} + async close() {} + private getLock(key: string): MutexInterface { let lock = (this.constructor).locks.get(key); if (!lock) { @@ -72,13 +86,26 @@ class LocalLockAdapter extends AbstractLockAdapter { clearTimeout(timer); } } - // async tryAcquire(key: string, ttl: number) { - // const lock = this.getLock(key); - // return lock.tryAcquire(ttl); + + // async tryAcquire(key: string) { + // try { + // const lock = this.getLock(key); + // await tryAcquire(lock); + // return { + // async acquire(ttl) { + // return this.acquire(key, ttl); + // }, + // async runExclusive(fn: () => Promise, ttl) { + // return this.runExclusive(key, fn, ttl); + // }, + // }; + // } catch (e) { + // throw new LockAcquireError('Lock acquire error', { cause: e }); + // } // } } -export interface LockAdapterConfig { +export interface LockAdapterConfig { Adapter: new (...args: any[]) => C; options?: Record; } @@ -89,7 +116,7 @@ export interface LockManagerOptions { export class LockManager { private registry = new Registry(); - private adapters = new Map(); + private adapters = new Map(); constructor(private options: LockManagerOptions = {}) { this.registry.register('local', { @@ -101,7 +128,7 @@ export class LockManager { this.registry.register(name, adapterConfig); } - private async getAdapter(): Promise { + private async getAdapter(): Promise { const type = this.options.defaultAdapter || 'local'; let client = this.adapters.get(type); if (!client) { From 483ea698b131e0ffe03c8954870d6ba15b94e5c8 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 15 Aug 2024 11:58:43 +0000 Subject: [PATCH 51/70] feat(server): add api --- .../server/src/__tests__/lock-manager.test.ts | 53 +++++++++++++------ packages/core/server/src/lock-manager.ts | 44 ++++++++------- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/packages/core/server/src/__tests__/lock-manager.test.ts b/packages/core/server/src/__tests__/lock-manager.test.ts index 4b6781d7ec..0fae1e2fb6 100644 --- a/packages/core/server/src/__tests__/lock-manager.test.ts +++ b/packages/core/server/src/__tests__/lock-manager.test.ts @@ -7,9 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Mutex, withTimeout } from 'async-mutex'; +import { Mutex, tryAcquire } from 'async-mutex'; import { Application } from '../application'; +import { LockAcquireError } from '../lock-manager'; function sleep(ms = 1000) { return new Promise((resolve) => { @@ -19,7 +20,7 @@ function sleep(ms = 1000) { describe('lock manager', () => { describe.skip('mutex example', () => { - it('mutex', async () => { + it('acquire and release', async () => { const order = []; const lock = new Mutex(); const release1 = await lock.acquire(); @@ -42,21 +43,18 @@ describe('lock manager', () => { expect(order).toEqual([1, 4, 2, 3, 5, 6]); }); - it.skip('with timeout', async () => { - const lock = withTimeout(new Mutex(), 200); - const r1 = await lock.acquire(); + it('tryAcquire', async () => { + const order = []; + const lock = new Mutex(); + const l1 = tryAcquire(lock); + expect(l1.isLocked()).toBe(false); + const release1 = await lock.acquire(); expect(lock.isLocked()).toBe(true); - const l2 = lock.acquire(); - await sleep(100); - expect(lock.isLocked()).toBe(true); - setTimeout(async () => { - expect(lock.isLocked()).toBe(false); - const r2 = await l2; - expect(lock.isLocked()).toBe(true); - await r2(); - expect(lock.isLocked()).toBe(false); - }, 150); - await sleep(300); + const l2 = tryAcquire(lock); + await expect(async () => { + const r2 = await l2.acquire(); + }).rejects.toThrow(); + await release1(); }); }); @@ -168,5 +166,28 @@ describe('lock manager', () => { await sleep(200); expect(order).toEqual([3, 4, 5, 1, 6, 2]); }); + + it('tryAcquire', async () => { + const release = await app.lockManager.acquire('test'); + await expect(app.lockManager.tryAcquire('test')).rejects.toThrowError(LockAcquireError); + await release(); + const lock = await app.lockManager.tryAcquire('test'); + expect(lock.acquire).toBeTypeOf('function'); + expect(lock.runExclusive).toBeTypeOf('function'); + + const order = []; + const r1 = await lock.acquire(200); + order.push(1); + setTimeout(async () => { + order.push(2); + await r1(); + order.push(3); + }, 100); + const r2 = await lock.acquire(200); + order.push(4); + await sleep(300); + await r2(); + expect(order).toEqual([1, 2, 3, 4]); + }); }); }); diff --git a/packages/core/server/src/lock-manager.ts b/packages/core/server/src/lock-manager.ts index 39f5a073bb..c29c92afaf 100644 --- a/packages/core/server/src/lock-manager.ts +++ b/packages/core/server/src/lock-manager.ts @@ -8,7 +8,7 @@ */ import { Registry } from '@nocobase/utils'; -import { Mutex, tryAcquire, MutexInterface, E_CANCELED } from 'async-mutex'; +import { Mutex, MutexInterface, E_CANCELED } from 'async-mutex'; export type Releaser = () => void | Promise; @@ -22,7 +22,7 @@ export interface ILockAdapter { close(): Promise; acquire(key: string, ttl: number): Releaser | Promise; runExclusive(key: string, fn: () => Promise, ttl: number): Promise; - // tryAcquire(key: string, timeout?: number): Promise; + tryAcquire(key: string, timeout?: number): Promise; } export class LockAbortError extends Error { @@ -32,7 +32,7 @@ export class LockAbortError extends Error { } export class LockAcquireError extends Error { - constructor(message, options) { + constructor(message, options?) { super(message, options); } } @@ -87,22 +87,20 @@ class LocalLockAdapter implements ILockAdapter { } } - // async tryAcquire(key: string) { - // try { - // const lock = this.getLock(key); - // await tryAcquire(lock); - // return { - // async acquire(ttl) { - // return this.acquire(key, ttl); - // }, - // async runExclusive(fn: () => Promise, ttl) { - // return this.runExclusive(key, fn, ttl); - // }, - // }; - // } catch (e) { - // throw new LockAcquireError('Lock acquire error', { cause: e }); - // } - // } + async tryAcquire(key: string) { + const lock = this.getLock(key); + if (lock.isLocked()) { + throw new LockAcquireError('lock is locked'); + } + return { + acquire: async (ttl) => { + return this.acquire(key, ttl); + }, + runExclusive: async (fn: () => Promise, ttl) => { + return this.runExclusive(key, fn, ttl); + }, + }; + } } export interface LockAdapterConfig { @@ -162,8 +160,8 @@ export class LockManager { return client.runExclusive(key, fn, ttl); } - // public async tryAcquire(key: string, ttl = 500) { - // const client = await this.getAdapter(); - // return client.tryAcquire(key, ttl); - // } + public async tryAcquire(key: string) { + const client = await this.getAdapter(); + return client.tryAcquire(key); + } } From 0935b898fb920bae6a0cfc92e4b942d5cf8d1707 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 15 Aug 2024 13:25:08 +0000 Subject: [PATCH 52/70] refactor(core): move lock-manager to independent package to be used in db --- packages/core/database/package.json | 2 +- packages/core/database/src/database.ts | 7 + .../core/database/src/fields/sort-field.ts | 5 +- packages/core/database/src/mock-database.ts | 5 + packages/core/lock-manager/LICENSE | 661 ++++++++++++++++++ packages/core/lock-manager/package.json | 12 + .../src/__tests__/lock-manager.test.ts | 50 +- packages/core/lock-manager/src/index.ts | 11 + .../src/lock-manager.ts | 2 + packages/core/server/package.json | 1 + packages/core/server/src/application.ts | 2 + 11 files changed, 716 insertions(+), 42 deletions(-) create mode 100644 packages/core/lock-manager/LICENSE create mode 100644 packages/core/lock-manager/package.json rename packages/core/{server => lock-manager}/src/__tests__/lock-manager.test.ts (75%) create mode 100644 packages/core/lock-manager/src/index.ts rename packages/core/{server => lock-manager}/src/lock-manager.ts (99%) diff --git a/packages/core/database/package.json b/packages/core/database/package.json index a79e2faf69..0216c205b4 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -6,9 +6,9 @@ "types": "./lib/index.d.ts", "license": "AGPL-3.0", "dependencies": { + "@nocobase/lock-manager": "1.4.0-alpha", "@nocobase/logger": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha", - "async-mutex": "^0.5.0", "chalk": "^4.1.1", "cron-parser": "4.4.0", "dayjs": "^1.11.8", diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index a3b813b96b..821e95708a 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -8,6 +8,7 @@ */ import { createConsoleLogger, createLogger, Logger, LoggerOptions } from '@nocobase/logger'; +import { LockManager } from '@nocobase/lock-manager'; import { applyMixins, AsyncEmitter } from '@nocobase/utils'; import chalk from 'chalk'; import merge from 'deepmerge'; @@ -106,6 +107,7 @@ export interface IDatabaseOptions extends Options { logger?: LoggerOptions | Logger; customHooks?: any; instanceId?: string; + lockManager?: LockManager; } export type DatabaseOptions = IDatabaseOptions; @@ -181,6 +183,7 @@ export class Database extends EventEmitter implements AsyncEmitter { modelHook: ModelHook; delayCollectionExtend = new Map(); logger: Logger; + lockManager: LockManager; interfaceManager = new InterfaceManager(this); collectionFactory: CollectionFactory = new CollectionFactory(this); @@ -199,6 +202,10 @@ export class Database extends EventEmitter implements AsyncEmitter { ...lodash.clone(options), }; + if (opts.lockManager) { + this.lockManager = opts.lockManager; + } + if (options.logger) { if (typeof options.logger['log'] === 'function') { this.logger = options.logger as Logger; diff --git a/packages/core/database/src/fields/sort-field.ts b/packages/core/database/src/fields/sort-field.ts index da4a77fd3e..d79e731a35 100644 --- a/packages/core/database/src/fields/sort-field.ts +++ b/packages/core/database/src/fields/sort-field.ts @@ -7,13 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Mutex } from 'async-mutex'; import { isNumber } from 'lodash'; import { DataTypes } from 'sequelize'; import { BaseColumnFieldOptions, Field } from './field'; -const sortFieldMutex = new Mutex(); - export class SortField extends Field { get dataType() { return DataTypes.BIGINT; @@ -36,7 +33,7 @@ export class SortField extends Field { } } - await sortFieldMutex.runExclusive(async () => { + await this.database.lockManager.runExclusive(this.context.collection.name, async () => { const max = await model.max(name, { ...options, where }); const newValue = (max || 0) + 1; instance.set(name, newValue); diff --git a/packages/core/database/src/mock-database.ts b/packages/core/database/src/mock-database.ts index 7b7a0c1dc3..97d4817c42 100644 --- a/packages/core/database/src/mock-database.ts +++ b/packages/core/database/src/mock-database.ts @@ -9,6 +9,7 @@ /* istanbul ignore file -- @preserve */ import { merge } from '@nocobase/utils'; +import { LockManager } from '@nocobase/lock-manager'; import { customAlphabet } from 'nanoid'; import fetch from 'node-fetch'; import path from 'path'; @@ -100,6 +101,10 @@ export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase { } } + if (!options.lockManager) { + dbOptions.lockManager = new LockManager(); + } + const db = new MockDatabase(dbOptions); return db; diff --git a/packages/core/lock-manager/LICENSE b/packages/core/lock-manager/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/core/lock-manager/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/core/lock-manager/package.json b/packages/core/lock-manager/package.json new file mode 100644 index 0000000000..de68254230 --- /dev/null +++ b/packages/core/lock-manager/package.json @@ -0,0 +1,12 @@ +{ + "name": "@nocobase/lock-manager", + "version": "1.3.0-alpha", + "main": "lib/index.js", + "license": "AGPL-3.0", + "dependencies": { + }, + "devDependencies": { + "@nocobase/utils": "1.3.0-alpha", + "async-mutex": "^0.5.0" + } +} diff --git a/packages/core/server/src/__tests__/lock-manager.test.ts b/packages/core/lock-manager/src/__tests__/lock-manager.test.ts similarity index 75% rename from packages/core/server/src/__tests__/lock-manager.test.ts rename to packages/core/lock-manager/src/__tests__/lock-manager.test.ts index 0fae1e2fb6..6aba291ddd 100644 --- a/packages/core/server/src/__tests__/lock-manager.test.ts +++ b/packages/core/lock-manager/src/__tests__/lock-manager.test.ts @@ -9,8 +9,7 @@ import { Mutex, tryAcquire } from 'async-mutex'; -import { Application } from '../application'; -import { LockAcquireError } from '../lock-manager'; +import { LockManager, LockAcquireError } from '../lock-manager'; function sleep(ms = 1000) { return new Promise((resolve) => { @@ -59,34 +58,11 @@ describe('lock manager', () => { }); describe('local lock', () => { - let app: Application; - - beforeEach(() => { - app = new Application({ - database: { - dialect: 'sqlite', - storage: ':memory:', - }, - resourcer: { - prefix: '/api', - }, - acl: false, - dataWrapping: false, - registerActions: false, - }); - }); - - afterEach(async () => { - return app.destroy(); - }); - - it('base api', async () => { - expect(app.lockManager).toBeDefined(); - }); + const lockManager = new LockManager(); it('acquire and release', async () => { const order = []; - const r1 = await app.lockManager.acquire('test'); + const r1 = await lockManager.acquire('test'); order.push(1); setTimeout(async () => { order.push(2); @@ -94,7 +70,7 @@ describe('lock manager', () => { order.push(3); }, 200); order.push(4); - const r2 = await app.lockManager.acquire('test'); + const r2 = await lockManager.acquire('test'); order.push(5); await r2(); order.push(6); @@ -103,7 +79,7 @@ describe('lock manager', () => { it('acquire and release with timeout', async () => { const order = []; - const r1 = await app.lockManager.acquire('test', 200); + const r1 = await lockManager.acquire('test', 200); order.push(1); setTimeout(async () => { order.push(2); @@ -111,7 +87,7 @@ describe('lock manager', () => { order.push(3); }, 400); order.push(4); - const r2 = await app.lockManager.acquire('test', 200); + const r2 = await lockManager.acquire('test', 200); order.push(5); await sleep(300); await r2(); @@ -122,14 +98,14 @@ describe('lock manager', () => { it('runExclusive', async () => { const order = []; setTimeout(async () => { - await app.lockManager.runExclusive('test', async () => { + await lockManager.runExclusive('test', async () => { order.push(1); await sleep(100); order.push(2); }); }, 100); order.push(3); - await app.lockManager.runExclusive('test', async () => { + await lockManager.runExclusive('test', async () => { order.push(4); await sleep(400); order.push(5); @@ -142,7 +118,7 @@ describe('lock manager', () => { it('runExclusive with timeout', async () => { const order = []; setTimeout(async () => { - await app.lockManager.runExclusive( + await lockManager.runExclusive( 'test', async () => { order.push(1); @@ -153,7 +129,7 @@ describe('lock manager', () => { ); }, 100); order.push(3); - await app.lockManager.runExclusive( + await lockManager.runExclusive( 'test', async () => { order.push(4); @@ -168,10 +144,10 @@ describe('lock manager', () => { }); it('tryAcquire', async () => { - const release = await app.lockManager.acquire('test'); - await expect(app.lockManager.tryAcquire('test')).rejects.toThrowError(LockAcquireError); + const release = await lockManager.acquire('test'); + await expect(lockManager.tryAcquire('test')).rejects.toThrowError(LockAcquireError); await release(); - const lock = await app.lockManager.tryAcquire('test'); + const lock = await lockManager.tryAcquire('test'); expect(lock.acquire).toBeTypeOf('function'); expect(lock.runExclusive).toBeTypeOf('function'); diff --git a/packages/core/lock-manager/src/index.ts b/packages/core/lock-manager/src/index.ts new file mode 100644 index 0000000000..8a27b87df1 --- /dev/null +++ b/packages/core/lock-manager/src/index.ts @@ -0,0 +1,11 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export * from './lock-manager'; +export { default } from './lock-manager'; diff --git a/packages/core/server/src/lock-manager.ts b/packages/core/lock-manager/src/lock-manager.ts similarity index 99% rename from packages/core/server/src/lock-manager.ts rename to packages/core/lock-manager/src/lock-manager.ts index c29c92afaf..de9d64d72a 100644 --- a/packages/core/server/src/lock-manager.ts +++ b/packages/core/lock-manager/src/lock-manager.ts @@ -165,3 +165,5 @@ export class LockManager { return client.tryAcquire(key); } } + +export default LockManager; diff --git a/packages/core/server/package.json b/packages/core/server/package.json index 79ac42b769..36a84bbd43 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -17,6 +17,7 @@ "@nocobase/data-source-manager": "1.4.0-alpha", "@nocobase/database": "1.4.0-alpha", "@nocobase/evaluators": "1.4.0-alpha", + "@nocobase/lock-manager": "1.4.0-alpha", "@nocobase/logger": "1.4.0-alpha", "@nocobase/resourcer": "1.4.0-alpha", "@nocobase/sdk": "1.4.0-alpha", diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 03fc74ae9a..b3cb156b06 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -25,6 +25,7 @@ import { import { ResourceOptions, Resourcer } from '@nocobase/resourcer'; import { Telemetry, TelemetryOptions } from '@nocobase/telemetry'; import { applyMixins, AsyncEmitter, importModule, Toposort, ToposortOptions } from '@nocobase/utils'; +import { LockManager, LockManagerOptions } from '@nocobase/lock-manager'; import { Command, CommandOptions, ParseOptions } from 'commander'; import { randomUUID } from 'crypto'; import glob from 'glob'; @@ -1239,6 +1240,7 @@ export class Application exten context: { app: this }, }, logger: this._logger.child({ module: 'database' }), + lockManager: this.lockManager, }); return db; } From c49c1704fd5267c95e9b90d80d9356bd63af75d7 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 15 Aug 2024 14:14:52 +0000 Subject: [PATCH 53/70] refactor(plugins): change to new lock manager to use locks --- .../plugins/@nocobase/plugin-acl/package.json | 1 - .../@nocobase/plugin-acl/src/server/server.ts | 6 ++-- .../plugin-action-export/package.json | 1 - .../src/server/actions/export-xlsx.ts | 31 +++++++++++++------ .../src/server/actions/import-xlsx.ts | 28 +++++++++++------ .../plugin-data-source-main/package.json | 1 - .../src/server/server.ts | 4 +-- 7 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-acl/package.json b/packages/plugins/@nocobase/plugin-acl/package.json index ed02957a8e..11c82472d1 100644 --- a/packages/plugins/@nocobase/plugin-acl/package.json +++ b/packages/plugins/@nocobase/plugin-acl/package.json @@ -14,7 +14,6 @@ ], "devDependencies": { "@types/jsonwebtoken": "^8.5.8", - "async-mutex": "^0.5.0", "jsonwebtoken": "^8.5.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/server.ts b/packages/plugins/@nocobase/plugin-acl/src/server/server.ts index 37936e0a9e..97d486e37e 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/server/server.ts @@ -11,7 +11,6 @@ import { Context, utils as actionUtils } from '@nocobase/actions'; import { Cache } from '@nocobase/cache'; import { Collection, RelationField, Transaction } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; -import { Mutex } from 'async-mutex'; import lodash from 'lodash'; import { resolve } from 'path'; import { availableActionResource } from './actions/available-actions'; @@ -275,10 +274,9 @@ export class PluginACLServer extends Plugin { } }); - const mutex = new Mutex(); - this.app.db.on('fields.afterDestroy', async (model, options) => { - await mutex.runExclusive(async () => { + const lockKey = `${this.name}:fields.afterDestroy:${model.get('collectionName')}:${model.get('name')}`; + await this.app.lockManager.runExclusive(lockKey, async () => { const collectionName = model.get('collectionName'); const fieldName = model.get('name'); diff --git a/packages/plugins/@nocobase/plugin-action-export/package.json b/packages/plugins/@nocobase/plugin-action-export/package.json index 85e9d7e1fc..db10d0cfd8 100644 --- a/packages/plugins/@nocobase/plugin-action-export/package.json +++ b/packages/plugins/@nocobase/plugin-action-export/package.json @@ -14,7 +14,6 @@ "@formily/react": "2.x", "@formily/shared": "2.x", "@types/node-xlsx": "^0.15.1", - "async-mutex": "^0.5.0", "file-saver": "^2.0.5", "node-xlsx": "^0.16.1", "react": "^18.2.0", diff --git a/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts b/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts index f37f94ea8a..81430e65aa 100644 --- a/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts +++ b/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts @@ -12,10 +12,9 @@ import { Repository } from '@nocobase/database'; import XlsxExporter from '../xlsx-exporter'; import XLSX from 'xlsx'; -import { Mutex } from 'async-mutex'; import { DataSource } from '@nocobase/data-source-manager'; - -const mutex = new Mutex(); +import PluginActionExportServer from '..'; +import { LockAcquireError } from '@nocobase/lock-manager'; async function exportXlsxAction(ctx: Context, next: Next) { const { title, filter, sort, fields, except } = ctx.action.params; @@ -53,15 +52,27 @@ async function exportXlsxAction(ctx: Context, next: Next) { } export async function exportXlsx(ctx: Context, next: Next) { - if (mutex.isLocked()) { - throw new Error( - ctx.t(`another export action is running, please try again later.`, { - ns: 'action-export', - }), - ); + const plugin = ctx.app.pm.get(PluginActionExportServer) as PluginActionExportServer; + const { collection } = ctx.getCurrentRepository(); + const dataSource = ctx.dataSource as DataSource; + const lockKey = `${plugin.name}:${dataSource.name}:${collection.name}`; + let lock; + try { + lock = ctx.app.lockManager.tryAcquire(lockKey); + } catch (error) { + if (error instanceof LockAcquireError) { + throw new Error( + ctx.t(`another export action is running, please try again later.`, { + ns: 'action-export', + }), + { + cause: error, + }, + ); + } } - const release = await mutex.acquire(); + const release = await lock.acquire(5000); try { await exportXlsxAction(ctx, next); diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts index 80ecd19407..28f40122fa 100644 --- a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts +++ b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts @@ -11,10 +11,9 @@ import { Context, Next } from '@nocobase/actions'; import { Repository } from '@nocobase/database'; import XLSX from 'xlsx'; import { XlsxImporter } from '../services/xlsx-importer'; -import { Mutex } from 'async-mutex'; import { DataSource } from '@nocobase/data-source-manager'; - -const mutex = new Mutex(); +import PluginActionImportServer from '..'; +import { LockAcquireError } from '@nocobase/lock-manager'; const IMPORT_LIMIT_COUNT = 2000; @@ -60,15 +59,24 @@ async function importXlsxAction(ctx: Context, next: Next) { } export async function importXlsx(ctx: Context, next: Next) { - if (mutex.isLocked()) { - throw new Error( - ctx.t(`another import action is running, please try again later.`, { - ns: 'action-import', - }), - ); + const plugin = ctx.app.pm.get(PluginActionImportServer) as PluginActionImportServer; + const { collection } = ctx.getCurrentRepository(); + const dataSource = ctx.dataSource as DataSource; + const lockKey = `${plugin.name}:${dataSource.name}:${collection.name}`; + let lock; + try { + lock = ctx.app.lockManager.tryAcquire(lockKey); + } catch (error) { + if (error instanceof LockAcquireError) { + throw new Error( + ctx.t(`another import action is running, please try again later.`, { + ns: 'action-import', + }), + ); + } } - const release = await mutex.acquire(); + const release = await lock.acquire(5000); try { await importXlsxAction(ctx, next); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/package.json b/packages/plugins/@nocobase/plugin-data-source-main/package.json index d02d57b39e..b781645d59 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/package.json +++ b/packages/plugins/@nocobase/plugin-data-source-main/package.json @@ -11,7 +11,6 @@ "license": "AGPL-3.0", "devDependencies": { "@hapi/topo": "^6.0.0", - "async-mutex": "^0.5.0", "toposort": "^2.0.2" }, "peerDependencies": { diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index acf95af08e..e2948e6c1c 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -315,9 +315,9 @@ export class PluginDataSourceMainServer extends Plugin { this.app.db.on('fields.beforeDestroy', beforeDestoryField(this.app.db)); this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db)); - const mutex = new Mutex(); this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => { - await mutex.runExclusive(async () => { + const lockKey = `${this.name}:fields.beforeDestroy:${model.get('collectionName')}:${model.get('name')}`; + await this.app.lockManager.runExclusive(lockKey, async () => { await model.remove(options); this.sendSyncMessage( From c1db4f2e0cf79da807d66021e764ad818f60f624 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Fri, 16 Aug 2024 03:22:54 +0000 Subject: [PATCH 54/70] fix(auth): fix test case --- packages/core/auth/package.json | 1 + packages/core/auth/src/__tests__/middleware.test.ts | 5 ++--- packages/core/lock-manager/src/lock-manager.ts | 2 +- packages/core/test/src/server/mock-server.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index af0e356596..66d63118eb 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -10,6 +10,7 @@ "@nocobase/cache": "1.4.0-alpha", "@nocobase/database": "1.4.0-alpha", "@nocobase/resourcer": "1.4.0-alpha", + "@nocobase/test": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" diff --git a/packages/core/auth/src/__tests__/middleware.test.ts b/packages/core/auth/src/__tests__/middleware.test.ts index 3f7333f07c..b1962a5cec 100644 --- a/packages/core/auth/src/__tests__/middleware.test.ts +++ b/packages/core/auth/src/__tests__/middleware.test.ts @@ -8,7 +8,7 @@ */ import { Database } from '@nocobase/database'; -import { MockServer, mockServer } from '@nocobase/test'; +import { MockServer, createMockServer } from '@nocobase/test'; import { vi } from 'vitest'; describe('middleware', () => { @@ -17,14 +17,13 @@ describe('middleware', () => { let agent; beforeEach(async () => { - app = mockServer({ + app = await createMockServer({ registerActions: true, acl: true, plugins: ['users', 'auth', 'acl', 'data-source-manager'], }); // app.plugin(ApiKeysPlugin); - await app.loadAndInstall({ clean: true }); db = app.db; agent = app.agent(); }); diff --git a/packages/core/lock-manager/src/lock-manager.ts b/packages/core/lock-manager/src/lock-manager.ts index de9d64d72a..a7cf844113 100644 --- a/packages/core/lock-manager/src/lock-manager.ts +++ b/packages/core/lock-manager/src/lock-manager.ts @@ -150,7 +150,7 @@ export class LockManager { } } - public async acquire(key: string, ttl = 500) { + public async acquire(key: string, ttl = 500): Promise { const client = await this.getAdapter(); return client.acquire(key, ttl); } diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 966504bc01..d5bf1978a2 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -325,7 +325,7 @@ export async function createMockCluster({ }; } -export async function createMockServer(options: MockServerOptions = {}) { +export async function createMockServer(options: MockServerOptions = {}): Promise { const { version, beforeInstall, skipInstall, skipStart, ...others } = options; const app: MockServer = mockServer(others); if (!skipInstall) { From ce353d7ba8e067f2f9cd1ac4b2d80ba90255250c Mon Sep 17 00:00:00 2001 From: Chareice Date: Fri, 16 Aug 2024 13:41:49 +0800 Subject: [PATCH 55/70] chore: ttl of sort field lock --- packages/core/database/src/fields/sort-field.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/core/database/src/fields/sort-field.ts b/packages/core/database/src/fields/sort-field.ts index d79e731a35..878a9d47e9 100644 --- a/packages/core/database/src/fields/sort-field.ts +++ b/packages/core/database/src/fields/sort-field.ts @@ -33,11 +33,17 @@ export class SortField extends Field { } } - await this.database.lockManager.runExclusive(this.context.collection.name, async () => { - const max = await model.max(name, { ...options, where }); - const newValue = (max || 0) + 1; - instance.set(name, newValue); - }); + await this.database.lockManager.runExclusive( + this.context.collection.name, + async () => { + const max = await model.max(name, { ...options, where }); + const newValue = (max || 0) + 1; + instance.set(name, newValue); + }, + { + ttl: 2000, + }, + ); }; onScopeChange = async (instance, options) => { From 9e8436ce19c27d47e90fed35c7cad582d9a77891 Mon Sep 17 00:00:00 2001 From: Chareice Date: Fri, 16 Aug 2024 14:26:38 +0800 Subject: [PATCH 56/70] fix: ttl --- packages/core/database/src/fields/sort-field.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/database/src/fields/sort-field.ts b/packages/core/database/src/fields/sort-field.ts index 878a9d47e9..35467d8cfa 100644 --- a/packages/core/database/src/fields/sort-field.ts +++ b/packages/core/database/src/fields/sort-field.ts @@ -40,9 +40,7 @@ export class SortField extends Field { const newValue = (max || 0) + 1; instance.set(name, newValue); }, - { - ttl: 2000, - }, + 2000, ); }; From ac0bae6be9c2d1b249103746154ff6593e95e9b4 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Fri, 16 Aug 2024 06:49:53 +0000 Subject: [PATCH 57/70] fix(plugins): revert lock usage back for some plugins --- .../plugin-action-export/package.json | 1 + .../src/server/actions/export-xlsx.ts | 31 ++++++------------- .../plugin-action-import/package.json | 1 + .../src/server/actions/import-xlsx.ts | 28 ++++++----------- 4 files changed, 22 insertions(+), 39 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-action-export/package.json b/packages/plugins/@nocobase/plugin-action-export/package.json index db10d0cfd8..85e9d7e1fc 100644 --- a/packages/plugins/@nocobase/plugin-action-export/package.json +++ b/packages/plugins/@nocobase/plugin-action-export/package.json @@ -14,6 +14,7 @@ "@formily/react": "2.x", "@formily/shared": "2.x", "@types/node-xlsx": "^0.15.1", + "async-mutex": "^0.5.0", "file-saver": "^2.0.5", "node-xlsx": "^0.16.1", "react": "^18.2.0", diff --git a/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts b/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts index 81430e65aa..f37f94ea8a 100644 --- a/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts +++ b/packages/plugins/@nocobase/plugin-action-export/src/server/actions/export-xlsx.ts @@ -12,9 +12,10 @@ import { Repository } from '@nocobase/database'; import XlsxExporter from '../xlsx-exporter'; import XLSX from 'xlsx'; +import { Mutex } from 'async-mutex'; import { DataSource } from '@nocobase/data-source-manager'; -import PluginActionExportServer from '..'; -import { LockAcquireError } from '@nocobase/lock-manager'; + +const mutex = new Mutex(); async function exportXlsxAction(ctx: Context, next: Next) { const { title, filter, sort, fields, except } = ctx.action.params; @@ -52,27 +53,15 @@ async function exportXlsxAction(ctx: Context, next: Next) { } export async function exportXlsx(ctx: Context, next: Next) { - const plugin = ctx.app.pm.get(PluginActionExportServer) as PluginActionExportServer; - const { collection } = ctx.getCurrentRepository(); - const dataSource = ctx.dataSource as DataSource; - const lockKey = `${plugin.name}:${dataSource.name}:${collection.name}`; - let lock; - try { - lock = ctx.app.lockManager.tryAcquire(lockKey); - } catch (error) { - if (error instanceof LockAcquireError) { - throw new Error( - ctx.t(`another export action is running, please try again later.`, { - ns: 'action-export', - }), - { - cause: error, - }, - ); - } + if (mutex.isLocked()) { + throw new Error( + ctx.t(`another export action is running, please try again later.`, { + ns: 'action-export', + }), + ); } - const release = await lock.acquire(5000); + const release = await mutex.acquire(); try { await exportXlsxAction(ctx, next); diff --git a/packages/plugins/@nocobase/plugin-action-import/package.json b/packages/plugins/@nocobase/plugin-action-import/package.json index eb27d8035e..31fa642e5e 100644 --- a/packages/plugins/@nocobase/plugin-action-import/package.json +++ b/packages/plugins/@nocobase/plugin-action-import/package.json @@ -18,6 +18,7 @@ "@koa/multer": "^3.0.2", "@types/node-xlsx": "^0.15.1", "antd": "5.x", + "async-mutex": "^0.5.0", "file-saver": "^2.0.5", "mathjs": "^10.6.0", "node-xlsx": "^0.16.1", diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts index 28f40122fa..2a3ab234b9 100644 --- a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts +++ b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts @@ -10,13 +10,14 @@ import { Context, Next } from '@nocobase/actions'; import { Repository } from '@nocobase/database'; import XLSX from 'xlsx'; +import { Mutex } from 'async-mutex'; import { XlsxImporter } from '../services/xlsx-importer'; import { DataSource } from '@nocobase/data-source-manager'; -import PluginActionImportServer from '..'; -import { LockAcquireError } from '@nocobase/lock-manager'; const IMPORT_LIMIT_COUNT = 2000; +const mutex = new Mutex(); + async function importXlsxAction(ctx: Context, next: Next) { let columns = (ctx.request.body as any).columns as any[]; if (typeof columns === 'string') { @@ -59,24 +60,15 @@ async function importXlsxAction(ctx: Context, next: Next) { } export async function importXlsx(ctx: Context, next: Next) { - const plugin = ctx.app.pm.get(PluginActionImportServer) as PluginActionImportServer; - const { collection } = ctx.getCurrentRepository(); - const dataSource = ctx.dataSource as DataSource; - const lockKey = `${plugin.name}:${dataSource.name}:${collection.name}`; - let lock; - try { - lock = ctx.app.lockManager.tryAcquire(lockKey); - } catch (error) { - if (error instanceof LockAcquireError) { - throw new Error( - ctx.t(`another import action is running, please try again later.`, { - ns: 'action-import', - }), - ); - } + if (mutex.isLocked()) { + throw new Error( + ctx.t(`another export action is running, please try again later.`, { + ns: 'action-export', + }), + ); } - const release = await lock.acquire(5000); + const release = await mutex.acquire(); try { await importXlsxAction(ctx, next); From 9b9e03ea19204633fe451b97ff6189632f00764f Mon Sep 17 00:00:00 2001 From: mytharcher Date: Tue, 20 Aug 2024 02:36:48 +0000 Subject: [PATCH 58/70] refactor(plugin-field-sort): move sort field to plugin --- .../auth/src/__tests__/middleware.test.ts | 2 +- .../collection-manager/collectionPlugin.ts | 2 - packages/core/database/src/database.ts | 7 - packages/core/database/src/fields/index.ts | 3 - packages/core/database/src/mock-database.ts | 4 - packages/core/server/src/application.ts | 1 - .../plugin-data-source-main/package.json | 1 + .../src/server/__tests__/index.ts | 2 +- .../@nocobase/plugin-field-sort/.npmignore | 2 + .../@nocobase/plugin-field-sort/README.md | 1 + .../@nocobase/plugin-field-sort/client.d.ts | 2 + .../@nocobase/plugin-field-sort/client.js | 1 + .../@nocobase/plugin-field-sort/package.json | 11 + .../@nocobase/plugin-field-sort/server.d.ts | 2 + .../@nocobase/plugin-field-sort/server.js | 1 + .../plugin-field-sort/src/client/client.d.ts | 249 ++++++++++++++++++ .../plugin-field-sort/src/client/index.tsx | 31 +++ .../plugin-field-sort/src/client/locale.ts | 21 ++ .../src/client/sort-interface.ts} | 4 +- .../@nocobase/plugin-field-sort/src/index.ts | 11 + .../plugin-field-sort/src/locale/en-US.json | 1 + .../plugin-field-sort/src/locale/zh-CN.json | 1 + .../src/server/__tests__/sort.test.ts} | 5 +- .../src/server/collections/.gitkeep | 0 .../plugin-field-sort/src/server/index.ts | 10 + .../plugin-field-sort/src/server/plugin.ts | 36 +++ .../src/server}/sort-field.ts | 7 +- .../server/__tests__/multiple-apps.test.ts | 2 +- .../server/__tests__/server-hook-impl.test.ts | 10 +- packages/presets/nocobase/package.json | 1 + packages/presets/nocobase/src/server/index.ts | 1 + ...0240818130314-move-sort-field-to-plugin.ts | 35 +++ 32 files changed, 438 insertions(+), 29 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-field-sort/.npmignore create mode 100644 packages/plugins/@nocobase/plugin-field-sort/README.md create mode 100644 packages/plugins/@nocobase/plugin-field-sort/client.d.ts create mode 100644 packages/plugins/@nocobase/plugin-field-sort/client.js create mode 100644 packages/plugins/@nocobase/plugin-field-sort/package.json create mode 100644 packages/plugins/@nocobase/plugin-field-sort/server.d.ts create mode 100644 packages/plugins/@nocobase/plugin-field-sort/server.js create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/client/client.d.ts create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/client/index.tsx create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/client/locale.ts rename packages/{core/client/src/collection-manager/interfaces/sort.ts => plugins/@nocobase/plugin-field-sort/src/client/sort-interface.ts} (93%) create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/index.ts create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/locale/en-US.json create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/locale/zh-CN.json rename packages/{core/database/src/__tests__/fields/sort-field.test.ts => plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts} (98%) create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/server/collections/.gitkeep create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts create mode 100644 packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts rename packages/{core/database/src/fields => plugins/@nocobase/plugin-field-sort/src/server}/sort-field.ts (96%) create mode 100644 packages/presets/nocobase/src/server/migrations/20240818130314-move-sort-field-to-plugin.ts diff --git a/packages/core/auth/src/__tests__/middleware.test.ts b/packages/core/auth/src/__tests__/middleware.test.ts index b1962a5cec..689a868dbb 100644 --- a/packages/core/auth/src/__tests__/middleware.test.ts +++ b/packages/core/auth/src/__tests__/middleware.test.ts @@ -20,7 +20,7 @@ describe('middleware', () => { app = await createMockServer({ registerActions: true, acl: true, - plugins: ['users', 'auth', 'acl', 'data-source-manager'], + plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager'], }); // app.plugin(ApiKeysPlugin); diff --git a/packages/core/client/src/collection-manager/collectionPlugin.ts b/packages/core/client/src/collection-manager/collectionPlugin.ts index 8d71266aaa..ae5d0140a7 100644 --- a/packages/core/client/src/collection-manager/collectionPlugin.ts +++ b/packages/core/client/src/collection-manager/collectionPlugin.ts @@ -48,7 +48,6 @@ import { UpdatedAtFieldInterface, UpdatedByFieldInterface, UrlFieldInterface, - SortFieldInterface, UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, @@ -169,7 +168,6 @@ export class CollectionPlugin extends Plugin { UpdatedAtFieldInterface, UpdatedByFieldInterface, UrlFieldInterface, - SortFieldInterface, UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 821e95708a..a3b813b96b 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -8,7 +8,6 @@ */ import { createConsoleLogger, createLogger, Logger, LoggerOptions } from '@nocobase/logger'; -import { LockManager } from '@nocobase/lock-manager'; import { applyMixins, AsyncEmitter } from '@nocobase/utils'; import chalk from 'chalk'; import merge from 'deepmerge'; @@ -107,7 +106,6 @@ export interface IDatabaseOptions extends Options { logger?: LoggerOptions | Logger; customHooks?: any; instanceId?: string; - lockManager?: LockManager; } export type DatabaseOptions = IDatabaseOptions; @@ -183,7 +181,6 @@ export class Database extends EventEmitter implements AsyncEmitter { modelHook: ModelHook; delayCollectionExtend = new Map(); logger: Logger; - lockManager: LockManager; interfaceManager = new InterfaceManager(this); collectionFactory: CollectionFactory = new CollectionFactory(this); @@ -202,10 +199,6 @@ export class Database extends EventEmitter implements AsyncEmitter { ...lodash.clone(options), }; - if (opts.lockManager) { - this.lockManager = opts.lockManager; - } - if (options.logger) { if (typeof options.logger['log'] === 'function') { this.logger = options.logger as Logger; diff --git a/packages/core/database/src/fields/index.ts b/packages/core/database/src/fields/index.ts index 610b6f1ad1..ec1b37d0b0 100644 --- a/packages/core/database/src/fields/index.ts +++ b/packages/core/database/src/fields/index.ts @@ -27,7 +27,6 @@ import { import { PasswordFieldOptions } from './password-field'; import { RadioFieldOptions } from './radio-field'; import { SetFieldOptions } from './set-field'; -import { SortFieldOptions } from './sort-field'; import { StringFieldOptions } from './string-field'; import { TextFieldOptions } from './text-field'; import { TimeFieldOptions } from './time-field'; @@ -52,7 +51,6 @@ export * from './password-field'; export * from './radio-field'; export * from './relation-field'; export * from './set-field'; -export * from './sort-field'; export * from './string-field'; export * from './text-field'; export * from './time-field'; @@ -74,7 +72,6 @@ export type FieldOptions = | JsonbFieldOptions | BooleanFieldOptions | RadioFieldOptions - | SortFieldOptions | TextFieldOptions | VirtualFieldOptions | ArrayFieldOptions diff --git a/packages/core/database/src/mock-database.ts b/packages/core/database/src/mock-database.ts index 97d4817c42..cd2fe0230a 100644 --- a/packages/core/database/src/mock-database.ts +++ b/packages/core/database/src/mock-database.ts @@ -101,10 +101,6 @@ export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase { } } - if (!options.lockManager) { - dbOptions.lockManager = new LockManager(); - } - const db = new MockDatabase(dbOptions); return db; diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index b3cb156b06..97e470117c 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -1240,7 +1240,6 @@ export class Application exten context: { app: this }, }, logger: this._logger.child({ module: 'database' }), - lockManager: this.lockManager, }); return db; } diff --git a/packages/plugins/@nocobase/plugin-data-source-main/package.json b/packages/plugins/@nocobase/plugin-data-source-main/package.json index b781645d59..4b471447f9 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/package.json +++ b/packages/plugins/@nocobase/plugin-data-source-main/package.json @@ -17,6 +17,7 @@ "@nocobase/client": "1.x", "@nocobase/database": "1.x", "@nocobase/plugin-error-handler": "1.x", + "@nocobase/plugin-field-sort": "1.x", "@nocobase/server": "1.x", "@nocobase/test": "1.x", "@nocobase/utils": "1.x" diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts index 92e2026ab4..8cd8450ded 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/index.ts @@ -13,7 +13,7 @@ export async function createApp(options: any = {}) { const app = await createMockServer({ acl: false, ...options, - plugins: ['error-handler', 'data-source-main', 'ui-schema-storage'], + plugins: ['error-handler', 'field-sort', 'data-source-main', 'ui-schema-storage'], }); return app; } diff --git a/packages/plugins/@nocobase/plugin-field-sort/.npmignore b/packages/plugins/@nocobase/plugin-field-sort/.npmignore new file mode 100644 index 0000000000..65f5e8779f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@nocobase/plugin-field-sort/README.md b/packages/plugins/@nocobase/plugin-field-sort/README.md new file mode 100644 index 0000000000..91fb36d7a3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/README.md @@ -0,0 +1 @@ +# @nocobase/plugin-field-sort diff --git a/packages/plugins/@nocobase/plugin-field-sort/client.d.ts b/packages/plugins/@nocobase/plugin-field-sort/client.d.ts new file mode 100644 index 0000000000..6c459cbac4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@nocobase/plugin-field-sort/client.js b/packages/plugins/@nocobase/plugin-field-sort/client.js new file mode 100644 index 0000000000..b6e3be70e6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@nocobase/plugin-field-sort/package.json b/packages/plugins/@nocobase/plugin-field-sort/package.json new file mode 100644 index 0000000000..e14c3e93dc --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/package.json @@ -0,0 +1,11 @@ +{ + "name": "@nocobase/plugin-field-sort", + "version": "1.3.0-alpha", + "main": "dist/server/index.js", + "dependencies": {}, + "peerDependencies": { + "@nocobase/client": "1.x", + "@nocobase/server": "1.x", + "@nocobase/test": "1.x" + } +} diff --git a/packages/plugins/@nocobase/plugin-field-sort/server.d.ts b/packages/plugins/@nocobase/plugin-field-sort/server.d.ts new file mode 100644 index 0000000000..c41081ddc6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@nocobase/plugin-field-sort/server.js b/packages/plugins/@nocobase/plugin-field-sort/server.js new file mode 100644 index 0000000000..972842039a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/client/client.d.ts b/packages/plugins/@nocobase/plugin-field-sort/src/client/client.d.ts new file mode 100644 index 0000000000..4e96f83fa1 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/client/client.d.ts @@ -0,0 +1,249 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +// CSS modules +type CSSModuleClasses = { readonly [key: string]: string }; + +declare module '*.module.css' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.scss' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.sass' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.less' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.styl' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.stylus' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.pcss' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.sss' { + const classes: CSSModuleClasses; + export default classes; +} + +// CSS +declare module '*.css' { } +declare module '*.scss' { } +declare module '*.sass' { } +declare module '*.less' { } +declare module '*.styl' { } +declare module '*.stylus' { } +declare module '*.pcss' { } +declare module '*.sss' { } + +// Built-in asset types +// see `src/node/constants.ts` + +// images +declare module '*.apng' { + const src: string; + export default src; +} +declare module '*.png' { + const src: string; + export default src; +} +declare module '*.jpg' { + const src: string; + export default src; +} +declare module '*.jpeg' { + const src: string; + export default src; +} +declare module '*.jfif' { + const src: string; + export default src; +} +declare module '*.pjpeg' { + const src: string; + export default src; +} +declare module '*.pjp' { + const src: string; + export default src; +} +declare module '*.gif' { + const src: string; + export default src; +} +declare module '*.svg' { + const src: string; + export default src; +} +declare module '*.ico' { + const src: string; + export default src; +} +declare module '*.webp' { + const src: string; + export default src; +} +declare module '*.avif' { + const src: string; + export default src; +} + +// media +declare module '*.mp4' { + const src: string; + export default src; +} +declare module '*.webm' { + const src: string; + export default src; +} +declare module '*.ogg' { + const src: string; + export default src; +} +declare module '*.mp3' { + const src: string; + export default src; +} +declare module '*.wav' { + const src: string; + export default src; +} +declare module '*.flac' { + const src: string; + export default src; +} +declare module '*.aac' { + const src: string; + export default src; +} +declare module '*.opus' { + const src: string; + export default src; +} +declare module '*.mov' { + const src: string; + export default src; +} +declare module '*.m4a' { + const src: string; + export default src; +} +declare module '*.vtt' { + const src: string; + export default src; +} + +// fonts +declare module '*.woff' { + const src: string; + export default src; +} +declare module '*.woff2' { + const src: string; + export default src; +} +declare module '*.eot' { + const src: string; + export default src; +} +declare module '*.ttf' { + const src: string; + export default src; +} +declare module '*.otf' { + const src: string; + export default src; +} + +// other +declare module '*.webmanifest' { + const src: string; + export default src; +} +declare module '*.pdf' { + const src: string; + export default src; +} +declare module '*.txt' { + const src: string; + export default src; +} + +// wasm?init +declare module '*.wasm?init' { + const initWasm: (options?: WebAssembly.Imports) => Promise; + export default initWasm; +} + +// web worker +declare module '*?worker' { + const workerConstructor: { + new(options?: { name?: string }): Worker; + }; + export default workerConstructor; +} + +declare module '*?worker&inline' { + const workerConstructor: { + new(options?: { name?: string }): Worker; + }; + export default workerConstructor; +} + +declare module '*?worker&url' { + const src: string; + export default src; +} + +declare module '*?sharedworker' { + const sharedWorkerConstructor: { + new(options?: { name?: string }): SharedWorker; + }; + export default sharedWorkerConstructor; +} + +declare module '*?sharedworker&inline' { + const sharedWorkerConstructor: { + new(options?: { name?: string }): SharedWorker; + }; + export default sharedWorkerConstructor; +} + +declare module '*?sharedworker&url' { + const src: string; + export default src; +} + +declare module '*?raw' { + const src: string; + export default src; +} + +declare module '*?url' { + const src: string; + export default src; +} + +declare module '*?inline' { + const src: string; + export default src; +} diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/client/index.tsx b/packages/plugins/@nocobase/plugin-field-sort/src/client/index.tsx new file mode 100644 index 0000000000..8dad0c3ecf --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/client/index.tsx @@ -0,0 +1,31 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Plugin } from '@nocobase/client'; +import { SortFieldInterface } from './sort-interface'; + +export class PluginFieldSortClient extends Plugin { + async afterAdd() { + // await this.app.pm.add() + } + + async beforeLoad() {} + + // You can get and modify the app instance here + async load() { + this.app.addFieldInterfaces([SortFieldInterface]); + // this.app.addComponents({}) + // this.app.addScopes({}) + // this.app.addProvider() + // this.app.addProviders() + // this.app.router.add() + } +} + +export default PluginFieldSortClient; diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/client/locale.ts b/packages/plugins/@nocobase/plugin-field-sort/src/client/locale.ts new file mode 100644 index 0000000000..84797b7d1b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/client/locale.ts @@ -0,0 +1,21 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +// @ts-ignore +import pkg from './../../package.json'; +import { useApp } from '@nocobase/client'; + +export function useT() { + const app = useApp(); + return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] }); +} + +export function tStr(key: string) { + return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`; +} diff --git a/packages/core/client/src/collection-manager/interfaces/sort.ts b/packages/plugins/@nocobase/plugin-field-sort/src/client/sort-interface.ts similarity index 93% rename from packages/core/client/src/collection-manager/interfaces/sort.ts rename to packages/plugins/@nocobase/plugin-field-sort/src/client/sort-interface.ts index 4acb62f036..845b6b24cf 100644 --- a/packages/core/client/src/collection-manager/interfaces/sort.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/client/sort-interface.ts @@ -7,9 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { i18n } from '../../i18n'; -import { defaultProps, operators } from './properties'; +import { CollectionFieldInterface, i18n, defaultProps, operators } from '@nocobase/client'; export class SortFieldInterface extends CollectionFieldInterface { name = 'sort'; type = 'object'; diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/index.ts b/packages/plugins/@nocobase/plugin-field-sort/src/index.ts new file mode 100644 index 0000000000..be99a2ff1a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/index.ts @@ -0,0 +1,11 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-field-sort/src/locale/en-US.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/locale/en-US.json @@ -0,0 +1 @@ +{} diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-field-sort/src/locale/zh-CN.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/locale/zh-CN.json @@ -0,0 +1 @@ +{} diff --git a/packages/core/database/src/__tests__/fields/sort-field.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts similarity index 98% rename from packages/core/database/src/__tests__/fields/sort-field.test.ts rename to packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts index f769d070d6..562eb13a38 100644 --- a/packages/core/database/src/__tests__/fields/sort-field.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts @@ -7,9 +7,8 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Database } from '../../database'; -import { mockDatabase } from '../'; -import { SortField } from '../../fields'; +import { Database, mockDatabase } from '@nocobase/database'; +import { SortField } from '../sort-field'; describe('string field', () => { let db: Database; diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-field-sort/src/server/collections/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts new file mode 100644 index 0000000000..be989de7c3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts @@ -0,0 +1,10 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export { default } from './plugin'; diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts new file mode 100644 index 0000000000..55e2421e4f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts @@ -0,0 +1,36 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Plugin } from '@nocobase/server'; +import { SortField } from './sort-field'; + +export class PluginFieldSortServer extends Plugin { + async afterAdd() {} + + async beforeLoad() { + const { lockManager } = this.app; + class SortFieldClass extends SortField {} + SortFieldClass.lockManager = lockManager; + this.app.db.registerFieldTypes({ + sort: SortFieldClass, + }); + } + + async load() {} + + async install() {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default PluginFieldSortServer; diff --git a/packages/core/database/src/fields/sort-field.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts similarity index 96% rename from packages/core/database/src/fields/sort-field.ts rename to packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts index 35467d8cfa..e9237685cd 100644 --- a/packages/core/database/src/fields/sort-field.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts @@ -9,9 +9,12 @@ import { isNumber } from 'lodash'; import { DataTypes } from 'sequelize'; -import { BaseColumnFieldOptions, Field } from './field'; +import { BaseColumnFieldOptions, Field } from '@nocobase/database'; +import { LockManager } from '@nocobase/lock-manager'; export class SortField extends Field { + static lockManager: LockManager; + get dataType() { return DataTypes.BIGINT; } @@ -33,7 +36,7 @@ export class SortField extends Field { } } - await this.database.lockManager.runExclusive( + await (this.constructor).lockManager.runExclusive( this.context.collection.name, async () => { const max = await model.max(name, { ...options, where }); diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts index 41931b8905..f4fd279045 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/multiple-apps.test.ts @@ -22,7 +22,7 @@ describe('multiple apps', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['nocobase', 'multi-app-manager'], + plugins: ['nocobase', 'field-sort', 'multi-app-manager'], }); db = app.db; }); diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts index 041edc4c33..8e375ca816 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts @@ -24,7 +24,15 @@ describe('server hooks', () => { beforeEach(async () => { app = await createMockServer({ registerActions: true, - plugins: ['ui-schema-storage', 'data-source-main', 'error-handler', 'users', 'acl', 'data-source-manager'], + plugins: [ + 'ui-schema-storage', + 'data-source-main', + 'field-sort', + 'error-handler', + 'users', + 'acl', + 'data-source-manager', + ], }); await app.runCommand('install', '-f'); db = app.db; diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 5b89f1b465..27d98412c7 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -35,6 +35,7 @@ "@nocobase/plugin-field-m2m-array": "1.4.0-alpha", "@nocobase/plugin-field-markdown-vditor": "1.4.0-alpha", "@nocobase/plugin-field-sequence": "1.4.0-alpha", + "@nocobase/plugin-field-sort": "1.4.0-alpha", "@nocobase/plugin-file-manager": "1.4.0-alpha", "@nocobase/plugin-gantt": "1.4.0-alpha", "@nocobase/plugin-graph-collection-manager": "1.4.0-alpha", diff --git a/packages/presets/nocobase/src/server/index.ts b/packages/presets/nocobase/src/server/index.ts index 44d8a948ed..f6d95eb90f 100644 --- a/packages/presets/nocobase/src/server/index.ts +++ b/packages/presets/nocobase/src/server/index.ts @@ -20,6 +20,7 @@ export class PresetNocoBase extends Plugin { 'file-manager', 'system-settings', 'field-sequence', + 'field-sort', 'verification', 'users', 'acl', diff --git a/packages/presets/nocobase/src/server/migrations/20240818130314-move-sort-field-to-plugin.ts b/packages/presets/nocobase/src/server/migrations/20240818130314-move-sort-field-to-plugin.ts new file mode 100644 index 0000000000..3f2976ceec --- /dev/null +++ b/packages/presets/nocobase/src/server/migrations/20240818130314-move-sort-field-to-plugin.ts @@ -0,0 +1,35 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Migration } from '@nocobase/server'; + +export default class extends Migration { + on = 'beforeLoad'; // 'beforeLoad' or 'afterLoad' + + async up() { + const existed = await this.pm.repository.findOne({ + filter: { + packageName: '@nocobase/plugin-field-sort', + }, + }); + + if (!existed) { + await this.pm.repository.create({ + values: { + name: 'field-sort', + packageName: '@nocobase/plugin-field-sort', + version: this.appVersion, + enabled: true, + installed: true, + builtIn: true, + }, + }); + } + } +} From 5580dc8fd1720bb45a42b19f4c02230489bfdd2d Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 14 Aug 2024 12:36:43 +0800 Subject: [PATCH 59/70] chore: update build ci --- .github/workflows/build-pro-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-pro-image.yml b/.github/workflows/build-pro-image.yml index 9a1b3204a7..d92870b0de 100644 --- a/.github/workflows/build-pro-image.yml +++ b/.github/workflows/build-pro-image.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v3 with: repository: nocobase/pro-plugins - ref: next + ref: main path: packages/pro-plugins fetch-depth: 0 ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} @@ -46,7 +46,7 @@ jobs: if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then git checkout ${{ github.event.pull_request.base.ref }} else - git checkout next + git checkout main fi fi - name: rm .git From 65b6fb7b03eb543887b8440f22c0578cce8d034f Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 21 Aug 2024 12:42:24 +0800 Subject: [PATCH 60/70] fix(server): fix build errors --- packages/core/actions/src/actions/move.ts | 19 ++++++++---- .../collection-manager/interfaces/index.ts | 1 - .../src/default-actions/move.ts | 29 ------------------- .../src/load-default-actions.ts | 3 -- packages/core/database/package.json | 1 - packages/core/database/src/mock-database.ts | 1 - packages/core/server/package.json | 1 - packages/core/server/src/application.ts | 5 ---- packages/core/server/src/index.ts | 1 - .../src/server/actions/import-xlsx.ts | 4 +-- .../@nocobase/plugin-field-sort/package.json | 7 ++++- .../plugin-field-sort/src/server/index.ts | 1 + 12 files changed, 23 insertions(+), 50 deletions(-) delete mode 100644 packages/core/data-source-manager/src/default-actions/move.ts diff --git a/packages/core/actions/src/actions/move.ts b/packages/core/actions/src/actions/move.ts index 9081b298c1..d94625bcd5 100644 --- a/packages/core/actions/src/actions/move.ts +++ b/packages/core/actions/src/actions/move.ts @@ -7,14 +7,23 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Model, Op } from 'sequelize'; +import { BelongsToManyRepository, Collection, HasManyRepository, TargetKey, Model, Op } from '@nocobase/database'; +import { Context } from '@nocobase/actions'; -import { BelongsToManyRepository, Collection, HasManyRepository, SortField, TargetKey } from '@nocobase/database'; -import { Context } from '..'; -import { getRepositoryFromParams } from '../utils'; +import { SortField } from './sort-field'; export async function move(ctx: Context, next) { - const repository = ctx.databaseRepository || getRepositoryFromParams(ctx); + const repository = ctx.getCurrentRepository(); + + if (repository.move) { + ctx.body = await repository.move(ctx.action.params); + return next(); + } + + if (repository.database) { + return ctx.throw(new Error(`Repository can not handle action move for ${ctx.action.resourceName}`)); + } + const { sourceId, targetId, targetScope, sticky, method } = ctx.action.params; let sortField = ctx.action.params.sortField; diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 6778d83413..7d45ec1b20 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -42,7 +42,6 @@ export * from './time'; export * from './updatedAt'; export * from './updatedBy'; export * from './url'; -export * from './sort'; export * from './uuid'; export * from './nanoid'; export * from './unixTimestamp'; diff --git a/packages/core/data-source-manager/src/default-actions/move.ts b/packages/core/data-source-manager/src/default-actions/move.ts deleted file mode 100644 index f8695abd70..0000000000 --- a/packages/core/data-source-manager/src/default-actions/move.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { Context } from '@nocobase/actions'; - -export function createMoveAction(databaseMoveAction) { - return async function move(ctx: Context, next) { - const repository = ctx.getCurrentRepository(); - - if (repository.move) { - ctx.body = await repository.move(ctx.action.params); - await next(); - return; - } - - if (repository.database) { - ctx.databaseRepository = repository; - return databaseMoveAction(ctx, next); - } - - throw new Error(`Repository can not handle action move for ${ctx.action.resourceName}`); - }; -} diff --git a/packages/core/data-source-manager/src/load-default-actions.ts b/packages/core/data-source-manager/src/load-default-actions.ts index a2d07dc1af..10b8b0aeec 100644 --- a/packages/core/data-source-manager/src/load-default-actions.ts +++ b/packages/core/data-source-manager/src/load-default-actions.ts @@ -8,9 +8,7 @@ */ import { list } from './default-actions/list'; -import { createMoveAction } from './default-actions/move'; import { proxyToRepository } from './default-actions/proxy-to-repository'; -import globalActions from '@nocobase/actions'; type Actions = { [key: string]: { params: Array | ((ctx: any) => Array); method: string } }; @@ -81,6 +79,5 @@ export function loadDefaultActions() { return carry; }, {}), list, - move: createMoveAction(globalActions.move), }; } diff --git a/packages/core/database/package.json b/packages/core/database/package.json index 0216c205b4..a3bd760c5e 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -6,7 +6,6 @@ "types": "./lib/index.d.ts", "license": "AGPL-3.0", "dependencies": { - "@nocobase/lock-manager": "1.4.0-alpha", "@nocobase/logger": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha", "chalk": "^4.1.1", diff --git a/packages/core/database/src/mock-database.ts b/packages/core/database/src/mock-database.ts index cd2fe0230a..7b7a0c1dc3 100644 --- a/packages/core/database/src/mock-database.ts +++ b/packages/core/database/src/mock-database.ts @@ -9,7 +9,6 @@ /* istanbul ignore file -- @preserve */ import { merge } from '@nocobase/utils'; -import { LockManager } from '@nocobase/lock-manager'; import { customAlphabet } from 'nanoid'; import fetch from 'node-fetch'; import path from 'path'; diff --git a/packages/core/server/package.json b/packages/core/server/package.json index 36a84bbd43..710a8aae83 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -27,7 +27,6 @@ "@types/ini": "^1.3.31", "@types/koa-send": "^4.1.3", "@types/multer": "^1.4.5", - "async-mutex": "^0.5.0", "axios": "^0.26.1", "chalk": "^4.1.1", "commander": "^9.2.0", diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 97e470117c..6e99688f4c 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -63,7 +63,6 @@ import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; import { SyncMessageManager } from './sync-message-manager'; -import { LockManager, LockManagerOptions } from './lock-manager'; export type PluginType = string | typeof Plugin; export type PluginConfiguration = PluginType | [PluginType, any]; @@ -534,10 +533,6 @@ export class Application exten await this.telemetry.shutdown(); } - if (this.lockManager) { - await this.lockManager.close(); - } - this.closeLogger(); const oldDb = this.db; diff --git a/packages/core/server/src/index.ts b/packages/core/server/src/index.ts index a78e1dfc59..e993b28c56 100644 --- a/packages/core/server/src/index.ts +++ b/packages/core/server/src/index.ts @@ -18,5 +18,4 @@ export * from './plugin-manager'; export * from './pub-sub-manager'; export * from './gateway'; export * from './app-supervisor'; -export * from './lock-manager'; export const OFFICIAL_PLUGIN_PREFIX = '@nocobase/plugin-'; diff --git a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts index 2a3ab234b9..09ad0e110d 100644 --- a/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts +++ b/packages/plugins/@nocobase/plugin-action-import/src/server/actions/import-xlsx.ts @@ -62,8 +62,8 @@ async function importXlsxAction(ctx: Context, next: Next) { export async function importXlsx(ctx: Context, next: Next) { if (mutex.isLocked()) { throw new Error( - ctx.t(`another export action is running, please try again later.`, { - ns: 'action-export', + ctx.t(`another import action is running, please try again later.`, { + ns: 'action-import', }), ); } diff --git a/packages/plugins/@nocobase/plugin-field-sort/package.json b/packages/plugins/@nocobase/plugin-field-sort/package.json index e14c3e93dc..78bde7f07a 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/package.json +++ b/packages/plugins/@nocobase/plugin-field-sort/package.json @@ -4,8 +4,13 @@ "main": "dist/server/index.js", "dependencies": {}, "peerDependencies": { + "@nocobase/actions": "1.x", "@nocobase/client": "1.x", + "@nocobase/database": "1.x", + "@nocobase/lock-manager": "1.x", "@nocobase/server": "1.x", - "@nocobase/test": "1.x" + "@nocobase/test": "1.x", + "lodash": "4.17.21", + "sequelize": "^6.26.0" } } diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts index be989de7c3..bde422fb27 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/index.ts @@ -8,3 +8,4 @@ */ export { default } from './plugin'; +export { move } from './action'; From 086364de84d37a04e271163ee33a868a36111406 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 21 Aug 2024 14:39:39 +0800 Subject: [PATCH 61/70] fix(plugin-field-sort): fix test case --- packages/core/actions/src/actions/move.ts | 2 +- .../src/__tests__/actions.test.ts | 56 ------------------- 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/packages/core/actions/src/actions/move.ts b/packages/core/actions/src/actions/move.ts index d94625bcd5..0df4884854 100644 --- a/packages/core/actions/src/actions/move.ts +++ b/packages/core/actions/src/actions/move.ts @@ -20,7 +20,7 @@ export async function move(ctx: Context, next) { return next(); } - if (repository.database) { + if (!repository.database) { return ctx.throw(new Error(`Repository can not handle action move for ${ctx.action.resourceName}`)); } diff --git a/packages/core/data-source-manager/src/__tests__/actions.test.ts b/packages/core/data-source-manager/src/__tests__/actions.test.ts index caf6fa0de9..c6f768efdd 100644 --- a/packages/core/data-source-manager/src/__tests__/actions.test.ts +++ b/packages/core/data-source-manager/src/__tests__/actions.test.ts @@ -9,7 +9,6 @@ import { list } from '../default-actions/list'; import { vi } from 'vitest'; -import { createMoveAction } from '../default-actions/move'; describe('action test', () => { describe('list action', async () => { @@ -100,59 +99,4 @@ describe('action test', () => { ]); }); }); - - describe('move action', async () => { - it('should call database move action', async () => { - const dbMove = vi.fn(); - const moveAction = createMoveAction(dbMove); - - const ctx: any = { - getCurrentRepository() { - return { - database: {}, - }; - }, - action: { - params: { - filterByTk: 1, - targetCollection: 'test', - }, - }, - }; - - await moveAction(ctx, () => {}); - - expect(dbMove).toHaveBeenCalled(); - }); - - it('should move when repository can move', async () => { - const moveAction = createMoveAction(() => {}); - - const ctx: any = { - getCurrentRepository() { - return {}; - }, - action: { - params: { - filterByTk: 1, - targetCollection: 'test', - }, - }, - }; - - const moveFn = vi.fn(); - - vi.spyOn(ctx, 'getCurrentRepository').mockImplementation(() => { - return { - move: async () => { - moveFn(); - }, - }; - }); - - await moveAction(ctx, () => {}); - - expect(moveFn).toHaveBeenCalled(); - }); - }); }); From fa15b49c0acf08b1b377727ae57621f6442d166f Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 21 Aug 2024 15:19:13 +0800 Subject: [PATCH 62/70] fix(plugin-field-sort): fix register move action --- .../@nocobase/plugin-field-sort/src/server/action.ts} | 0 .../@nocobase/plugin-field-sort/src/server/plugin.ts | 10 ++++++++++ 2 files changed, 10 insertions(+) rename packages/{core/actions/src/actions/move.ts => plugins/@nocobase/plugin-field-sort/src/server/action.ts} (100%) diff --git a/packages/core/actions/src/actions/move.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts similarity index 100% rename from packages/core/actions/src/actions/move.ts rename to packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts index 55e2421e4f..bd2fb66503 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts @@ -8,7 +8,10 @@ */ import { Plugin } from '@nocobase/server'; +import { DataSource } from '@nocobase/data-source-manager'; + import { SortField } from './sort-field'; +import { move } from './action'; export class PluginFieldSortServer extends Plugin { async afterAdd() {} @@ -20,6 +23,13 @@ export class PluginFieldSortServer extends Plugin { this.app.db.registerFieldTypes({ sort: SortFieldClass, }); + + this.app.dataSourceManager.beforeAddDataSource((dataSource: DataSource) => { + // @ts-ignore + if (dataSource.collectionManager.db) { + dataSource.resourceManager.registerActionHandlers({ move }); + } + }); } async load() {} From 2251742b8641028a1622e635b3730a098bf345e3 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 22 Aug 2024 17:42:03 +0800 Subject: [PATCH 63/70] fix(plugin-field-sort): fix load logic --- .../plugins/@nocobase/plugin-field-sort/src/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts index bd2fb66503..e61827c183 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/plugin.ts @@ -26,7 +26,7 @@ export class PluginFieldSortServer extends Plugin { this.app.dataSourceManager.beforeAddDataSource((dataSource: DataSource) => { // @ts-ignore - if (dataSource.collectionManager.db) { + if (dataSource.collectionManager?.db) { dataSource.resourceManager.registerActionHandlers({ move }); } }); From 15593b518561869d05b9c46e09ae95819f65ce1a Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 22 Aug 2024 21:30:52 +0800 Subject: [PATCH 64/70] fix(plugin-data-source-main): fix lock usage --- .../@nocobase/plugin-data-source-main/src/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index e2948e6c1c..9b77f75f52 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -316,7 +316,7 @@ export class PluginDataSourceMainServer extends Plugin { this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db)); this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => { - const lockKey = `${this.name}:fields.beforeDestroy:${model.get('collectionName')}:${model.get('name')}`; + const lockKey = `${this.name}:fields.beforeDestroy:${model.get('collectionName')}`; await this.app.lockManager.runExclusive(lockKey, async () => { await model.remove(options); From bce81bd4ca94d67cdade222d0150d6e073cabe20 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Thu, 22 Aug 2024 22:25:50 +0800 Subject: [PATCH 65/70] chore(plugin-data-source-main): remove unused import --- .../@nocobase/plugin-data-source-main/src/server/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index 9b77f75f52..98cf37984e 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -10,7 +10,6 @@ import { Filter, InheritedCollection, UniqueConstraintError } from '@nocobase/database'; import PluginErrorHandler from '@nocobase/plugin-error-handler'; import { Plugin } from '@nocobase/server'; -import { Mutex } from 'async-mutex'; import lodash from 'lodash'; import path from 'path'; import * as process from 'process'; From 845a6bd4476a2cc60acd4fff5902299d14c6a3ed Mon Sep 17 00:00:00 2001 From: Junyi Date: Thu, 22 Aug 2024 23:46:39 +0800 Subject: [PATCH 66/70] fix(server): fix import crypto in pub sub manager (#5111) --- packages/core/server/src/pub-sub-manager/handler-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/server/src/pub-sub-manager/handler-manager.ts b/packages/core/server/src/pub-sub-manager/handler-manager.ts index b2b972df83..9dfe801c5c 100644 --- a/packages/core/server/src/pub-sub-manager/handler-manager.ts +++ b/packages/core/server/src/pub-sub-manager/handler-manager.ts @@ -7,6 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import crypto from 'node:crypto'; import _ from 'lodash'; import { type PubSubManagerSubscribeOptions } from './types'; From d0dc0428fbac01bc9d749637f1e631df730f11b0 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Mon, 26 Aug 2024 21:46:13 +0800 Subject: [PATCH 67/70] fix(plugin-field-sort): fix build and test cases --- packages/core/actions/src/actions/index.ts | 1 - packages/core/auth/package.json | 1 - .../src/__tests__/collection.sortable.test.ts | 75 ------------------- packages/core/lock-manager/package.json | 4 +- packages/core/server/src/application.ts | 3 +- .../src/server/__tests__/prepare.ts | 1 + .../src/server/__tests__/role-user.test.ts | 2 +- .../src/server/__tests__/actions.test.ts | 2 +- .../src/server/__tests__/actions.test.ts | 2 +- .../src/server/__tests__/signin.test.ts | 2 +- .../src/server/__tests__/actions.test.ts | 4 +- .../src/server/__tests__/auth-model.test.ts | 2 +- .../src/server/__tests__/auth.test.ts | 2 +- .../server/__tests__/token-blacklist.test.ts | 2 +- .../src/server/__tests__/actions.test.ts | 2 +- .../src/server/__tests__/prepare.ts | 11 ++- .../src/server/__tests__/sync.test.ts | 4 +- .../src/server/__tests__/tree.test.ts | 3 +- .../src/server/__tests__/cluster.test.ts | 2 +- .../src/server/__tests__/through.test.ts | 4 +- .../src/server/__tests__/api.test.ts | 2 +- .../__tests__/external-data-source.test.ts | 2 +- .../src/server/__tests__/query.test.ts | 2 +- .../__tests__/belongs-to-array-field.test.ts | 2 +- .../__tests__/m2m-array-bigint-api.test.ts | 2 +- .../__tests__/m2m-array-string-api.test.ts | 2 +- .../@nocobase/plugin-field-sort/package.json | 2 +- .../src/server}/__tests__/move-action.test.ts | 74 +++++++++--------- .../server}/__tests__/sort-collection.test.ts | 74 ++++++++++++++++-- .../src/server/__tests__/sort.test.ts | 17 +++-- .../plugin-field-sort/src/server/action.ts | 10 +-- .../src/server/__tests__/index.ts | 2 +- .../src/server/__tests__/sync.test.ts | 1 + .../server/__tests__/mock-get-schema.test.ts | 6 +- .../src/server/__tests__/index.ts | 9 ++- .../server/__tests__/fieldsHistory.test.ts | 2 +- .../src/server/__tests__/snapshots.test.ts | 2 +- .../server/__tests__/server-hook-impl.test.ts | 1 + .../src/server/__tests__/server-hook.test.ts | 2 +- .../src/server/__tests__/actions.test.ts | 2 +- .../src/server/__tests__/fields.test.ts | 2 +- .../src/server/__tests__/model.test.ts | 2 +- .../src/server/__tests__/role-users.test.ts | 2 +- .../plugin-workflow-test/src/server/index.ts | 1 + 44 files changed, 175 insertions(+), 177 deletions(-) delete mode 100644 packages/core/database/src/__tests__/collection.sortable.test.ts rename packages/{core/actions/src => plugins/@nocobase/plugin-field-sort/src/server}/__tests__/move-action.test.ts (96%) rename packages/{core/actions/src => plugins/@nocobase/plugin-field-sort/src/server}/__tests__/sort-collection.test.ts (75%) diff --git a/packages/core/actions/src/actions/index.ts b/packages/core/actions/src/actions/index.ts index 054956ca58..4d52699e42 100644 --- a/packages/core/actions/src/actions/index.ts +++ b/packages/core/actions/src/actions/index.ts @@ -16,6 +16,5 @@ export * from './add'; export * from './set'; export * from './remove'; export * from './toggle'; -export * from './move'; export * from './first-or-create'; export * from './update-or-create'; diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index 66d63118eb..af0e356596 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -10,7 +10,6 @@ "@nocobase/cache": "1.4.0-alpha", "@nocobase/database": "1.4.0-alpha", "@nocobase/resourcer": "1.4.0-alpha", - "@nocobase/test": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" diff --git a/packages/core/database/src/__tests__/collection.sortable.test.ts b/packages/core/database/src/__tests__/collection.sortable.test.ts deleted file mode 100644 index 88feeff6fd..0000000000 --- a/packages/core/database/src/__tests__/collection.sortable.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { mockDatabase } from './index'; -import { Database } from '../database'; - -describe('collection sortable options', () => { - let db: Database; - - beforeEach(async () => { - db = mockDatabase(); - await db.clean({ drop: true }); - }); - - afterEach(async () => { - await db.close(); - }); - - test('sortable=true', async () => { - const Test = db.collection({ - name: 'test', - sortable: true, - }); - - const model = Test.model; - - await db.sync(); - const instance = await model.create(); - expect(model.rawAttributes['sort']).toBeDefined(); - expect(instance.get('sort')).toBe(1); - }); - - test('sortable=string', async () => { - const Test = db.collection({ - name: 'test', - sortable: 'order', - }); - - const model = Test.model; - - await db.sync(); - const instance = await model.create(); - expect(model.rawAttributes['order']).toBeDefined(); - expect(instance.get('order')).toBe(1); - }); - - test('sortable=object', async () => { - const Test = db.collection({ - name: 'test', - sortable: { - name: 'sort', - scopeKey: 'status', - }, - fields: [{ type: 'string', name: 'status' }], - }); - - await db.sync(); - - const t1 = await Test.model.create({ status: 'publish' }); - const t2 = await Test.model.create({ status: 'publish' }); - const t3 = await Test.model.create({ status: 'draft' }); - const t4 = await Test.model.create({ status: 'draft' }); - - expect(t1.get('sort')).toBe(1); - expect(t2.get('sort')).toBe(2); - expect(t3.get('sort')).toBe(1); - expect(t4.get('sort')).toBe(2); - }); -}); diff --git a/packages/core/lock-manager/package.json b/packages/core/lock-manager/package.json index de68254230..b3296dd5b0 100644 --- a/packages/core/lock-manager/package.json +++ b/packages/core/lock-manager/package.json @@ -1,12 +1,12 @@ { "name": "@nocobase/lock-manager", - "version": "1.3.0-alpha", + "version": "1.4.0-alpha", "main": "lib/index.js", "license": "AGPL-3.0", "dependencies": { }, "devDependencies": { - "@nocobase/utils": "1.3.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "async-mutex": "^0.5.0" } } diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 6e99688f4c..369716237e 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -37,7 +37,6 @@ import lodash from 'lodash'; import { RecordableHistogram } from 'node:perf_hooks'; import path, { basename, resolve } from 'path'; import semver from 'semver'; -import packageJson from '../package.json'; import { createACL } from './acl'; import { AppCommand } from './app-command'; import { AppSupervisor } from './app-supervisor'; @@ -64,6 +63,8 @@ import { InstallOptions, PluginManager } from './plugin-manager'; import { createPubSubManager, PubSubManager, PubSubManagerOptions } from './pub-sub-manager'; import { SyncMessageManager } from './sync-message-manager'; +import packageJson from '../package.json'; + export type PluginType = string | typeof Plugin; export type PluginConfiguration = PluginType | [PluginType, any]; diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/prepare.ts b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/prepare.ts index 406fd37556..349d26cc39 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/prepare.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/prepare.ts @@ -16,6 +16,7 @@ export async function prepareApp(): Promise { plugins: [ 'acl', 'error-handler', + 'field-sort', 'users', 'ui-schema-storage', 'data-source-main', diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts index 9a17205654..9d51ed6552 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/server/__tests__/role-user.test.ts @@ -20,7 +20,7 @@ describe('role', () => { beforeEach(async () => { api = await createMockServer({ - plugins: ['users', 'acl', 'auth', 'data-source-manager'], + plugins: ['field-sort', 'users', 'acl', 'auth', 'data-source-manager'], }); db = api.db; usersPlugin = api.getPlugin('users'); diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/server/__tests__/actions.test.ts index 72aeae12e1..4013434921 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/server/__tests__/actions.test.ts @@ -22,7 +22,7 @@ describe('actions', () => { app = await createMockServer({ registerActions: true, acl: true, - plugins: ['users', 'auth', 'acl', 'action-custom-request', 'data-source-manager'], + plugins: ['field-sort', 'users', 'auth', 'acl', 'action-custom-request', 'data-source-manager'], }); db = app.db; repo = db.getRepository('customRequests'); diff --git a/packages/plugins/@nocobase/plugin-api-keys/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-api-keys/src/server/__tests__/actions.test.ts index 0a7921b7e6..35125d745e 100644 --- a/packages/plugins/@nocobase/plugin-api-keys/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-api-keys/src/server/__tests__/actions.test.ts @@ -32,7 +32,7 @@ describe('actions', () => { app = await createMockServer({ registerActions: true, acl: true, - plugins: ['users', 'auth', 'api-keys', 'acl', 'data-source-manager'], + plugins: ['field-sort', 'users', 'auth', 'api-keys', 'acl', 'data-source-manager'], }); db = app.db; diff --git a/packages/plugins/@nocobase/plugin-auth-sms/src/server/__tests__/signin.test.ts b/packages/plugins/@nocobase/plugin-auth-sms/src/server/__tests__/signin.test.ts index b0895c8d3c..203050a25b 100644 --- a/packages/plugins/@nocobase/plugin-auth-sms/src/server/__tests__/signin.test.ts +++ b/packages/plugins/@nocobase/plugin-auth-sms/src/server/__tests__/signin.test.ts @@ -30,7 +30,7 @@ describe('signin', () => { beforeAll(async () => { app = await createMockServer({ - plugins: ['users', 'auth', 'verification', 'acl', 'auth-sms', 'data-source-manager'], + plugins: ['field-sort', 'users', 'auth', 'verification', 'acl', 'auth-sms', 'data-source-manager'], }); db = app.db; agent = app.agent(); diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts index c5f87ca261..46316e2aed 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts @@ -19,7 +19,7 @@ describe('actions', () => { beforeAll(async () => { app = await createMockServer({ - plugins: ['auth'], + plugins: ['field-sort', 'auth'], }); db = app.db; repo = db.getRepository('authenticators'); @@ -95,7 +95,7 @@ describe('actions', () => { process.env.INIT_ROOT_PASSWORD = '123456'; process.env.INIT_ROOT_NICKNAME = 'Test'; app = await createMockServer({ - plugins: ['auth', 'users'], + plugins: ['field-sort', 'auth', 'users'], }); db = app.db; agent = app.agent(); diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth-model.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth-model.test.ts index 134972ac90..757d52dc34 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth-model.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth-model.test.ts @@ -18,7 +18,7 @@ describe('AuthModel', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['auth', 'users'], + plugins: ['field-sort', 'auth', 'users'], }); db = app.db; repo = db.getRepository('authenticators'); diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth.test.ts index c911251c93..ce23374eea 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/auth.test.ts @@ -19,7 +19,7 @@ describe('auth', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['users', 'auth'], + plugins: ['field-sort', 'users', 'auth'], }); db = app.db; diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-blacklist.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-blacklist.test.ts index aa476d912f..3601b4ec86 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-blacklist.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-blacklist.test.ts @@ -19,7 +19,7 @@ describe('token-blacklist', () => { beforeAll(async () => { app = await createMockServer({ - plugins: ['auth'], + plugins: ['field-sort', 'auth'], }); db = app.db; repo = db.getRepository('tokenBlacklist'); diff --git a/packages/plugins/@nocobase/plugin-collection-sql/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-collection-sql/src/server/__tests__/actions.test.ts index 0685eae2ce..f61371f9ee 100644 --- a/packages/plugins/@nocobase/plugin-collection-sql/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-collection-sql/src/server/__tests__/actions.test.ts @@ -18,7 +18,7 @@ describe('sql collection', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['data-source-main', 'error-handler', 'collection-sql'], + plugins: ['field-sort', 'data-source-main', 'error-handler', 'collection-sql'], }); db = app.db; db.options.underscored = false; diff --git a/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/prepare.ts b/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/prepare.ts index 73cbb6baad..be6b114658 100644 --- a/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/prepare.ts +++ b/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/prepare.ts @@ -16,6 +16,7 @@ export async function prepareApp(): Promise { plugins: [ 'acl', 'error-handler', + 'field-sort', 'users', 'ui-schema-storage', 'data-source-main', @@ -32,6 +33,7 @@ export async function createApp(options: any = {}) { acl: false, ...options, plugins: [ + 'field-sort', 'data-source-main', 'users', 'collection-tree', @@ -47,7 +49,14 @@ export async function createAppWithNoUsersPlugin(options: any = {}) { const app = await createMockServer({ acl: false, ...options, - plugins: ['data-source-main', 'collection-tree', 'error-handler', 'data-source-manager', 'ui-schema-storage'], + plugins: [ + 'field-sort', + 'data-source-main', + 'collection-tree', + 'error-handler', + 'data-source-manager', + 'ui-schema-storage', + ], }); return app; } diff --git a/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/sync.test.ts b/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/sync.test.ts index 2a1aae5043..f35c67e2fb 100644 --- a/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/sync.test.ts +++ b/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/sync.test.ts @@ -18,7 +18,7 @@ describe('tree collection sync', async () => { beforeEach(async () => { app = await createMockServer({ version: '1.3.0-alpha', - plugins: ['data-source-main', 'data-source-manager', 'error-handler', 'collection-tree'], + plugins: ['field-sort', 'data-source-main', 'data-source-manager', 'error-handler', 'collection-tree'], }); db = app.db; }); @@ -61,7 +61,7 @@ describe('collection tree migrate test', () => { beforeEach(async () => { app = await createMockServer({ version: '1.3.0-alpha', - plugins: ['data-source-main', 'data-source-manager', 'error-handler', 'collection-tree'], + plugins: ['field-sort', 'data-source-main', 'data-source-manager', 'error-handler', 'collection-tree'], }); db = app.db; repo = app.db.getRepository('applicationPlugins'); diff --git a/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/tree.test.ts b/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/tree.test.ts index 94bb64d0c5..2bf4d3fdd4 100644 --- a/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/tree.test.ts +++ b/packages/plugins/@nocobase/plugin-collection-tree/src/server/__tests__/tree.test.ts @@ -42,7 +42,8 @@ describe('tree', () => { }); const userPlugin = app.getPlugin('users'); - const agent = app.agent().login(user).set('X-With-ACL-Meta', true); + const agent = app.agent().login(user); + agent.set('X-With-ACL-Meta', 'true'); app.acl.allow('table_a', ['*']); app.acl.allow('collections', ['*']); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts index ce64564fc2..d920c0aced 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/cluster.test.ts @@ -13,7 +13,7 @@ describe('cluster', () => { let cluster; beforeEach(async () => { cluster = await createMockCluster({ - plugins: ['error-handler', 'data-source-main', 'ui-schema-storage'], + plugins: ['error-handler', 'field-sort', 'data-source-main', 'ui-schema-storage'], acl: false, }); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/through.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/through.test.ts index 4a7bc55c51..9464de85fd 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/through.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/through.test.ts @@ -16,7 +16,7 @@ describe('collections repository', () => { tablePrefix: 'through_', }, acl: false, - plugins: ['error-handler', 'data-source-main'], + plugins: ['error-handler', 'field-sort', 'data-source-main'], }); await app1 @@ -121,7 +121,7 @@ describe('collections repository', () => { await app1.destroy(); const app2 = await startMockServer({ - plugins: ['error-handler', 'data-source-main'], + plugins: ['error-handler', 'field-sort', 'data-source-main'], database: { tablePrefix: 'through_', database: app1.db.options.database, diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/api.test.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/api.test.ts index 6ca72cdd16..5978dec1e1 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/api.test.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/api.test.ts @@ -20,7 +20,7 @@ describe('api', () => { beforeAll(async () => { app = await createMockServer({ acl: true, - plugins: ['users', 'auth', 'data-visualization'], + plugins: ['field-sort', 'users', 'auth', 'data-visualization'], }); db = app.db; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/external-data-source.test.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/external-data-source.test.ts index 32bd4d9bb9..64188cd2b2 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/external-data-source.test.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/external-data-source.test.ts @@ -19,7 +19,7 @@ describe('external data source', () => { beforeAll(async () => { process.env.INIT_ROOT_USERNAME = 'test'; app = await createMockServer({ - plugins: ['data-source-manager', 'users', 'acl'], + plugins: ['field-sort', 'data-source-manager', 'users', 'acl'], }); db = app.db; ctx = { diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/query.test.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/query.test.ts index 814aa29f69..0f38380655 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/query.test.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/server/__tests__/query.test.ts @@ -29,7 +29,7 @@ describe('query', () => { let db: Database; beforeAll(async () => { app = await createMockServer({ - plugins: ['data-source-manager', 'users', 'acl'], + plugins: ['field-sort', 'data-source-manager', 'users', 'acl'], }); db = app.db; db.options.underscored = true; diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/belongs-to-array-field.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/belongs-to-array-field.test.ts index 5d0f9feee1..2fa2c42f66 100644 --- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/belongs-to-array-field.test.ts +++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/belongs-to-array-field.test.ts @@ -17,7 +17,7 @@ describe('belongs to array field', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['field-m2m-array', 'data-source-manager', 'data-source-main', 'error-handler'], + plugins: ['field-m2m-array', 'data-source-manager', 'field-sort', 'data-source-main', 'error-handler'], }); db = app.db; fieldRepo = db.getRepository('fields'); diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts index 5eef17ccda..daae9ce8b6 100644 --- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts +++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-bigint-api.test.ts @@ -17,7 +17,7 @@ describe('m2m array api, bigInt targetKey', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['field-m2m-array', 'data-source-manager', 'data-source-main', 'error-handler'], + plugins: ['field-m2m-array', 'data-source-manager', 'field-sort', 'data-source-main', 'error-handler'], }); db = app.db; await db.getRepository('collections').create({ diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts index 76e4cc79d0..79bfecd02d 100644 --- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts +++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/__tests__/m2m-array-string-api.test.ts @@ -17,7 +17,7 @@ describe('m2m array api, string targetKey', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['field-m2m-array', 'data-source-manager', 'data-source-main', 'error-handler'], + plugins: ['field-m2m-array', 'data-source-manager', 'field-sort', 'data-source-main', 'error-handler'], }); db = app.db; await db.getRepository('collections').create({ diff --git a/packages/plugins/@nocobase/plugin-field-sort/package.json b/packages/plugins/@nocobase/plugin-field-sort/package.json index 78bde7f07a..29fd0cca19 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/package.json +++ b/packages/plugins/@nocobase/plugin-field-sort/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-field-sort", - "version": "1.3.0-alpha", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "dependencies": {}, "peerDependencies": { diff --git a/packages/core/actions/src/__tests__/move-action.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts similarity index 96% rename from packages/core/actions/src/__tests__/move-action.test.ts rename to packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts index d9c6cf916d..8c85622d07 100644 --- a/packages/core/actions/src/__tests__/move-action.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts @@ -7,22 +7,30 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { mockServer, MockServer } from './index'; -import { registerActions } from '@nocobase/actions'; +import { createMockServer, MockServer } from '@nocobase/test'; import { Collection, Database } from '@nocobase/database'; import { waitSecond } from '@nocobase/test'; -describe('sort action', () => { - describe('associations', () => { - let api: MockServer; +import Plugin from '..'; +describe('sort action', () => { + let api: MockServer; + let db: Database; + + beforeEach(async () => { + api = await createMockServer({ + plugins: [Plugin, 'data-source-main', 'error-handler'], + }); + }); + + afterEach(async () => { + return api.destroy(); + }); + + describe('associations', () => { let UserCollection: Collection; beforeEach(async () => { - api = mockServer(); - - registerActions(api); - UserCollection = api.db.collection({ name: 'users', fields: [ @@ -67,10 +75,6 @@ describe('sort action', () => { } }); - afterEach(async () => { - return api.destroy(); - }); - it('should not move association items when association not sortable', async () => { const u1 = await api.db.getRepository('users').findOne({ filter: { @@ -143,7 +147,7 @@ describe('sort action', () => { }); expect(u1Posts.body).toMatchObject({ - rows: [ + data: [ { title: 'u1p2', }, @@ -162,12 +166,7 @@ describe('sort action', () => { }); describe('same scope', () => { - let api: MockServer; - beforeEach(async () => { - api = mockServer(); - - registerActions(api); api.db.collection({ name: 'tests', fields: [ @@ -202,7 +201,7 @@ describe('sort action', () => { }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't2', sort: 1, @@ -237,7 +236,7 @@ describe('sort action', () => { }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't3', sort: 1, @@ -272,7 +271,7 @@ describe('sort action', () => { sort: ['sort2'], }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't2', sort2: 1, @@ -306,7 +305,7 @@ describe('sort action', () => { sort: ['sort'], }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't3', sort: 0, @@ -329,14 +328,9 @@ describe('sort action', () => { }); describe('different scope', () => { - let api: MockServer; - let db: Database; - beforeEach(async () => { - api = mockServer(); db = api.db; - registerActions(api); api.db.collection({ name: 'tests', fields: [ @@ -495,7 +489,7 @@ describe('sort action', () => { }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't12', sort: 2, @@ -520,7 +514,7 @@ describe('sort action', () => { }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't21', sort: 1, @@ -561,7 +555,7 @@ describe('sort action', () => { }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't12', sort: 2, @@ -584,7 +578,7 @@ describe('sort action', () => { filter: { state: 2 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't21', sort: 1, @@ -622,7 +616,7 @@ describe('sort action', () => { filter: { state: 1 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't11', sort: 1, @@ -653,7 +647,7 @@ describe('sort action', () => { filter: { state: 2 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't21', sort: 1, @@ -684,7 +678,7 @@ describe('sort action', () => { filter: { state: 1 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't11', sort: 1, @@ -715,7 +709,7 @@ describe('sort action', () => { filter: { state: 2 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't21', sort: 1, @@ -750,7 +744,7 @@ describe('sort action', () => { filter: { state: 1 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't12', sort: 2, @@ -773,7 +767,7 @@ describe('sort action', () => { filter: { state: 2 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't21', sort: 1, @@ -817,7 +811,7 @@ describe('sort action', () => { filter: { state: 1 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't12', }, @@ -837,7 +831,7 @@ describe('sort action', () => { filter: { state: 2 }, }); expect(response.body).toMatchObject({ - rows: [ + data: [ { title: 't11', }, diff --git a/packages/core/actions/src/__tests__/sort-collection.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts similarity index 75% rename from packages/core/actions/src/__tests__/sort-collection.test.ts rename to packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts index 2b329033c7..a89c7c9199 100644 --- a/packages/core/actions/src/__tests__/sort-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts @@ -7,16 +7,23 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { mockServer } from './index'; -import { SortAbleCollection } from '../actions'; import lodash from 'lodash'; +import Database from '@nocobase/database'; +import { createMockServer, MockServer } from '@nocobase/test'; +import { SortableCollection } from '../action'; +import Plugin from '..'; describe('sort collections', () => { - let app; + let app: MockServer; + let db: Database; let Post; beforeEach(async () => { - app = mockServer(); + app = await createMockServer({ + plugins: ['error-handler', Plugin, 'data-source-main'], + }); + + db = app.db; }); afterEach(async () => { @@ -50,6 +57,57 @@ describe('sort collections', () => { } }); + test('sortable=true', async () => { + const Test = db.collection({ + name: 'test', + sortable: true, + }); + + const model = Test.model; + + await db.sync(); + const instance = await model.create(); + expect(model.rawAttributes['sort']).toBeDefined(); + expect(instance.get('sort')).toBe(1); + }); + + test('sortable=string', async () => { + const Test = db.collection({ + name: 'test', + sortable: 'order', + }); + + const model = Test.model; + + await db.sync(); + const instance = await model.create(); + expect(model.rawAttributes['order']).toBeDefined(); + expect(instance.get('order')).toBe(1); + }); + + test('sortable=object', async () => { + const Test = db.collection({ + name: 'test', + sortable: { + name: 'sort', + scopeKey: 'status', + }, + fields: [{ type: 'string', name: 'status' }], + }); + + await db.sync(); + + const t1 = await Test.model.create({ status: 'publish' }); + const t2 = await Test.model.create({ status: 'publish' }); + const t3 = await Test.model.create({ status: 'draft' }); + const t4 = await Test.model.create({ status: 'draft' }); + + expect(t1.get('sort')).toBe(1); + expect(t2.get('sort')).toBe(2); + expect(t3.get('sort')).toBe(1); + expect(t4.get('sort')).toBe(2); + }); + test('forward insert', async () => { const t2 = await Post.repository.findOne({ filter: { @@ -62,7 +120,7 @@ describe('sort collections', () => { title: 't4', }, }); - const sortCollection = new SortAbleCollection(Post); + const sortCollection = new SortableCollection(Post); await sortCollection.move(t2.get('id'), t4.get('id')); @@ -95,7 +153,7 @@ describe('sort collections', () => { title: 't4', }, }); - const sortCollection = new SortAbleCollection(Post); + const sortCollection = new SortableCollection(Post); await sortCollection.move(t4.get('id'), t2.get('id')); @@ -173,7 +231,7 @@ describe('sort collections', () => { }, }); - const sortCollection = new SortAbleCollection(Post); + const sortCollection = new SortableCollection(Post); await sortCollection.move(s1t2.get('id'), s1t4.get('id')); const results = ( await Post.repository.find({ @@ -245,7 +303,7 @@ describe('sort collections', () => { }, }); - const sortCollection = new SortAbleCollection(Post); + const sortCollection = new SortableCollection(Post); await sortCollection.move(s1t1.get('id'), s2t3.get('id')); diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts index 562eb13a38..1e66e81f49 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts @@ -7,23 +7,24 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Database, mockDatabase } from '@nocobase/database'; -import { SortField } from '../sort-field'; +import { Database } from '@nocobase/database'; +import { createMockServer, MockServer } from '@nocobase/test'; + +import Plugin from '..'; describe('string field', () => { + let app: MockServer; let db: Database; beforeEach(async () => { - db = mockDatabase(); - await db.clean({ drop: true }); - - db.registerFieldTypes({ - sort: SortField, + app = await createMockServer({ + plugins: [Plugin, 'data-source-main', 'error-handler'], }); + db = app.db; }); afterEach(async () => { - await db.close(); + await app.destroy(); }); it('should init with camelCase scope key', async () => { diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts index 0df4884854..bebfcdb376 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts @@ -36,21 +36,21 @@ export async function move(ctx: Context, next) { sortField = `${repository.association.foreignKey}Sort`; } - const sortAbleCollection = new SortAbleCollection(repository.collection, sortField); + const sortableCollection = new SortableCollection(repository.collection, sortField); if (sourceId && targetId) { - await sortAbleCollection.move(sourceId, targetId, { + await sortableCollection.move(sourceId, targetId, { insertAfter: method === 'insertAfter', }); } // change scope if (sourceId && targetScope) { - await sortAbleCollection.changeScope(sourceId, targetScope, method); + await sortableCollection.changeScope(sourceId, targetScope, method); } if (sourceId && sticky) { - await sortAbleCollection.sticky(sourceId); + await sortableCollection.sticky(sourceId); } ctx.body = 'ok'; @@ -66,7 +66,7 @@ interface MoveOptions { insertAfter?: boolean; } -export class SortAbleCollection { +export class SortableCollection { collection: Collection; field: SortField; scopeKey: string; diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/index.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/index.ts index 4f5c3b3bb9..50d3c82073 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/index.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/index.ts @@ -18,7 +18,7 @@ export async function getApp(options = {}): Promise { cors: { origin: '*', }, - plugins: ['users', 'auth', 'file-manager'], + plugins: ['field-sort', 'users', 'auth', 'file-manager'], }); app.use(async (ctx, next) => { diff --git a/packages/plugins/@nocobase/plugin-localization/src/server/__tests__/sync.test.ts b/packages/plugins/@nocobase/plugin-localization/src/server/__tests__/sync.test.ts index 561ec4fe6b..1551e57a97 100644 --- a/packages/plugins/@nocobase/plugin-localization/src/server/__tests__/sync.test.ts +++ b/packages/plugins/@nocobase/plugin-localization/src/server/__tests__/sync.test.ts @@ -22,6 +22,7 @@ describe('sync', () => { app = await createMockServer({ plugins: [ // 'data-source-manager', + 'field-sort', 'data-source-main', 'localization', 'ui-schema-storage', diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/mock-get-schema.test.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/mock-get-schema.test.ts index f016f6c7d2..3fb6f39b91 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/mock-get-schema.test.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/server/__tests__/mock-get-schema.test.ts @@ -44,7 +44,7 @@ describe('test with start', () => { }; const app = await createMockServer({ - plugins: ['multi-app-manager'], + plugins: ['field-sort', 'multi-app-manager'], }); const db = app.db; @@ -74,7 +74,7 @@ describe('test with start', () => { it('should install into difference database', async () => { const app = await createMockServer({ - plugins: ['multi-app-manager'], + plugins: ['field-sort', 'multi-app-manager'], }); const db = app.db; @@ -85,7 +85,7 @@ describe('test with start', () => { values: { name, options: { - plugins: ['ui-schema-storage'], + plugins: ['field-sort', 'ui-schema-storage'], }, }, context: { diff --git a/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/__tests__/index.ts b/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/__tests__/index.ts index f965544dea..9a9352fce5 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/__tests__/index.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/server/__tests__/index.ts @@ -13,7 +13,14 @@ export async function createApp(options = {}) { const app = await createMockServer({ acl: false, ...options, - plugins: ['users', 'error-handler', 'data-source-main', 'multi-app-manager', 'multi-app-share-collection'], + plugins: [ + 'field-sort', + 'users', + 'error-handler', + 'data-source-main', + 'multi-app-manager', + 'multi-app-share-collection', + ], }); return app; diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/fieldsHistory.test.ts b/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/fieldsHistory.test.ts index 4cf166665b..6a7158ba1e 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/fieldsHistory.test.ts +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/fieldsHistory.test.ts @@ -16,7 +16,7 @@ describe('actions', () => { app = await createMockServer({ registerActions: true, acl: false, - plugins: ['error-handler', 'users', 'ui-schema-storage', 'data-source-main', 'snapshot-field'], + plugins: ['error-handler', 'field-sort', 'users', 'ui-schema-storage', 'data-source-main', 'snapshot-field'], }); }); diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/snapshots.test.ts b/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/snapshots.test.ts index d0e1407e63..48adce4f19 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/snapshots.test.ts +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/server/__tests__/snapshots.test.ts @@ -27,7 +27,7 @@ describe('actions', () => { app = await createMockServer({ registerActions: true, acl: false, - plugins: ['error-handler', 'users', 'ui-schema-storage', 'data-source-main', 'snapshot-field'], + plugins: ['error-handler', 'field-sort', 'users', 'ui-schema-storage', 'data-source-main', 'snapshot-field'], }); }); diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts index 8e375ca816..ded50478ee 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts @@ -26,6 +26,7 @@ describe('server hooks', () => { registerActions: true, plugins: [ 'ui-schema-storage', + 'field-sort', 'data-source-main', 'field-sort', 'error-handler', diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook.test.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook.test.ts index 1cf1b4adda..0ab94a035d 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook.test.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook.test.ts @@ -65,7 +65,7 @@ describe('server hooks', () => { beforeEach(async () => { app = await createMockServer({ registerActions: true, - plugins: ['ui-schema-storage', 'data-source-main', 'error-handler'], + plugins: ['ui-schema-storage', 'field-sort', 'data-source-main', 'error-handler'], }); db = app.db; diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts index 592a504947..831aac71f0 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts @@ -23,7 +23,7 @@ describe('actions', () => { process.env.INIT_ROOT_PASSWORD = '123456'; process.env.INIT_ROOT_NICKNAME = 'Test'; app = await createMockServer({ - plugins: ['auth', 'users', 'acl', 'data-source-manager'], + plugins: ['field-sort', 'auth', 'users', 'acl', 'data-source-manager'], }); db = app.db; diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts index 05e6b2d758..07dfcf08ed 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts @@ -18,7 +18,7 @@ describe('createdBy/updatedBy', () => { beforeEach(async () => { api = await createMockServer({ - plugins: ['acl', 'users', 'data-source-main', 'error-handler', 'data-source-manager'], + plugins: ['acl', 'field-sort', 'users', 'data-source-main', 'error-handler', 'data-source-manager'], }); db = api.db; diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/model.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/model.test.ts index 8883f9f8fb..dada1e54dc 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/model.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/model.test.ts @@ -17,7 +17,7 @@ describe('models', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['auth', 'users'], + plugins: ['field-sort', 'auth', 'users'], }); db = app.db; }); diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/role-users.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/role-users.test.ts index 5fdd261f02..fc4f9a08a3 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/role-users.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/role-users.test.ts @@ -18,7 +18,7 @@ describe('actions', () => { beforeAll(async () => { app = await createMockServer({ - plugins: ['acl', 'users', 'data-source-manager'], + plugins: ['acl', 'field-sort', 'users', 'data-source-manager'], }); db = app.db; repo = db.getRepository('users'); diff --git a/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts b/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts index 070b41c79d..786f1dc722 100644 --- a/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts @@ -43,6 +43,7 @@ export async function getApp({ const app = await createMockServer({ ...options, plugins: [ + 'field-sort', [ 'workflow', { From b83435eb2be2b05f9e0895b4b8ddf913b6989471 Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 28 Aug 2024 11:18:50 +0800 Subject: [PATCH 68/70] fix(plugin-user-data-sync): fix test with sort field --- .../src/server/__tests__/resource-manager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts index 8319865b93..4894818460 100644 --- a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts @@ -18,7 +18,7 @@ describe('user-data-resource-manager', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['user-data-sync'], + plugins: ['field-sort', 'user-data-sync'], }); db = app.db; resourceManager = new UserDataResourceManager(); From 54d51e3d23de0a4d9dbb35397764dc318b88422c Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 28 Aug 2024 12:21:59 +0800 Subject: [PATCH 69/70] fix(plugin-users): fix test with sort field --- .../plugin-user-data-sync/src/server/__tests__/api.test.ts | 2 +- .../plugin-users/src/server/__tests__/data-sync.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts index dd066ead86..341803db1a 100644 --- a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts @@ -19,7 +19,7 @@ describe('api', async () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['user-data-sync'], + plugins: ['field-sort', 'user-data-sync'], }); agent = app.agent(); const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer; diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts index 816925a7dd..60e29c1e45 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts @@ -18,7 +18,7 @@ describe('user data sync', () => { beforeEach(async () => { app = await createMockServer({ - plugins: ['user-data-sync', 'users'], + plugins: ['field-sort', 'user-data-sync', 'users'], }); db = app.db; const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer; From 5b426064ade563a4b6545ad8047c689ed31b095a Mon Sep 17 00:00:00 2001 From: mytharcher Date: Wed, 28 Aug 2024 15:13:40 +0800 Subject: [PATCH 70/70] fix(server): fix package import --- packages/core/server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/server/package.json b/packages/core/server/package.json index 710a8aae83..36a84bbd43 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -27,6 +27,7 @@ "@types/ini": "^1.3.31", "@types/koa-send": "^4.1.3", "@types/multer": "^1.4.5", + "async-mutex": "^0.5.0", "axios": "^0.26.1", "chalk": "^4.1.1", "commander": "^9.2.0",