@nocobase/server 1.8.0-beta.9 → 1.9.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,8 @@ import AesEncryptor from './aes-encryptor';
35
35
  import { AuditManager } from './audit-manager';
36
36
  import { Environment } from './environment';
37
37
  import { ServiceContainer } from './service-container';
38
+ import { EventQueue, EventQueueOptions } from './event-queue';
39
+ import { BackgroundJobManager, BackgroundJobManagerOptions } from './background-job-manager';
38
40
  export type PluginType = string | typeof Plugin;
39
41
  export type PluginConfiguration = PluginType | [PluginType, any];
40
42
  export interface ResourceManagerOptions {
@@ -55,6 +57,7 @@ export interface AppTelemetryOptions extends TelemetryOptions {
55
57
  enabled?: boolean;
56
58
  }
57
59
  export interface ApplicationOptions {
60
+ instanceId?: string;
58
61
  database?: IDatabaseOptions | Database;
59
62
  cacheManager?: CacheManagerOptions;
60
63
  /**
@@ -82,6 +85,8 @@ export interface ApplicationOptions {
82
85
  authManager?: AuthManagerOptions;
83
86
  auditManager?: AuditManager;
84
87
  lockManager?: LockManagerOptions;
88
+ eventQueue?: EventQueueOptions;
89
+ backgroundJobManager?: BackgroundJobManagerOptions;
85
90
  /**
86
91
  * @internal
87
92
  */
@@ -128,6 +133,7 @@ export type MaintainingCommandStatus = {
128
133
  };
129
134
  export declare class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa implements AsyncEmitter {
130
135
  options: ApplicationOptions;
136
+ readonly instanceId: string;
131
137
  /**
132
138
  * @internal
133
139
  */
@@ -174,6 +180,8 @@ export declare class Application<StateT = DefaultState, ContextT = DefaultContex
174
180
  private _actionCommand;
175
181
  container: ServiceContainer;
176
182
  lockManager: LockManager;
183
+ eventQueue: EventQueue;
184
+ backgroundJobManager: BackgroundJobManager;
177
185
  constructor(options: ApplicationOptions);
178
186
  private static staticCommands;
179
187
  static addCommand(callback: (app: Application) => void): void;
@@ -246,6 +254,11 @@ export declare class Application<StateT = DefaultState, ContextT = DefaultContex
246
254
  get dataSourceManager(): DataSourceManager;
247
255
  protected _aesEncryptor: AesEncryptor;
248
256
  get aesEncryptor(): AesEncryptor;
257
+ /**
258
+ * Check if the application is serving as a specific worker.
259
+ * @experimental
260
+ */
261
+ serving(key?: string): boolean;
249
262
  /**
250
263
  * @internal
251
264
  */
@@ -56,6 +56,7 @@ var import_glob = __toESM(require("glob"));
56
56
  var import_koa = __toESM(require("koa"));
57
57
  var import_koa_compose = __toESM(require("koa-compose"));
58
58
  var import_lodash = __toESM(require("lodash"));
59
+ var import_nanoid = require("nanoid");
59
60
  var import_path = __toESM(require("path"));
60
61
  var import_semver = __toESM(require("semver"));
61
62
  var import_acl = require("./acl");
@@ -81,10 +82,13 @@ var import_aes_encryptor = __toESM(require("./aes-encryptor"));
81
82
  var import_audit_manager = require("./audit-manager");
82
83
  var import_environment = require("./environment");
83
84
  var import_service_container = require("./service-container");
85
+ var import_event_queue = require("./event-queue");
86
+ var import_background_job_manager = require("./background-job-manager");
84
87
  const _Application = class _Application extends import_koa.default {
85
88
  constructor(options) {
86
89
  super();
87
90
  this.options = options;
91
+ this.instanceId = options.instanceId || (0, import_nanoid.nanoid)();
88
92
  this.context.reqId = (0, import_crypto.randomUUID)();
89
93
  this.rawOptions = this.name == "main" ? import_lodash.default.cloneDeep(options) : {};
90
94
  this.init();
@@ -92,6 +96,7 @@ const _Application = class _Application extends import_koa.default {
92
96
  this._appSupervisor.addApp(this);
93
97
  }
94
98
  }
99
+ instanceId;
95
100
  /**
96
101
  * @internal
97
102
  */
@@ -131,6 +136,8 @@ const _Application = class _Application extends import_koa.default {
131
136
  _actionCommand;
132
137
  container = new import_service_container.ServiceContainer();
133
138
  lockManager;
139
+ eventQueue;
140
+ backgroundJobManager;
134
141
  static addCommand(callback) {
135
142
  this.staticCommands.push(callback);
136
143
  }
@@ -263,6 +270,31 @@ const _Application = class _Application extends import_koa.default {
263
270
  get aesEncryptor() {
264
271
  return this._aesEncryptor;
265
272
  }
273
+ /**
274
+ * Check if the application is serving as a specific worker.
275
+ * @experimental
276
+ */
277
+ serving(key) {
278
+ const { WORKER_MODE = "" } = process.env;
279
+ if (!WORKER_MODE) {
280
+ return true;
281
+ }
282
+ const topics = WORKER_MODE.trim().split(",");
283
+ if (key) {
284
+ if (WORKER_MODE === "*") {
285
+ return true;
286
+ }
287
+ if (topics.includes(key)) {
288
+ return true;
289
+ }
290
+ return false;
291
+ } else {
292
+ if (topics.includes("!")) {
293
+ return true;
294
+ }
295
+ return false;
296
+ }
297
+ }
266
298
  /**
267
299
  * @internal
268
300
  */
@@ -855,6 +887,8 @@ const _Application = class _Application extends import_koa.default {
855
887
  this._i18n = (0, import_helper.createI18n)(options);
856
888
  this.pubSubManager = (0, import_pub_sub_manager.createPubSubManager)(this, options.pubSubManager);
857
889
  this.syncMessageManager = new import_sync_message_manager.SyncMessageManager(this, options.syncMessageManager);
890
+ this.eventQueue = new import_event_queue.EventQueue(this, options.eventQueue);
891
+ this.backgroundJobManager = new import_background_job_manager.BackgroundJobManager(this, options.backgroundJobManager);
858
892
  this.lockManager = new import_lock_manager.LockManager({
859
893
  defaultAdapter: process.env.LOCK_ADAPTER_DEFAULT,
860
894
  ...options.lockManager
@@ -0,0 +1,40 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import Application from './application';
10
+ import { QueueEventOptions, QueueMessageOptions } from './event-queue';
11
+ export interface BackgroundJobManagerOptions {
12
+ channel?: string;
13
+ }
14
+ type BackgroundJobEventOptions = Pick<QueueEventOptions, 'process' | 'idle'>;
15
+ declare class BackgroundJobManager {
16
+ private app;
17
+ private options;
18
+ static DEFAULT_CHANNEL: string;
19
+ private subscriptions;
20
+ private processing;
21
+ private get channel();
22
+ private onAfterStart;
23
+ private onBeforeStop;
24
+ private process;
25
+ constructor(app: Application, options?: BackgroundJobManagerOptions);
26
+ private get idle();
27
+ /**
28
+ * 订阅指定主题的任务处理器
29
+ * @param options 订阅选项
30
+ */
31
+ subscribe(topic: string, options: BackgroundJobEventOptions): void;
32
+ /**
33
+ * 取消订阅指定主题
34
+ * @param topic 主题名称
35
+ */
36
+ unsubscribe(topic: string): void;
37
+ publish(topic: string, payload: any, options?: QueueMessageOptions): Promise<void>;
38
+ }
39
+ export { BackgroundJobManager };
40
+ export default BackgroundJobManager;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") {
22
+ for (let key of __getOwnPropNames(from))
23
+ if (!__hasOwnProp.call(to, key) && key !== except)
24
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
25
+ }
26
+ return to;
27
+ };
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
30
+ var background_job_manager_exports = {};
31
+ __export(background_job_manager_exports, {
32
+ BackgroundJobManager: () => BackgroundJobManager,
33
+ default: () => background_job_manager_default
34
+ });
35
+ module.exports = __toCommonJS(background_job_manager_exports);
36
+ const _BackgroundJobManager = class _BackgroundJobManager {
37
+ constructor(app, options = {}) {
38
+ this.app = app;
39
+ this.options = options;
40
+ this.app.on("afterStart", this.onAfterStart);
41
+ this.app.on("beforeStop", this.onBeforeStop);
42
+ }
43
+ subscriptions = /* @__PURE__ */ new Map();
44
+ // topic -> handler
45
+ processing = null;
46
+ get channel() {
47
+ return this.options.channel ?? _BackgroundJobManager.DEFAULT_CHANNEL;
48
+ }
49
+ onAfterStart = /* @__PURE__ */ __name(() => {
50
+ this.app.eventQueue.subscribe(this.channel, {
51
+ idle: /* @__PURE__ */ __name(() => this.idle, "idle"),
52
+ process: this.process
53
+ });
54
+ }, "onAfterStart");
55
+ onBeforeStop = /* @__PURE__ */ __name(() => {
56
+ this.app.eventQueue.unsubscribe(this.channel);
57
+ }, "onBeforeStop");
58
+ process = /* @__PURE__ */ __name(async ({ topic, payload }, options) => {
59
+ const event = this.subscriptions.get(topic);
60
+ if (!event) {
61
+ this.app.logger.warn(`No handler found for topic: ${topic}, event skipped.`);
62
+ return;
63
+ }
64
+ this.processing = event.process(payload, options);
65
+ try {
66
+ await this.processing;
67
+ this.app.logger.debug(`Completed background job ${topic}:${options.id}`);
68
+ } catch (error) {
69
+ this.app.logger.error(`Failed to process background job ${topic}:${options.id}`, error);
70
+ throw error;
71
+ } finally {
72
+ this.processing = null;
73
+ }
74
+ }, "process");
75
+ get idle() {
76
+ return !this.processing && [...this.subscriptions.values()].every((event) => event.idle());
77
+ }
78
+ /**
79
+ * 订阅指定主题的任务处理器
80
+ * @param options 订阅选项
81
+ */
82
+ subscribe(topic, options) {
83
+ if (this.subscriptions.has(topic)) {
84
+ this.app.logger.warn(`Topic "${topic}" already has a handler, skip...`);
85
+ return;
86
+ }
87
+ this.subscriptions.set(topic, options);
88
+ this.app.logger.debug(`Subscribed to background job topic: ${topic}`);
89
+ }
90
+ /**
91
+ * 取消订阅指定主题
92
+ * @param topic 主题名称
93
+ */
94
+ unsubscribe(topic) {
95
+ if (this.subscriptions.has(topic)) {
96
+ this.subscriptions.delete(topic);
97
+ this.app.logger.debug(`Unsubscribed from background job topic: ${topic}`);
98
+ }
99
+ }
100
+ async publish(topic, payload, options) {
101
+ await this.app.eventQueue.publish(this.channel, { topic, payload }, options);
102
+ }
103
+ };
104
+ __name(_BackgroundJobManager, "BackgroundJobManager");
105
+ __publicField(_BackgroundJobManager, "DEFAULT_CHANNEL", "background-jobs");
106
+ let BackgroundJobManager = _BackgroundJobManager;
107
+ var background_job_manager_default = BackgroundJobManager;
108
+ // Annotate the CommonJS export names for ESM import in node:
109
+ 0 && (module.exports = {
110
+ BackgroundJobManager
111
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import Application from './application';
10
+ export declare const QUEUE_DEFAULT_INTERVAL = 250;
11
+ export declare const QUEUE_DEFAULT_CONCURRENCY = 1;
12
+ export declare const QUEUE_DEFAULT_ACK_TIMEOUT = 15000;
13
+ export type QueueCallbackOptions = {
14
+ id?: string;
15
+ retried?: number;
16
+ signal?: AbortSignal;
17
+ };
18
+ export type QueueCallback = (message: any, options: QueueCallbackOptions) => Promise<void> | void;
19
+ export type QueueEventOptions = {
20
+ /**
21
+ * @experimental
22
+ */
23
+ interval?: number;
24
+ concurrency?: number;
25
+ idle(): boolean;
26
+ process: QueueCallback;
27
+ };
28
+ export type QueueMessageOptions = {
29
+ timeout?: number;
30
+ maxRetries?: number;
31
+ retried?: number;
32
+ timestamp?: number;
33
+ };
34
+ export interface IEventQueueAdapter {
35
+ isConnected(): boolean;
36
+ connect(): Promise<void> | void;
37
+ close(): Promise<void> | void;
38
+ subscribe(channel: string, event: QueueEventOptions): void;
39
+ unsubscribe(channel: string): void;
40
+ publish(channel: string, message: any, options: QueueMessageOptions): Promise<void> | void;
41
+ }
42
+ export interface EventQueueOptions {
43
+ channelPrefix?: string;
44
+ }
45
+ export declare class MemoryEventQueueAdapter implements IEventQueueAdapter {
46
+ private options;
47
+ private connected;
48
+ private emitter;
49
+ private reading;
50
+ protected events: Map<string, QueueEventOptions>;
51
+ protected queues: Map<string, {
52
+ id: string;
53
+ content: any;
54
+ options?: QueueMessageOptions;
55
+ }[]>;
56
+ get processing(): Promise<void[]>;
57
+ private get storagePath();
58
+ listen: (channel: string) => Promise<void>;
59
+ constructor(options: {
60
+ appName: string;
61
+ });
62
+ isConnected(): boolean;
63
+ setConnected(connected: boolean): void;
64
+ loadFromStorage(): Promise<void>;
65
+ private saveToStorage;
66
+ connect(): Promise<void>;
67
+ close(): Promise<void>;
68
+ subscribe(channel: string, options: QueueEventOptions): void;
69
+ unsubscribe(channel: string): void;
70
+ publish(channel: string, content: any, options?: QueueMessageOptions): void;
71
+ consume(channel: string, once?: boolean): Promise<void>;
72
+ read(channel: string): Promise<void>;
73
+ process(channel: string, { id, message }: {
74
+ id: any;
75
+ message: any;
76
+ }): Promise<void>;
77
+ }
78
+ export declare class EventQueue {
79
+ protected app: Application;
80
+ protected options: EventQueueOptions;
81
+ protected adapter: IEventQueueAdapter;
82
+ protected events: Map<string, QueueEventOptions>;
83
+ get channelPrefix(): string;
84
+ constructor(app: Application, options?: EventQueueOptions);
85
+ getFullChannel(channel: string): string;
86
+ setAdapter<A extends IEventQueueAdapter>(adapter: A): void;
87
+ isConnected(): boolean;
88
+ connect(): Promise<void>;
89
+ close(): Promise<void>;
90
+ subscribe(channel: string, options: QueueEventOptions): void;
91
+ unsubscribe(channel: string): void;
92
+ publish(channel: string, message: any, options?: QueueMessageOptions): Promise<void>;
93
+ }
94
+ export default EventQueue;
@@ -0,0 +1,350 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
+ var event_queue_exports = {};
39
+ __export(event_queue_exports, {
40
+ EventQueue: () => EventQueue,
41
+ MemoryEventQueueAdapter: () => MemoryEventQueueAdapter,
42
+ QUEUE_DEFAULT_ACK_TIMEOUT: () => QUEUE_DEFAULT_ACK_TIMEOUT,
43
+ QUEUE_DEFAULT_CONCURRENCY: () => QUEUE_DEFAULT_CONCURRENCY,
44
+ QUEUE_DEFAULT_INTERVAL: () => QUEUE_DEFAULT_INTERVAL,
45
+ default: () => event_queue_default
46
+ });
47
+ module.exports = __toCommonJS(event_queue_exports);
48
+ var import_crypto = require("crypto");
49
+ var import_events = require("events");
50
+ var import_path = __toESM(require("path"));
51
+ var import_promises = __toESM(require("fs/promises"));
52
+ var import_utils = require("@nocobase/utils");
53
+ const QUEUE_DEFAULT_INTERVAL = 250;
54
+ const QUEUE_DEFAULT_CONCURRENCY = 1;
55
+ const QUEUE_DEFAULT_ACK_TIMEOUT = 15e3;
56
+ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
57
+ constructor(options) {
58
+ this.options = options;
59
+ this.emitter.setMaxListeners(0);
60
+ }
61
+ connected = false;
62
+ emitter = new import_events.EventEmitter();
63
+ reading = /* @__PURE__ */ new Map();
64
+ events = /* @__PURE__ */ new Map();
65
+ queues = /* @__PURE__ */ new Map();
66
+ get processing() {
67
+ const processing = Array.from(this.reading.values());
68
+ if (processing.length > 0) {
69
+ return Promise.all(processing);
70
+ }
71
+ return null;
72
+ }
73
+ get storagePath() {
74
+ return import_path.default.resolve(process.cwd(), "storage", "apps", this.options.appName, "event-queue.json");
75
+ }
76
+ listen = /* @__PURE__ */ __name(async (channel) => {
77
+ if (!this.connected) {
78
+ return;
79
+ }
80
+ if (this.reading.has(channel)) {
81
+ console.debug(`memory queue (${channel}) is already reading, waiting last reading to end...`);
82
+ await this.reading.get(channel);
83
+ }
84
+ const event = this.events.get(channel);
85
+ if (!event) {
86
+ console.warn(`memory queue (${channel}) not found, skipping...`);
87
+ return;
88
+ }
89
+ if (!event.idle()) {
90
+ console.debug(`memory queue (${channel}) is not idle, skipping...`);
91
+ return;
92
+ }
93
+ const reading = this.read(channel);
94
+ this.reading.set(channel, reading);
95
+ await reading;
96
+ }, "listen");
97
+ isConnected() {
98
+ return this.connected;
99
+ }
100
+ setConnected(connected) {
101
+ this.connected = connected;
102
+ }
103
+ async loadFromStorage() {
104
+ let queues = {};
105
+ let exists = false;
106
+ try {
107
+ await import_promises.default.stat(this.storagePath);
108
+ exists = true;
109
+ } catch (ex) {
110
+ console.info(`memory queue storage file not found, skip`);
111
+ }
112
+ if (exists) {
113
+ try {
114
+ const queueJson = await import_promises.default.readFile(this.storagePath);
115
+ queues = JSON.parse(queueJson.toString());
116
+ console.debug("memory queue loaded from storage", queues);
117
+ await import_promises.default.unlink(this.storagePath);
118
+ } catch (ex) {
119
+ console.error("failed to load queue from storage", ex);
120
+ }
121
+ }
122
+ this.queues = new Map(Object.entries(queues));
123
+ }
124
+ async saveToStorage() {
125
+ const queues = Array.from(this.queues.entries()).reduce((acc, [channel, queue]) => {
126
+ if (queue == null ? void 0 : queue.length) {
127
+ acc[channel] = queue;
128
+ }
129
+ return acc;
130
+ }, {});
131
+ if (Object.keys(queues).length) {
132
+ await import_promises.default.mkdir(import_path.default.dirname(this.storagePath), { recursive: true });
133
+ await import_promises.default.writeFile(this.storagePath, JSON.stringify(queues));
134
+ console.debug("memory queue saved to storage", queues);
135
+ } else {
136
+ console.debug("memory queue empty, no need to save to storage");
137
+ }
138
+ }
139
+ async connect() {
140
+ if (this.connected) {
141
+ return;
142
+ }
143
+ await this.loadFromStorage();
144
+ this.connected = true;
145
+ setImmediate(() => {
146
+ for (const channel of this.events.keys()) {
147
+ this.consume(channel);
148
+ }
149
+ });
150
+ }
151
+ async close() {
152
+ this.connected = false;
153
+ if (this.processing) {
154
+ console.info("memory queue waiting for processing job...");
155
+ await this.processing;
156
+ console.info("memory queue job cleaned");
157
+ }
158
+ console.log("memory queue gracefully shutting down...");
159
+ await this.saveToStorage();
160
+ }
161
+ subscribe(channel, options) {
162
+ if (this.events.has(channel)) {
163
+ return;
164
+ }
165
+ this.events.set(channel, options);
166
+ if (!this.queues.has(channel)) {
167
+ this.queues.set(channel, []);
168
+ }
169
+ this.emitter.on(channel, this.listen);
170
+ if (this.connected) {
171
+ this.consume(channel);
172
+ }
173
+ }
174
+ unsubscribe(channel) {
175
+ if (!this.events.has(channel)) {
176
+ return;
177
+ }
178
+ this.events.delete(channel);
179
+ this.emitter.off(channel, this.listen);
180
+ }
181
+ publish(channel, content, options = { timestamp: Date.now() }) {
182
+ const event = this.events.get(channel);
183
+ if (!event) {
184
+ return;
185
+ }
186
+ if (!this.queues.get(channel)) {
187
+ this.queues.set(channel, []);
188
+ }
189
+ const queue = this.queues.get(channel);
190
+ const message = { id: (0, import_crypto.randomUUID)(), content, options };
191
+ queue.push(message);
192
+ console.debug(`memory queue (${channel}) published message`, content);
193
+ setImmediate(() => {
194
+ this.emitter.emit(channel, channel);
195
+ });
196
+ }
197
+ async consume(channel, once = false) {
198
+ while (this.connected && this.events.get(channel)) {
199
+ const event = this.events.get(channel);
200
+ const interval = event.interval || QUEUE_DEFAULT_INTERVAL;
201
+ const queue = this.queues.get(channel);
202
+ if (event.idle() && (queue == null ? void 0 : queue.length)) {
203
+ await this.listen(channel);
204
+ }
205
+ if (once) {
206
+ break;
207
+ }
208
+ await (0, import_utils.sleep)(interval);
209
+ }
210
+ }
211
+ async read(channel) {
212
+ const event = this.events.get(channel);
213
+ if (!event) {
214
+ this.reading.delete(channel);
215
+ return;
216
+ }
217
+ const queue = this.queues.get(channel);
218
+ if (queue == null ? void 0 : queue.length) {
219
+ const messages = queue.slice(0, event.concurrency || QUEUE_DEFAULT_CONCURRENCY);
220
+ console.debug(`memory queue (${channel}) read ${messages.length} messages`, messages);
221
+ queue.splice(0, messages.length);
222
+ const batch = messages.map(({ id, ...message }) => this.process(channel, { id, message }));
223
+ await Promise.all(batch);
224
+ }
225
+ this.reading.delete(channel);
226
+ }
227
+ async process(channel, { id, message }) {
228
+ const event = this.events.get(channel);
229
+ const { content, options: { timeout = QUEUE_DEFAULT_ACK_TIMEOUT, maxRetries = 0, retried = 0 } = {} } = message;
230
+ try {
231
+ console.debug(`memory queue (${channel}) processing message (${id})...`, content);
232
+ await event.process(content, {
233
+ id,
234
+ retried,
235
+ signal: AbortSignal.timeout(timeout)
236
+ });
237
+ console.debug(`memory queue (${channel}) consumed message (${id})`);
238
+ } catch (ex) {
239
+ if (maxRetries > 0 && retried < maxRetries) {
240
+ const currentRetry = retried + 1;
241
+ console.warn(
242
+ `memory queue (${channel}) consum message (${id}) failed, retrying (${currentRetry} / ${maxRetries})...`,
243
+ ex
244
+ );
245
+ setImmediate(() => {
246
+ this.publish(channel, content, { timeout, maxRetries, retried: currentRetry, timestamp: Date.now() });
247
+ });
248
+ } else {
249
+ console.error(ex);
250
+ }
251
+ }
252
+ }
253
+ };
254
+ __name(_MemoryEventQueueAdapter, "MemoryEventQueueAdapter");
255
+ let MemoryEventQueueAdapter = _MemoryEventQueueAdapter;
256
+ const _EventQueue = class _EventQueue {
257
+ constructor(app, options = {}) {
258
+ this.app = app;
259
+ this.options = options;
260
+ this.setAdapter(new MemoryEventQueueAdapter({ appName: this.app.name }));
261
+ app.on("afterStart", async () => {
262
+ await this.connect();
263
+ });
264
+ app.on("beforeStop", async () => {
265
+ app.logger.info("[queue] gracefully shutting down...");
266
+ await this.close();
267
+ });
268
+ }
269
+ adapter;
270
+ events = /* @__PURE__ */ new Map();
271
+ get channelPrefix() {
272
+ var _a;
273
+ return (_a = this.options) == null ? void 0 : _a.channelPrefix;
274
+ }
275
+ getFullChannel(channel) {
276
+ return [this.app.name, this.channelPrefix, channel].filter(Boolean).join(".");
277
+ }
278
+ setAdapter(adapter) {
279
+ this.adapter = adapter;
280
+ }
281
+ isConnected() {
282
+ if (!this.adapter) {
283
+ return false;
284
+ }
285
+ return this.adapter.isConnected();
286
+ }
287
+ async connect() {
288
+ if (!this.adapter) {
289
+ throw new Error("no adapter set, cannot connect");
290
+ }
291
+ await this.adapter.connect();
292
+ for (const [channel, event] of this.events.entries()) {
293
+ this.adapter.subscribe(this.getFullChannel(channel), event);
294
+ }
295
+ }
296
+ async close() {
297
+ if (!this.adapter) {
298
+ return;
299
+ }
300
+ await this.adapter.close();
301
+ for (const channel of this.events.keys()) {
302
+ this.adapter.unsubscribe(this.getFullChannel(channel));
303
+ }
304
+ }
305
+ subscribe(channel, options) {
306
+ if (this.events.has(channel)) {
307
+ this.app.logger.warn(`event queue already subscribed on channel "${channel}", new subscription will be ignored`);
308
+ return;
309
+ }
310
+ this.events.set(channel, options);
311
+ if (this.isConnected()) {
312
+ this.adapter.subscribe(this.getFullChannel(channel), options);
313
+ }
314
+ }
315
+ unsubscribe(channel) {
316
+ if (!this.events.has(channel)) {
317
+ return;
318
+ }
319
+ this.events.delete(channel);
320
+ if (this.isConnected()) {
321
+ this.adapter.unsubscribe(this.getFullChannel(channel));
322
+ }
323
+ }
324
+ async publish(channel, message, options = {}) {
325
+ if (!this.adapter) {
326
+ throw new Error("no adapter set, cannot publish");
327
+ }
328
+ if (!this.isConnected()) {
329
+ throw new Error("event queue not connected, cannot publish");
330
+ }
331
+ const c = this.getFullChannel(channel);
332
+ this.app.logger.debug("event queue publishing:", { channel: c, message });
333
+ await this.adapter.publish(c, message, {
334
+ timeout: QUEUE_DEFAULT_ACK_TIMEOUT,
335
+ ...options,
336
+ timestamp: Date.now()
337
+ });
338
+ }
339
+ };
340
+ __name(_EventQueue, "EventQueue");
341
+ let EventQueue = _EventQueue;
342
+ var event_queue_default = EventQueue;
343
+ // Annotate the CommonJS export names for ESM import in node:
344
+ 0 && (module.exports = {
345
+ EventQueue,
346
+ MemoryEventQueueAdapter,
347
+ QUEUE_DEFAULT_ACK_TIMEOUT,
348
+ QUEUE_DEFAULT_CONCURRENCY,
349
+ QUEUE_DEFAULT_INTERVAL
350
+ });
package/lib/index.d.ts CHANGED
@@ -17,6 +17,8 @@ export * from './migration';
17
17
  export * from './plugin';
18
18
  export * from './plugin-manager';
19
19
  export * from './pub-sub-manager';
20
+ export * from './event-queue';
21
+ export * from './background-job-manager';
20
22
  export declare const OFFICIAL_PLUGIN_PREFIX = "@nocobase/plugin-";
21
23
  export { appendToBuiltInPlugins, findAllPlugins, findBuiltInPlugins, findLocalPlugins, packageNameTrim, } from './plugin-manager/findPackageNames';
22
24
  export { runPluginStaticImports } from './run-plugin-static-imports';
package/lib/index.js CHANGED
@@ -59,6 +59,8 @@ __reExport(src_exports, require("./migration"), module.exports);
59
59
  __reExport(src_exports, require("./plugin"), module.exports);
60
60
  __reExport(src_exports, require("./plugin-manager"), module.exports);
61
61
  __reExport(src_exports, require("./pub-sub-manager"), module.exports);
62
+ __reExport(src_exports, require("./event-queue"), module.exports);
63
+ __reExport(src_exports, require("./background-job-manager"), module.exports);
62
64
  var import_findPackageNames = require("./plugin-manager/findPackageNames");
63
65
  var import_run_plugin_static_imports = require("./run-plugin-static-imports");
64
66
  const OFFICIAL_PLUGIN_PREFIX = "@nocobase/plugin-";
@@ -80,5 +82,7 @@ const OFFICIAL_PLUGIN_PREFIX = "@nocobase/plugin-";
80
82
  ...require("./migration"),
81
83
  ...require("./plugin"),
82
84
  ...require("./plugin-manager"),
83
- ...require("./pub-sub-manager")
85
+ ...require("./pub-sub-manager"),
86
+ ...require("./event-queue"),
87
+ ...require("./background-job-manager")
84
88
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/server",
3
- "version": "1.8.0-beta.9",
3
+ "version": "1.9.0-alpha.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "license": "AGPL-3.0",
@@ -10,19 +10,19 @@
10
10
  "@koa/cors": "^5.0.0",
11
11
  "@koa/multer": "^3.1.0",
12
12
  "@koa/router": "^13.1.0",
13
- "@nocobase/acl": "1.8.0-beta.9",
14
- "@nocobase/actions": "1.8.0-beta.9",
15
- "@nocobase/auth": "1.8.0-beta.9",
16
- "@nocobase/cache": "1.8.0-beta.9",
17
- "@nocobase/data-source-manager": "1.8.0-beta.9",
18
- "@nocobase/database": "1.8.0-beta.9",
19
- "@nocobase/evaluators": "1.8.0-beta.9",
20
- "@nocobase/lock-manager": "1.8.0-beta.9",
21
- "@nocobase/logger": "1.8.0-beta.9",
22
- "@nocobase/resourcer": "1.8.0-beta.9",
23
- "@nocobase/sdk": "1.8.0-beta.9",
24
- "@nocobase/telemetry": "1.8.0-beta.9",
25
- "@nocobase/utils": "1.8.0-beta.9",
13
+ "@nocobase/acl": "1.9.0-alpha.1",
14
+ "@nocobase/actions": "1.9.0-alpha.1",
15
+ "@nocobase/auth": "1.9.0-alpha.1",
16
+ "@nocobase/cache": "1.9.0-alpha.1",
17
+ "@nocobase/data-source-manager": "1.9.0-alpha.1",
18
+ "@nocobase/database": "1.9.0-alpha.1",
19
+ "@nocobase/evaluators": "1.9.0-alpha.1",
20
+ "@nocobase/lock-manager": "1.9.0-alpha.1",
21
+ "@nocobase/logger": "1.9.0-alpha.1",
22
+ "@nocobase/resourcer": "1.9.0-alpha.1",
23
+ "@nocobase/sdk": "1.9.0-alpha.1",
24
+ "@nocobase/telemetry": "1.9.0-alpha.1",
25
+ "@nocobase/utils": "1.9.0-alpha.1",
26
26
  "@types/decompress": "4.2.7",
27
27
  "@types/ini": "^1.3.31",
28
28
  "@types/koa-send": "^4.1.3",
@@ -57,5 +57,5 @@
57
57
  "@types/serve-handler": "^6.1.1",
58
58
  "@types/ws": "^8.5.5"
59
59
  },
60
- "gitHead": "a3449d646c72965845f8c52e52fff9dba759c564"
60
+ "gitHead": "71ebde8a27da3c5c4d64194571804eeab6e95ce0"
61
61
  }