@fraqjs/fraq 0.12.0 → 0.13.0

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.
package/dist/index.d.mts CHANGED
@@ -5547,6 +5547,7 @@ type RouteActivation =
5547
5547
  type: 'prefix';
5548
5548
  prefix: string;
5549
5549
  };
5550
+ type RouteActivationInput = RouteActivation | readonly RouteActivation[];
5550
5551
  type RouteDescriptor =
5551
5552
  | {
5552
5553
  type: 'command';
@@ -5560,7 +5561,7 @@ type RouteDescriptor =
5560
5561
  path: readonly string[];
5561
5562
  meta?: RouteMeta;
5562
5563
  };
5563
- type RouteActivationResolver = (route: RouteDescriptor, session: Session) => readonly RouteActivation[] | undefined;
5564
+ type RouteActivationResolver = (route: RouteDescriptor, session: Session) => RouteActivationInput | undefined;
5564
5565
  declare const defaultRouteActivationResolver: RouteActivationResolver;
5565
5566
  type RouteEntry =
5566
5567
  | {
@@ -5642,9 +5643,9 @@ declare class Router {
5642
5643
  //#endregion
5643
5644
  //#region src/core/activation.d.ts
5644
5645
  type ContextRouteActivationByScene = Partial<
5645
- Record<IncomingMessage['message_scene'] | 'default', readonly RouteActivation[]>
5646
+ Record<IncomingMessage['message_scene'] | 'default', RouteActivationInput>
5646
5647
  >;
5647
- type ContextRouteActivation = readonly RouteActivation[] | ContextRouteActivationByScene;
5648
+ type ContextRouteActivation = RouteActivationInput | ContextRouteActivationByScene;
5648
5649
  interface ContextRouteActivationMatcher {
5649
5650
  type?: RouteDescriptor['type'] | readonly RouteDescriptor['type'][];
5650
5651
  plugin?: string | readonly string[];
@@ -5673,18 +5674,42 @@ declare function createContextRouteActivationResolver(
5673
5674
  ): RouteActivationResolver;
5674
5675
  //#endregion
5675
5676
  //#region src/protocol/endpoint.d.ts
5677
+ type ApiEndpointName = keyof ApiEndpoints$1;
5676
5678
  type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];
5677
5679
  type AllOptional<T> = RequiredKeys<T> extends never ? true : false;
5678
- type RawApiEndpoint<E extends keyof ApiEndpoints$1> = {
5680
+ type RawApiEndpoint<E extends ApiEndpointName> = {
5679
5681
  request: ApiEndpoints$1[E]['request_ZodInput'] extends null ? null : ApiEndpoints$1[E]['request_ZodInput'];
5680
5682
  response: ApiEndpoints$1[E]['response'] extends null ? null : ApiEndpoints$1[E]['response'];
5681
5683
  };
5682
- type ApiEndpointFunction<E extends keyof ApiEndpoints$1> = RawApiEndpoint<E>['request'] extends null
5683
- ? () => Promise<RawApiEndpoint<E>['response']>
5684
+ type ApiRequest<E extends ApiEndpointName> = RawApiEndpoint<E>['request'];
5685
+ type ApiParams<E extends ApiEndpointName> = RawApiEndpoint<E>['request'] extends null
5686
+ ? undefined
5684
5687
  : AllOptional<RawApiEndpoint<E>['request']> extends true
5685
- ? (params?: RawApiEndpoint<E>['request']) => Promise<RawApiEndpoint<E>['response']>
5686
- : (params: RawApiEndpoint<E>['request']) => Promise<RawApiEndpoint<E>['response']>;
5687
- type ApiEndpoints = { [E in keyof ApiEndpoints$1]: ApiEndpointFunction<E> };
5688
+ ? RawApiEndpoint<E>['request'] | undefined
5689
+ : RawApiEndpoint<E>['request'];
5690
+ type ApiResponse<E extends ApiEndpointName> = RawApiEndpoint<E>['response'];
5691
+ interface ApiCall<E extends ApiEndpointName = ApiEndpointName> {
5692
+ endpoint: E;
5693
+ params: ApiParams<E>;
5694
+ }
5695
+ type ApiNext<E extends ApiEndpointName> =
5696
+ undefined extends ApiParams<E>
5697
+ ? (params?: ApiParams<E>) => Promise<ApiResponse<E>>
5698
+ : (params: ApiParams<E>) => Promise<ApiResponse<E>>;
5699
+ type ApiHook<E extends ApiEndpointName> = (
5700
+ params: ApiParams<E>,
5701
+ next: ApiNext<E>,
5702
+ call: ApiCall<E>,
5703
+ ) => ApiResponse<E> | Promise<ApiResponse<E>>;
5704
+ type AnyApiCall = { [E in ApiEndpointName]: ApiCall<E> }[ApiEndpointName];
5705
+ type AnyApiNext = (params?: unknown) => Promise<unknown>;
5706
+ type AnyApiHook = (call: AnyApiCall, next: AnyApiNext) => unknown | Promise<unknown>;
5707
+ type ApiEndpointFunction<E extends ApiEndpointName> = RawApiEndpoint<E>['request'] extends null
5708
+ ? () => Promise<ApiResponse<E>>
5709
+ : AllOptional<RawApiEndpoint<E>['request']> extends true
5710
+ ? (params?: RawApiEndpoint<E>['request']) => Promise<ApiResponse<E>>
5711
+ : (params: RawApiEndpoint<E>['request']) => Promise<ApiResponse<E>>;
5712
+ type ApiEndpoints = { [E in ApiEndpointName]: ApiEndpointFunction<E> };
5688
5713
  type EventMap = {
5689
5714
  [K in Event['event_type']]: Extract<
5690
5715
  Event,
@@ -5817,16 +5842,18 @@ interface ContextUrlOptions {
5817
5842
  installEventSource?: boolean;
5818
5843
  }
5819
5844
  declare class Context {
5820
- readonly client: MilkyClient;
5821
5845
  readonly router: Router;
5822
5846
  readonly logger: Logger;
5823
5847
  readonly name: string;
5824
5848
  readonly routeActivationResolver: RouteActivationResolver;
5849
+ readonly client: MilkyClient;
5850
+ private readonly baseClient;
5825
5851
  private readonly parent?;
5826
5852
  private readonly filter?;
5827
5853
  private readonly eventBus;
5828
5854
  private readonly plugins;
5829
5855
  private readonly services;
5856
+ private readonly apiHookEntries;
5830
5857
  private readonly subContexts;
5831
5858
  private readonly eventSourceRuntimes;
5832
5859
  private readonly parentEventForwarder?;
@@ -5844,6 +5871,8 @@ declare class Context {
5844
5871
  ...args: T
5845
5872
  ): void;
5846
5873
  installEventSource(eventSource: MilkyEventSource): void;
5874
+ hookApi<E extends ApiEndpointName>(endpoint: E, hook: ApiHook<E>): () => void;
5875
+ hookApi(hook: AnyApiHook): () => void;
5847
5876
  provide<T extends object>(service: ServiceClass<T>, instance: T): void;
5848
5877
  resolve<T extends object>(service: ServiceClass<T>): T;
5849
5878
  tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
@@ -5857,6 +5886,7 @@ declare class Context {
5857
5886
  private startInternal;
5858
5887
  private getState;
5859
5888
  private stopInternal;
5889
+ private assertCanRegisterApiHook;
5860
5890
  private assertCanScheduleTimer;
5861
5891
  private runTimerCallback;
5862
5892
  private clearTimers;
@@ -5878,6 +5908,11 @@ declare class Context {
5878
5908
  private runEventSource;
5879
5909
  private waitForReconnectDelay;
5880
5910
  private resolveReconnectDelay;
5911
+ private createHookClient;
5912
+ private wrapAnyApiHook;
5913
+ private callHookedApi;
5914
+ private collectApiHooks;
5915
+ private callBaseApi;
5881
5916
  static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
5882
5917
  static fromClient(client: MilkyClient, options?: ContextOptions): Context;
5883
5918
  }
@@ -5923,7 +5958,17 @@ declare namespace seg {
5923
5958
  }
5924
5959
  //#endregion
5925
5960
  export {
5961
+ AnyApiCall,
5962
+ AnyApiHook,
5963
+ AnyApiNext,
5964
+ ApiCall,
5965
+ ApiEndpointName,
5926
5966
  ApiEndpoints,
5967
+ ApiHook,
5968
+ ApiNext,
5969
+ ApiParams,
5970
+ ApiRequest,
5971
+ ApiResponse,
5927
5972
  Capturer,
5928
5973
  Command,
5929
5974
  CommandBuilder,
@@ -5956,6 +6001,7 @@ export {
5956
6001
  Plugin,
5957
6002
  RawPattern,
5958
6003
  RouteActivation,
6004
+ RouteActivationInput,
5959
6005
  RouteActivationResolver,
5960
6006
  RouteBranch,
5961
6007
  RouteDescriptor,
package/dist/index.mjs CHANGED
@@ -75,7 +75,7 @@ function uniqueTags(...tagLists) {
75
75
  }
76
76
  //#endregion
77
77
  //#region src/routing/tokenizer.ts
78
- const WHITESPACE_CHARS = new Set([
78
+ const WHITESPACE_CHARS = /* @__PURE__ */ new Set([
79
79
  " ",
80
80
  " ",
81
81
  "\n",
@@ -316,7 +316,7 @@ var Router = class Router {
316
316
  for (const branch of this.branchesFrom(session, [], { includeHidden: true })) {
317
317
  const descriptor = this.describeBranch(branch);
318
318
  const activations = this.activationResolver(descriptor, session) ?? DEFAULT_ACTIVATIONS;
319
- for (const activation of activations) {
319
+ for (const activation of routeActivationInputs(activations)) {
320
320
  const tokenizer = new Tokenizer(message.segments);
321
321
  if (!this.consumeActivation(tokenizer, activation, session)) continue;
322
322
  const match = this.matchBranch(branch, tokenizer, activation);
@@ -485,6 +485,12 @@ var Router = class Router {
485
485
  return params;
486
486
  }
487
487
  };
488
+ function routeActivationInputs(activations) {
489
+ return isRouteActivationArray(activations) ? activations : [activations];
490
+ }
491
+ function isRouteActivationArray(activations) {
492
+ return Array.isArray(activations);
493
+ }
488
494
  //#endregion
489
495
  //#region src/core/activation.ts
490
496
  function createContextRouteActivationResolver(routing, fallback = defaultRouteActivationResolver) {
@@ -516,11 +522,14 @@ function isContextRouteActivationRuleArray(rules) {
516
522
  return Array.isArray(rules);
517
523
  }
518
524
  function resolveContextRouteActivation(activation, scene) {
519
- if (isRouteActivationArray(activation)) return activation;
525
+ if (isRouteActivationInput(activation)) return activation;
520
526
  return activation[scene] ?? activation.default;
521
527
  }
522
- function isRouteActivationArray(activation) {
523
- return Array.isArray(activation);
528
+ function isRouteActivationInput(activation) {
529
+ return Array.isArray(activation) || isRouteActivation(activation);
530
+ }
531
+ function isRouteActivation(activation) {
532
+ return "type" in activation;
524
533
  }
525
534
  function matchesContextRouteActivationRule(route, session, rule) {
526
535
  const match = rule.match;
@@ -811,16 +820,18 @@ function implementsESNextDisposable(service) {
811
820
  const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
812
821
  const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
813
822
  var Context = class Context {
814
- client;
815
823
  router = new Router();
816
824
  logger;
817
825
  name;
818
826
  routeActivationResolver;
827
+ client;
828
+ baseClient;
819
829
  parent;
820
830
  filter;
821
831
  eventBus = mitt();
822
832
  plugins = [];
823
833
  services = /* @__PURE__ */ new Map();
834
+ apiHookEntries = [];
824
835
  subContexts = /* @__PURE__ */ new Map();
825
836
  eventSourceRuntimes = /* @__PURE__ */ new Map();
826
837
  parentEventForwarder;
@@ -831,8 +842,9 @@ var Context = class Context {
831
842
  state = "idle";
832
843
  startPromise;
833
844
  stopPromise;
834
- constructor(client, options, name, parent, filter) {
835
- this.client = client;
845
+ constructor(baseClient, options, name, parent, filter) {
846
+ this.baseClient = baseClient;
847
+ this.client = this.createHookClient();
836
848
  this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
837
849
  this.maxReconnectDelayMs = options?.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
838
850
  this.logHandler = options?.logHandler ?? parent?.logHandler;
@@ -883,6 +895,26 @@ var Context = class Context {
883
895
  this.eventSourceRuntimes.set(eventSource, {});
884
896
  if (this.state === "started") this.startEventSource(eventSource);
885
897
  }
898
+ hookApi(endpointOrHook, hook) {
899
+ this.assertCanRegisterApiHook();
900
+ let entry;
901
+ if (typeof endpointOrHook === "function") entry = { hook: this.wrapAnyApiHook(endpointOrHook) };
902
+ else {
903
+ if (!hook) throw new Error(`API hook for endpoint ${endpointOrHook} is missing a handler.`);
904
+ entry = {
905
+ endpoint: endpointOrHook,
906
+ hook
907
+ };
908
+ }
909
+ this.apiHookEntries.push(entry);
910
+ let disposed = false;
911
+ return () => {
912
+ if (disposed) return;
913
+ disposed = true;
914
+ const index = this.apiHookEntries.indexOf(entry);
915
+ if (index !== -1) this.apiHookEntries.splice(index, 1);
916
+ };
917
+ }
886
918
  provide(service, instance) {
887
919
  if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
888
920
  if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
@@ -912,7 +944,7 @@ and implement the dispose method to clean up resources when the context stops.
912
944
  if (filter) throw new Error(`Sub context "${name}" already exists, so the provided filter cannot be applied. Please use fork('${name}') without a filter to get the existing subcontext.`);
913
945
  return this.subContexts.get(name);
914
946
  }
915
- const subContext = new Context(this.client, void 0, name, this, filter);
947
+ const subContext = new Context(this.baseClient, void 0, name, this, filter);
916
948
  this.subContexts.set(name, subContext);
917
949
  return subContext;
918
950
  }
@@ -1044,9 +1076,14 @@ and implement the dispose method to clean up resources when the context stops.
1044
1076
  errors.push(error);
1045
1077
  }
1046
1078
  }
1079
+ this.apiHookEntries.length = 0;
1047
1080
  if (errors.length === 1) throw errors[0];
1048
1081
  if (errors.length > 1) throw new AggregateError(errors, `Context "${this.name}" failed to stop cleanly.`);
1049
1082
  }
1083
+ assertCanRegisterApiHook() {
1084
+ if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot register API hooks while it is stopping.`);
1085
+ if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot register API hooks after it has stopped.`);
1086
+ }
1050
1087
  assertCanScheduleTimer() {
1051
1088
  if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot schedule timers while it is stopping.`);
1052
1089
  if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot schedule timers after it has stopped.`);
@@ -1281,8 +1318,53 @@ and implement the dispose method to clean up resources when the context stops.
1281
1318
  runtime.resolveReconnectTimer = void 0;
1282
1319
  resolve?.();
1283
1320
  }
1321
+ createHookClient() {
1322
+ return new Proxy(this.baseClient, { get: (target, prop, receiver) => {
1323
+ if (typeof prop === "string" && prop.includes("_")) return (params) => this.callHookedApi(prop, params);
1324
+ return Reflect.get(target, prop, receiver);
1325
+ } });
1326
+ }
1327
+ wrapAnyApiHook(hook) {
1328
+ return (params, next, call) => {
1329
+ return hook(call, (nextParams = params) => next(nextParams));
1330
+ };
1331
+ }
1332
+ async callHookedApi(endpoint, params) {
1333
+ const hooks = this.collectApiHooks(endpoint);
1334
+ const dispatch = async (index, currentParams) => {
1335
+ const hook = hooks[index];
1336
+ if (!hook) return await this.callBaseApi(endpoint, currentParams);
1337
+ let nextCalled = false;
1338
+ return await hook(currentParams, async (nextParams = currentParams) => {
1339
+ if (nextCalled) throw new Error(`API hook for endpoint ${endpoint} called next() multiple times.`);
1340
+ nextCalled = true;
1341
+ return await dispatch(index + 1, nextParams);
1342
+ }, {
1343
+ endpoint,
1344
+ params: currentParams
1345
+ });
1346
+ };
1347
+ return await dispatch(0, params);
1348
+ }
1349
+ collectApiHooks(endpoint) {
1350
+ const hooks = [];
1351
+ let context = this;
1352
+ while (context) {
1353
+ for (const entry of context.apiHookEntries.toReversed()) if (entry.endpoint === void 0 || entry.endpoint === endpoint) hooks.push(entry.hook);
1354
+ context = context.parent;
1355
+ }
1356
+ return hooks;
1357
+ }
1358
+ async callBaseApi(endpoint, params) {
1359
+ const callApi = this.baseClient.callApi;
1360
+ if (typeof callApi === "function") return await callApi.call(this.baseClient, endpoint, params);
1361
+ const method = this.baseClient[endpoint];
1362
+ if (typeof method !== "function") throw new Error(`Milky client does not implement API endpoint ${endpoint}.`);
1363
+ return await method.call(this.baseClient, params);
1364
+ }
1284
1365
  static fromUrl(baseUrl, options) {
1285
- const context = new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
1366
+ const client = createMilkyClient(baseUrl, { accessToken: options?.accessToken });
1367
+ const context = new Context(client, options);
1286
1368
  if (options?.installEventSource ?? true) context.installEventSource(createMilkyWebSocketEventSource(baseUrl, { accessToken: options?.accessToken }));
1287
1369
  return context;
1288
1370
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.12.0",
4
+ "version": "0.13.0",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"