mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-05 11:35:34 +08:00
@@ -0,0 +1 @@
|
||||
export const cmdSymbol = Symbol('cmds');
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './consts.js';
|
||||
export * from './manager.js';
|
||||
export * from './types.js';
|
||||
@@ -0,0 +1,237 @@
|
||||
import { LifeCycleWatcher } from '../extension/index.js';
|
||||
import { cmdSymbol } from './consts.js';
|
||||
import type { Chain, Command, InitCommandCtx } from './types.js';
|
||||
|
||||
/**
|
||||
* Command manager to manage all commands
|
||||
*
|
||||
* Commands are functions that take a context and a next function as arguments
|
||||
*
|
||||
* ```ts
|
||||
* const myCommand: Command<input, output> = (ctx, next) => {
|
||||
* const count = ctx.count || 0;
|
||||
*
|
||||
* const success = someOperation();
|
||||
* if (success) {
|
||||
* return next({ count: count + 1 });
|
||||
* }
|
||||
* // if the command is not successful, you can return without calling next
|
||||
* return;
|
||||
* ```
|
||||
*
|
||||
* Command input and output data can be defined in the `Command` type
|
||||
*
|
||||
* ```ts
|
||||
* // input: ctx.firstName, ctx.lastName
|
||||
* // output: ctx.fullName
|
||||
* const myCommand: Command<{ firstName: string; lastName: string }, { fullName: string }> = (ctx, next) => {
|
||||
* const { firstName, lastName } = ctx;
|
||||
* const fullName = `${firstName} ${lastName}`;
|
||||
* return next({ fullName });
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Commands can be run in two ways:
|
||||
*
|
||||
* 1. Using `exec` method
|
||||
* `exec` is used to run a single command
|
||||
* ```ts
|
||||
* const [result, data] = commandManager.exec(myCommand, payload);
|
||||
* ```
|
||||
*
|
||||
* 2. Using `chain` method
|
||||
* `chain` is used to run a series of commands
|
||||
* ```ts
|
||||
* const chain = commandManager.chain();
|
||||
* const [result, data] = chain
|
||||
* .pipe(myCommand1)
|
||||
* .pipe(myCommand2, payload)
|
||||
* .run();
|
||||
* ```
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Command chains will stop running if a command is not successful
|
||||
*
|
||||
* ```ts
|
||||
* const chain = commandManager.chain();
|
||||
* const [result, data] = chain
|
||||
* .chain(myCommand1) <-- if this fail
|
||||
* .chain(myCommand2, payload) <- this won't run
|
||||
* .run();
|
||||
*
|
||||
* result <- result will be `false`
|
||||
* ```
|
||||
*
|
||||
* You can use `try` to run a series of commands and if one of them is successful, it will continue to the next command
|
||||
* ```ts
|
||||
* const chain = commandManager.chain();
|
||||
* const [result, data] = chain
|
||||
* .try(chain => [
|
||||
* chain.pipe(myCommand1), <- if this fail
|
||||
* chain.pipe(myCommand2, payload), <- this will run, if this success
|
||||
* chain.pipe(myCommand3), <- this won't run
|
||||
* ])
|
||||
* .run();
|
||||
* ```
|
||||
*
|
||||
* The `tryAll` method is similar to `try`, but it will run all commands even if one of them is successful
|
||||
* ```ts
|
||||
* const chain = commandManager.chain();
|
||||
* const [result, data] = chain
|
||||
* .try(chain => [
|
||||
* chain.pipe(myCommand1), <- if this success
|
||||
* chain.pipe(myCommand2), <- this will also run
|
||||
* chain.pipe(myCommand3), <- so will this
|
||||
* ])
|
||||
* .run();
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export class CommandManager extends LifeCycleWatcher {
|
||||
static override readonly key = 'commandManager';
|
||||
|
||||
private readonly _createChain = (_cmds: Command[]): Chain => {
|
||||
const getCommandCtx = this._getCommandCtx;
|
||||
const createChain = this._createChain;
|
||||
const chain = this.chain;
|
||||
|
||||
return {
|
||||
[cmdSymbol]: _cmds,
|
||||
run: function (this: Chain) {
|
||||
let ctx = getCommandCtx();
|
||||
let success = false;
|
||||
try {
|
||||
const cmds = this[cmdSymbol];
|
||||
ctx = runCmds(ctx, [
|
||||
...cmds,
|
||||
(_, next) => {
|
||||
success = true;
|
||||
next();
|
||||
},
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return [success, ctx];
|
||||
},
|
||||
with: function (this: Chain, value) {
|
||||
const cmds = this[cmdSymbol];
|
||||
return createChain([...cmds, (_, next) => next(value)]) as never;
|
||||
},
|
||||
pipe: function (this: Chain, command: Command, input?: object) {
|
||||
const cmds = this[cmdSymbol];
|
||||
return createChain([
|
||||
...cmds,
|
||||
(ctx, next) => command({ ...ctx, ...input }, next),
|
||||
]);
|
||||
},
|
||||
try: function (this: Chain, fn) {
|
||||
const cmds = this[cmdSymbol];
|
||||
return createChain([
|
||||
...cmds,
|
||||
(beforeCtx, next) => {
|
||||
let ctx = beforeCtx;
|
||||
|
||||
const commands = fn(chain());
|
||||
|
||||
commands.some(innerChain => {
|
||||
innerChain[cmdSymbol] = [
|
||||
(_, next) => {
|
||||
next(ctx);
|
||||
},
|
||||
...innerChain[cmdSymbol],
|
||||
];
|
||||
|
||||
const [success, branchCtx] = innerChain.run();
|
||||
ctx = { ...ctx, ...branchCtx };
|
||||
|
||||
if (success) {
|
||||
next(ctx);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
]) as never;
|
||||
},
|
||||
tryAll: function (this: Chain, fn) {
|
||||
const cmds = this[cmdSymbol];
|
||||
return createChain([
|
||||
...cmds,
|
||||
(beforeCtx, next) => {
|
||||
let ctx = beforeCtx;
|
||||
|
||||
let allFail = true;
|
||||
|
||||
const commands = fn(chain());
|
||||
|
||||
commands.forEach(innerChain => {
|
||||
innerChain[cmdSymbol] = [
|
||||
(_, next) => {
|
||||
next(ctx);
|
||||
},
|
||||
...innerChain[cmdSymbol],
|
||||
];
|
||||
|
||||
const [success, branchCtx] = innerChain.run();
|
||||
ctx = { ...ctx, ...branchCtx };
|
||||
|
||||
if (success) {
|
||||
allFail = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!allFail) {
|
||||
next(ctx);
|
||||
}
|
||||
},
|
||||
]) as never;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private readonly _getCommandCtx = (): InitCommandCtx => {
|
||||
return {
|
||||
std: this.std,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a chain to run a series of commands
|
||||
* ```ts
|
||||
* const chain = commandManager.chain();
|
||||
* const [result, data] = chain
|
||||
* .myCommand1()
|
||||
* .myCommand2(payload)
|
||||
* .run();
|
||||
* ```
|
||||
* @returns [success, data] - success is a boolean to indicate if the chain is successful,
|
||||
* data is the final context after running the chain
|
||||
*/
|
||||
chain = (): Chain<InitCommandCtx> => {
|
||||
return this._createChain([]);
|
||||
};
|
||||
|
||||
exec = <Output extends object, Input extends object>(
|
||||
command: Command<Input, Output>,
|
||||
input?: Input
|
||||
) => {
|
||||
return this.chain().pipe(command, input).run();
|
||||
};
|
||||
}
|
||||
|
||||
function runCmds(ctx: InitCommandCtx, [cmd, ...rest]: Command[]) {
|
||||
let _ctx = ctx;
|
||||
if (cmd) {
|
||||
cmd(ctx, data => {
|
||||
_ctx = runCmds({ ...ctx, ...data }, rest);
|
||||
});
|
||||
}
|
||||
return _ctx;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// type A = {};
|
||||
// type B = { prop?: string };
|
||||
// type C = { prop: string };
|
||||
// type TestA = MakeOptionalIfEmpty<A>; // void | {}
|
||||
// type TestB = MakeOptionalIfEmpty<B>; // void | { prop?: string }
|
||||
// type TestC = MakeOptionalIfEmpty<C>; // { prop: string }
|
||||
import type { BlockStdScope } from '../scope/std-scope.js';
|
||||
import type { cmdSymbol } from './consts.js';
|
||||
|
||||
export interface InitCommandCtx {
|
||||
std: BlockStdScope;
|
||||
}
|
||||
|
||||
export type Cmds = {
|
||||
[cmdSymbol]: Command[];
|
||||
};
|
||||
|
||||
export type Command<Input = InitCommandCtx, Output = {}> = (
|
||||
input: Input & InitCommandCtx,
|
||||
next: (output?: Output) => void
|
||||
) => void;
|
||||
|
||||
export type Chain<CommandCtx extends object = InitCommandCtx> = {
|
||||
[cmdSymbol]: Command[];
|
||||
with: <Out extends object>(input: Out) => Chain<CommandCtx & Out>;
|
||||
pipe: {
|
||||
<Out extends object>(
|
||||
command: Command<CommandCtx, Out>
|
||||
): Chain<CommandCtx & Out>;
|
||||
<Out extends object, In extends object>(
|
||||
command: Command<In, Out>,
|
||||
input?: In
|
||||
): Chain<CommandCtx & In & Out>;
|
||||
};
|
||||
try: <Out extends object>(
|
||||
commands: (chain: Chain<CommandCtx>) => Chain<CommandCtx & Out>[]
|
||||
) => Chain<CommandCtx & Out>;
|
||||
tryAll: <Out extends object>(
|
||||
commands: (chain: Chain<CommandCtx>) => Chain<CommandCtx & Out>[]
|
||||
) => Chain<CommandCtx & Out>;
|
||||
run: () => [false, Partial<CommandCtx> & InitCommandCtx] | [true, CommandCtx];
|
||||
};
|
||||
Reference in New Issue
Block a user