Skip to content

Commit

Permalink
feat: mv Singleton from egg
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Jan 20, 2025
1 parent c07821d commit c355ec2
Show file tree
Hide file tree
Showing 4 changed files with 579 additions and 1 deletion.
33 changes: 32 additions & 1 deletion src/egg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
MiddlewareFunc as KoaMiddlewareFunc,
Next,
} from '@eggjs/koa';
import { EggConsoleLogger } from 'egg-logger';
import { EggConsoleLogger, Logger } from 'egg-logger';
import { RegisterOptions, ResourcesController, EggRouter as Router } from '@eggjs/router';
import type { ReadyFunctionArg } from 'get-ready';
import { BaseContextClass } from './base_context_class.js';
Expand All @@ -19,6 +19,9 @@ import { Lifecycle } from './lifecycle.js';
import { EggLoader } from './loader/egg_loader.js';
import utils from './utils/index.js';
import { EggAppConfig } from './types.js';
import {
Singleton, type SingletonCreateMethod, type SingletonOptions,
} from './singleton.js';

const debug = debuglog('@eggjs/core/egg');

Expand Down Expand Up @@ -185,6 +188,34 @@ export class EggCore extends KoaApplication {
});
}

get logger(): Logger {
return this.console;
}

get coreLogger(): Logger {
return this.console;
}

/**
* create a singleton instance
* @param {String} name - unique name for singleton
* @param {Function|AsyncFunction} create - method will be invoked when singleton instance create
*/
addSingleton(name: string, create: SingletonCreateMethod) {
const options: SingletonOptions = {
name,
create,
app: this,
};
const singleton = new Singleton(options);
const initPromise = singleton.init();
if (initPromise) {
this.beforeStart(async () => {
await initPromise;
});
}
}

/**
* override koa's app.use, support generator function
* @since 1.0.0
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { utils };
export * from './egg.js';
export * from './base_context_class.js';
export * from './lifecycle.js';
export * from './singleton.js';
export * from './loader/egg_loader.js';
export * from './loader/file_loader.js';
export * from './loader/context_loader.js';
Expand Down
149 changes: 149 additions & 0 deletions src/singleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import assert from 'node:assert';
import { isAsyncFunction } from 'is-type-of';
import type { EggCore } from './egg.js';

export type SingletonCreateMethod =
(config: Record<string, any>, app: EggCore, clientName: string) => unknown | Promise<unknown>;

export interface SingletonOptions {
name: string;
app: EggCore;
create: SingletonCreateMethod;
}

export class Singleton<T = any> {
readonly clients = new Map<string, T>();
readonly app: EggCore;
readonly create: SingletonCreateMethod;
readonly name: string;
readonly options: Record<string, any>;

constructor(options: SingletonOptions) {
assert(options.name, '[egg:singleton] Singleton#constructor options.name is required');
assert(options.app, '[egg:singleton] Singleton#constructor options.app is required');
assert(options.create, '[egg:singleton] Singleton#constructor options.create is required');
assert(!(options.name in options.app), `[egg:singleton] ${options.name} is already exists in app`);
this.app = options.app;
this.name = options.name;
this.create = options.create;
this.options = options.app.config[this.name] ?? {};
}

init() {
return isAsyncFunction(this.create) ? this.initAsync() : this.initSync();
}

initSync() {
const options = this.options;
assert(!(options.client && options.clients),
`[egg:singleton] ${this.name} can not set options.client and options.clients both`);

// alias app[name] as client, but still support createInstance method
if (options.client) {
const client = this.createInstance(options.client, options.name);
this.#setClientToApp(client);
this.#extendDynamicMethods(client);
return;
}

// multi client, use app[name].getSingletonInstance(id)
if (options.clients) {
Object.keys(options.clients).forEach(id => {
const client = this.createInstance(options.clients[id], id);
this.clients.set(id, client);
});
this.#setClientToApp(this);
return;
}

// no config.clients and config.client
this.#setClientToApp(this);
}

async initAsync() {
const options = this.options;
assert(!(options.client && options.clients),
`[egg:singleton] ${this.name} can not set options.client and options.clients both`);

// alias app[name] as client, but still support createInstance method
if (options.client) {
const client = await this.createInstanceAsync(options.client, options.name);
this.#setClientToApp(client);
this.#extendDynamicMethods(client);
return;
}

// multi client, use app[name].getInstance(id)
if (options.clients) {
await Promise.all(Object.keys(options.clients).map((id: string) => {
return this.createInstanceAsync(options.clients[id], id)
.then(client => this.clients.set(id, client));
}));
this.#setClientToApp(this);
return;
}

// no config.clients and config.client
this.#setClientToApp(this);
}

#setClientToApp(client: unknown) {
Reflect.set(this.app, this.name, client);
}

/**
* @deprecated please use `getSingletonInstance(id)` instead
*/
get(id: string) {
return this.clients.get(id)!;
}

/**
* Get singleton instance by id
*/
getSingletonInstance(id: string) {
return this.clients.get(id)!;
}

createInstance(config: Record<string, any>, clientName: string) {
// async creator only support createInstanceAsync
assert(!isAsyncFunction(this.create),
`egg:singleton ${this.name} only support create asynchronous, please use createInstanceAsync`);
// options.default will be merge in to options.clients[id]
config = {
...this.options.default,
...config,
};
return (this.create as SingletonCreateMethod)(config, this.app, clientName) as T;
}

async createInstanceAsync(config: Record<string, any>, clientName: string) {
// options.default will be merge in to options.clients[id]
config = {
...this.options.default,
...config,
};
return await this.create(config, this.app, clientName) as T;
}

#extendDynamicMethods(client: any) {
assert(!client.createInstance, 'singleton instance should not have createInstance method');
assert(!client.createInstanceAsync, 'singleton instance should not have createInstanceAsync method');

try {
let extendable = client;
// Object.preventExtensions() or Object.freeze()
if (!Object.isExtensible(client) || Object.isFrozen(client)) {
// eslint-disable-next-line no-proto
extendable = client.__proto__ || client;
}
extendable.createInstance = this.createInstance.bind(this);
extendable.createInstanceAsync = this.createInstanceAsync.bind(this);
} catch (err) {
this.app.coreLogger.warn(
'[egg:singleton] %s dynamic create is disabled because of client is un-extendable',
this.name);
this.app.coreLogger.warn(err);
}
}
}
Loading

0 comments on commit c355ec2

Please sign in to comment.