@fraqjs/fraq 0.12.1 → 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
@@ -5674,18 +5674,42 @@ declare function createContextRouteActivationResolver(
5674
5674
  ): RouteActivationResolver;
5675
5675
  //#endregion
5676
5676
  //#region src/protocol/endpoint.d.ts
5677
+ type ApiEndpointName = keyof ApiEndpoints$1;
5677
5678
  type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];
5678
5679
  type AllOptional<T> = RequiredKeys<T> extends never ? true : false;
5679
- type RawApiEndpoint<E extends keyof ApiEndpoints$1> = {
5680
+ type RawApiEndpoint<E extends ApiEndpointName> = {
5680
5681
  request: ApiEndpoints$1[E]['request_ZodInput'] extends null ? null : ApiEndpoints$1[E]['request_ZodInput'];
5681
5682
  response: ApiEndpoints$1[E]['response'] extends null ? null : ApiEndpoints$1[E]['response'];
5682
5683
  };
5683
- type ApiEndpointFunction<E extends keyof ApiEndpoints$1> = RawApiEndpoint<E>['request'] extends null
5684
- ? () => 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
5685
5687
  : AllOptional<RawApiEndpoint<E>['request']> extends true
5686
- ? (params?: RawApiEndpoint<E>['request']) => Promise<RawApiEndpoint<E>['response']>
5687
- : (params: RawApiEndpoint<E>['request']) => Promise<RawApiEndpoint<E>['response']>;
5688
- 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> };
5689
5713
  type EventMap = {
5690
5714
  [K in Event['event_type']]: Extract<
5691
5715
  Event,
@@ -5818,16 +5842,18 @@ interface ContextUrlOptions {
5818
5842
  installEventSource?: boolean;
5819
5843
  }
5820
5844
  declare class Context {
5821
- readonly client: MilkyClient;
5822
5845
  readonly router: Router;
5823
5846
  readonly logger: Logger;
5824
5847
  readonly name: string;
5825
5848
  readonly routeActivationResolver: RouteActivationResolver;
5849
+ readonly client: MilkyClient;
5850
+ private readonly baseClient;
5826
5851
  private readonly parent?;
5827
5852
  private readonly filter?;
5828
5853
  private readonly eventBus;
5829
5854
  private readonly plugins;
5830
5855
  private readonly services;
5856
+ private readonly apiHookEntries;
5831
5857
  private readonly subContexts;
5832
5858
  private readonly eventSourceRuntimes;
5833
5859
  private readonly parentEventForwarder?;
@@ -5845,6 +5871,8 @@ declare class Context {
5845
5871
  ...args: T
5846
5872
  ): void;
5847
5873
  installEventSource(eventSource: MilkyEventSource): void;
5874
+ hookApi<E extends ApiEndpointName>(endpoint: E, hook: ApiHook<E>): () => void;
5875
+ hookApi(hook: AnyApiHook): () => void;
5848
5876
  provide<T extends object>(service: ServiceClass<T>, instance: T): void;
5849
5877
  resolve<T extends object>(service: ServiceClass<T>): T;
5850
5878
  tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
@@ -5858,6 +5886,7 @@ declare class Context {
5858
5886
  private startInternal;
5859
5887
  private getState;
5860
5888
  private stopInternal;
5889
+ private assertCanRegisterApiHook;
5861
5890
  private assertCanScheduleTimer;
5862
5891
  private runTimerCallback;
5863
5892
  private clearTimers;
@@ -5879,6 +5908,11 @@ declare class Context {
5879
5908
  private runEventSource;
5880
5909
  private waitForReconnectDelay;
5881
5910
  private resolveReconnectDelay;
5911
+ private createHookClient;
5912
+ private wrapAnyApiHook;
5913
+ private callHookedApi;
5914
+ private collectApiHooks;
5915
+ private callBaseApi;
5882
5916
  static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
5883
5917
  static fromClient(client: MilkyClient, options?: ContextOptions): Context;
5884
5918
  }
@@ -5924,7 +5958,17 @@ declare namespace seg {
5924
5958
  }
5925
5959
  //#endregion
5926
5960
  export {
5961
+ AnyApiCall,
5962
+ AnyApiHook,
5963
+ AnyApiNext,
5964
+ ApiCall,
5965
+ ApiEndpointName,
5927
5966
  ApiEndpoints,
5967
+ ApiHook,
5968
+ ApiNext,
5969
+ ApiParams,
5970
+ ApiRequest,
5971
+ ApiResponse,
5928
5972
  Capturer,
5929
5973
  Command,
5930
5974
  CommandBuilder,
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",
@@ -820,16 +820,18 @@ function implementsESNextDisposable(service) {
820
820
  const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
821
821
  const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
822
822
  var Context = class Context {
823
- client;
824
823
  router = new Router();
825
824
  logger;
826
825
  name;
827
826
  routeActivationResolver;
827
+ client;
828
+ baseClient;
828
829
  parent;
829
830
  filter;
830
831
  eventBus = mitt();
831
832
  plugins = [];
832
833
  services = /* @__PURE__ */ new Map();
834
+ apiHookEntries = [];
833
835
  subContexts = /* @__PURE__ */ new Map();
834
836
  eventSourceRuntimes = /* @__PURE__ */ new Map();
835
837
  parentEventForwarder;
@@ -840,8 +842,9 @@ var Context = class Context {
840
842
  state = "idle";
841
843
  startPromise;
842
844
  stopPromise;
843
- constructor(client, options, name, parent, filter) {
844
- this.client = client;
845
+ constructor(baseClient, options, name, parent, filter) {
846
+ this.baseClient = baseClient;
847
+ this.client = this.createHookClient();
845
848
  this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
846
849
  this.maxReconnectDelayMs = options?.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
847
850
  this.logHandler = options?.logHandler ?? parent?.logHandler;
@@ -892,6 +895,26 @@ var Context = class Context {
892
895
  this.eventSourceRuntimes.set(eventSource, {});
893
896
  if (this.state === "started") this.startEventSource(eventSource);
894
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
+ }
895
918
  provide(service, instance) {
896
919
  if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
897
920
  if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
@@ -921,7 +944,7 @@ and implement the dispose method to clean up resources when the context stops.
921
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.`);
922
945
  return this.subContexts.get(name);
923
946
  }
924
- const subContext = new Context(this.client, void 0, name, this, filter);
947
+ const subContext = new Context(this.baseClient, void 0, name, this, filter);
925
948
  this.subContexts.set(name, subContext);
926
949
  return subContext;
927
950
  }
@@ -1053,9 +1076,14 @@ and implement the dispose method to clean up resources when the context stops.
1053
1076
  errors.push(error);
1054
1077
  }
1055
1078
  }
1079
+ this.apiHookEntries.length = 0;
1056
1080
  if (errors.length === 1) throw errors[0];
1057
1081
  if (errors.length > 1) throw new AggregateError(errors, `Context "${this.name}" failed to stop cleanly.`);
1058
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
+ }
1059
1087
  assertCanScheduleTimer() {
1060
1088
  if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot schedule timers while it is stopping.`);
1061
1089
  if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot schedule timers after it has stopped.`);
@@ -1290,8 +1318,53 @@ and implement the dispose method to clean up resources when the context stops.
1290
1318
  runtime.resolveReconnectTimer = void 0;
1291
1319
  resolve?.();
1292
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
+ }
1293
1365
  static fromUrl(baseUrl, options) {
1294
- 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);
1295
1368
  if (options?.installEventSource ?? true) context.installEventSource(createMilkyWebSocketEventSource(baseUrl, { accessToken: options?.accessToken }));
1296
1369
  return context;
1297
1370
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.12.1",
4
+ "version": "0.13.0",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"