@fraqjs/fraq 0.12.1 → 0.13.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.
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,
@@ -5804,7 +5828,7 @@ declare function definePlugin<
5804
5828
  OI extends Injection | undefined,
5805
5829
  >(plugin: Plugin<T, I, OI>): Plugin<T, I, OI>;
5806
5830
  //#endregion
5807
- //#region src/core/context.d.ts
5831
+ //#region src/core/context/index.d.ts
5808
5832
  interface ContextOptions {
5809
5833
  reconnect?: {
5810
5834
  initialDelayMs?: number;
@@ -5818,26 +5842,23 @@ 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
- private readonly plugins;
5830
- private readonly services;
5831
5854
  private readonly subContexts;
5832
- private readonly eventSourceRuntimes;
5833
5855
  private readonly parentEventForwarder?;
5834
- private readonly timers;
5835
- private readonly initialReconnectDelayMs;
5836
- private readonly maxReconnectDelayMs;
5837
5856
  private readonly logHandler?;
5838
- private state;
5839
- private startPromise?;
5840
- private stopPromise?;
5857
+ private readonly plugins;
5858
+ private readonly apiHooks;
5859
+ private readonly eventSources;
5860
+ private readonly timers;
5861
+ private readonly lifecycle;
5841
5862
  private constructor();
5842
5863
  on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): () => void;
5843
5864
  install<T extends ParameterList, I extends Injection | undefined, OI extends Injection | undefined>(
@@ -5845,6 +5866,8 @@ declare class Context {
5845
5866
  ...args: T
5846
5867
  ): void;
5847
5868
  installEventSource(eventSource: MilkyEventSource): void;
5869
+ hookApi<E extends ApiEndpointName>(endpoint: E, hook: ApiHook<E>): () => void;
5870
+ hookApi(hook: AnyApiHook): () => void;
5848
5871
  provide<T extends object>(service: ServiceClass<T>, instance: T): void;
5849
5872
  resolve<T extends object>(service: ServiceClass<T>): T;
5850
5873
  tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
@@ -5855,30 +5878,6 @@ declare class Context {
5855
5878
  createSession(selfId: number, message: IncomingMessage): Session;
5856
5879
  start(): Promise<void>;
5857
5880
  stop(): Promise<void>;
5858
- private startInternal;
5859
- private getState;
5860
- private stopInternal;
5861
- private assertCanScheduleTimer;
5862
- private runTimerCallback;
5863
- private clearTimers;
5864
- private stopEventSources;
5865
- private acceptsParentEvent;
5866
- private applyPlugins;
5867
- private recursiveApplyPlugins;
5868
- private startPlugins;
5869
- private recursiveStartPlugins;
5870
- private sortPlugins;
5871
- private collectPendingProvidedServices;
5872
- private areRequiredServicesAvailable;
5873
- private areOptionalServicesReady;
5874
- private collectAvailableServices;
5875
- private createUnresolvablePluginError;
5876
- private getPluginContext;
5877
- private createProxyContextForPlugin;
5878
- private startEventSource;
5879
- private runEventSource;
5880
- private waitForReconnectDelay;
5881
- private resolveReconnectDelay;
5882
5881
  static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
5883
5882
  static fromClient(client: MilkyClient, options?: ContextOptions): Context;
5884
5883
  }
@@ -5924,7 +5923,17 @@ declare namespace seg {
5924
5923
  }
5925
5924
  //#endregion
5926
5925
  export {
5926
+ AnyApiCall,
5927
+ AnyApiHook,
5928
+ AnyApiNext,
5929
+ ApiCall,
5930
+ ApiEndpointName,
5927
5931
  ApiEndpoints,
5932
+ ApiHook,
5933
+ ApiNext,
5934
+ ApiParams,
5935
+ ApiRequest,
5936
+ ApiResponse,
5928
5937
  Capturer,
5929
5938
  Command,
5930
5939
  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",
@@ -314,12 +314,16 @@ var Router = class Router {
314
314
  }
315
315
  match(session, message) {
316
316
  for (const branch of this.branchesFrom(session, [], { includeHidden: true })) {
317
- const descriptor = this.describeBranch(branch);
318
- const activations = this.activationResolver(descriptor, session) ?? DEFAULT_ACTIVATIONS;
319
- for (const activation of routeActivationInputs(activations)) {
317
+ const literalActivationIndex = branch.type === "rawPattern" ? Object.values(branch.rawPattern.pattern).findIndex((parameter) => parameter.capturer.typeInstruction.type === "literal") : -1;
318
+ let activationInputs = DEFAULT_ACTIVATIONS;
319
+ if (branch.type === "command" || literalActivationIndex !== -1) {
320
+ const descriptor = this.describeBranch(branch);
321
+ const activations = this.activationResolver(descriptor, session) ?? DEFAULT_ACTIVATIONS;
322
+ activationInputs = Array.isArray(activations) ? activations : [activations];
323
+ }
324
+ for (const activation of activationInputs) {
320
325
  const tokenizer = new Tokenizer(message.segments);
321
- if (!this.consumeActivation(tokenizer, activation, session)) continue;
322
- const match = this.matchBranch(branch, tokenizer, activation);
326
+ const match = this.matchBranch(branch, tokenizer, activation, session, literalActivationIndex);
323
327
  if (match !== void 0) return match;
324
328
  }
325
329
  }
@@ -366,11 +370,12 @@ var Router = class Router {
366
370
  case "prefix": return tokenizer.consumeTextPrefix(activation.prefix);
367
371
  }
368
372
  }
369
- matchBranch(branch, tokenizer, activation) {
373
+ matchBranch(branch, tokenizer, activation, session, literalActivationIndex) {
374
+ if (branch.type === "command" && !this.consumeActivation(tokenizer, activation, session)) return;
370
375
  if (!this.matchPath(branch.path, tokenizer)) return;
371
376
  switch (branch.type) {
372
377
  case "command": return this.matchCommand(branch.command, tokenizer, branch.path, activation);
373
- case "rawPattern": return this.matchRawPattern(branch.rawPattern, tokenizer, branch.path, activation);
378
+ case "rawPattern": return this.matchRawPattern(branch.rawPattern, tokenizer, branch.path, activation, session, literalActivationIndex);
374
379
  }
375
380
  }
376
381
  matchPath(path, tokenizer) {
@@ -395,9 +400,15 @@ var Router = class Router {
395
400
  activation
396
401
  };
397
402
  }
398
- matchRawPattern(rawPattern, tokenizer, path, activation) {
399
- const params = this.capturePattern(rawPattern.pattern, tokenizer);
400
- if (params === void 0 || tokenizer.hasNext()) return;
403
+ matchRawPattern(rawPattern, tokenizer, path, activation, session, literalActivationIndex) {
404
+ const params = {};
405
+ for (const [index, [name, parameter]] of Object.entries(rawPattern.pattern).entries()) {
406
+ if (index === literalActivationIndex && !this.consumeActivation(tokenizer, activation, session)) return;
407
+ const value = parameter.capturer.capture(tokenizer);
408
+ if (value === void 0) return;
409
+ params[name] = value;
410
+ }
411
+ if (tokenizer.hasNext()) return;
401
412
  return {
402
413
  type: "rawPattern",
403
414
  path: [...path],
@@ -465,7 +476,6 @@ var Router = class Router {
465
476
  validatePattern(pattern, options) {
466
477
  const entries = Object.entries(pattern);
467
478
  if (options?.rawPattern && entries.length === 0) throw new Error("Raw pattern must have at least one parameter.");
468
- if (options?.rawPattern && entries[0]?.[1].capturer.typeInstruction.type === "literal") throw new Error("The first parameter of a raw pattern cannot be a literal, as it would conflict with command patterns.");
469
479
  const catchAllEntryIndex = entries.findIndex(([, parameter]) => {
470
480
  return parameter.capturer.typeInstruction.type === "catchAll";
471
481
  });
@@ -485,12 +495,6 @@ var Router = class Router {
485
495
  return params;
486
496
  }
487
497
  };
488
- function routeActivationInputs(activations) {
489
- return isRouteActivationArray(activations) ? activations : [activations];
490
- }
491
- function isRouteActivationArray(activations) {
492
- return Array.isArray(activations);
493
- }
494
498
  //#endregion
495
499
  //#region src/core/activation.ts
496
500
  function createContextRouteActivationResolver(routing, fallback = defaultRouteActivationResolver) {
@@ -502,7 +506,8 @@ function createContextRouteActivationResolver(routing, fallback = defaultRouteAc
502
506
  }
503
507
  function compileContextRouteActivationConfig(config, fallback) {
504
508
  return (route, session) => {
505
- for (const rule of contextRouteActivationRules(config.rules)) {
509
+ const rules = config.rules === void 0 ? [] : Array.isArray(config.rules) ? config.rules : [config.rules];
510
+ for (const rule of rules) {
506
511
  if (!matchesContextRouteActivationRule(route, session, rule)) continue;
507
512
  const activation = resolveContextRouteActivation(rule.activation, session.raw.message_scene);
508
513
  if (activation !== void 0) return activation;
@@ -514,22 +519,10 @@ function compileContextRouteActivationConfig(config, fallback) {
514
519
  return fallback(route, session);
515
520
  };
516
521
  }
517
- function contextRouteActivationRules(rules) {
518
- if (rules === void 0) return [];
519
- return isContextRouteActivationRuleArray(rules) ? rules : [rules];
520
- }
521
- function isContextRouteActivationRuleArray(rules) {
522
- return Array.isArray(rules);
523
- }
524
522
  function resolveContextRouteActivation(activation, scene) {
525
- if (isRouteActivationInput(activation)) return activation;
526
- return activation[scene] ?? activation.default;
527
- }
528
- function isRouteActivationInput(activation) {
529
- return Array.isArray(activation) || isRouteActivation(activation);
530
- }
531
- function isRouteActivation(activation) {
532
- return "type" in activation;
523
+ if (Array.isArray(activation) || "type" in activation) return activation;
524
+ const activationByScene = activation;
525
+ return activationByScene[scene] ?? activationByScene.default;
533
526
  }
534
527
  function matchesContextRouteActivationRule(route, session, rule) {
535
528
  const match = rule.match;
@@ -808,190 +801,235 @@ function combineLogHandlers(...handlers) {
808
801
  };
809
802
  }
810
803
  //#endregion
811
- //#region src/core/service.ts
812
- function isDisposable(service) {
813
- return "dispose" in service && typeof service.dispose === "function";
814
- }
815
- function implementsESNextDisposable(service) {
816
- return Symbol.dispose in service && typeof service[Symbol.dispose] === "function";
817
- }
804
+ //#region src/core/context/api-hooks.ts
805
+ var ApiHookRegistry = class {
806
+ baseClient;
807
+ parent;
808
+ contextName;
809
+ getState;
810
+ client;
811
+ entries = [];
812
+ constructor(baseClient, parent, contextName, getState) {
813
+ this.baseClient = baseClient;
814
+ this.parent = parent;
815
+ this.contextName = contextName;
816
+ this.getState = getState;
817
+ this.client = this.createHookClient();
818
+ }
819
+ register(endpointOrHook, hook) {
820
+ const state = this.getState();
821
+ if (state === "stopping") throw new Error(`Context "${this.contextName}" cannot register API hooks while it is stopping.`);
822
+ if (state === "stopped") throw new Error(`Context "${this.contextName}" cannot register API hooks after it has stopped.`);
823
+ let entry;
824
+ if (typeof endpointOrHook === "function") entry = { hook: (params, next, call) => {
825
+ return endpointOrHook(call, (nextParams = params) => next(nextParams));
826
+ } };
827
+ else {
828
+ if (!hook) throw new Error(`API hook for endpoint ${endpointOrHook} is missing a handler.`);
829
+ entry = {
830
+ endpoint: endpointOrHook,
831
+ hook
832
+ };
833
+ }
834
+ this.entries.push(entry);
835
+ let disposed = false;
836
+ return () => {
837
+ if (disposed) return;
838
+ disposed = true;
839
+ const index = this.entries.indexOf(entry);
840
+ if (index !== -1) this.entries.splice(index, 1);
841
+ };
842
+ }
843
+ clear() {
844
+ this.entries.length = 0;
845
+ }
846
+ createHookClient() {
847
+ return new Proxy(this.baseClient, { get: (target, prop, receiver) => {
848
+ if (typeof prop === "string" && prop.includes("_")) return (params) => this.callHookedApi(prop, params);
849
+ return Reflect.get(target, prop, receiver);
850
+ } });
851
+ }
852
+ async callHookedApi(endpoint, params) {
853
+ const hooks = this.collectApiHooks(endpoint);
854
+ const dispatch = async (index, currentParams) => {
855
+ const hook = hooks[index];
856
+ if (!hook) return await this.callBaseApi(endpoint, currentParams);
857
+ let nextCalled = false;
858
+ return await hook(currentParams, async (nextParams = currentParams) => {
859
+ if (nextCalled) throw new Error(`API hook for endpoint ${endpoint} called next() multiple times.`);
860
+ nextCalled = true;
861
+ return await dispatch(index + 1, nextParams);
862
+ }, {
863
+ endpoint,
864
+ params: currentParams
865
+ });
866
+ };
867
+ return await dispatch(0, params);
868
+ }
869
+ collectApiHooks(endpoint) {
870
+ const hooks = [];
871
+ let registry = this;
872
+ while (registry) {
873
+ for (const entry of registry.entries.toReversed()) if (entry.endpoint === void 0 || entry.endpoint === endpoint) hooks.push(entry.hook);
874
+ registry = registry.parent;
875
+ }
876
+ return hooks;
877
+ }
878
+ async callBaseApi(endpoint, params) {
879
+ const callApi = this.baseClient.callApi;
880
+ if (typeof callApi === "function") return await callApi.call(this.baseClient, endpoint, params);
881
+ const method = this.baseClient[endpoint];
882
+ if (typeof method !== "function") throw new Error(`Milky client does not implement API endpoint ${endpoint}.`);
883
+ return await method.call(this.baseClient, params);
884
+ }
885
+ };
818
886
  //#endregion
819
- //#region src/core/context.ts
887
+ //#region src/core/context/event-sources.ts
820
888
  const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
821
889
  const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
822
- var Context = class Context {
823
- client;
824
- router = new Router();
890
+ var EventSourceRegistry = class {
825
891
  logger;
826
- name;
827
- routeActivationResolver;
828
- parent;
829
- filter;
830
- eventBus = mitt();
831
- plugins = [];
832
- services = /* @__PURE__ */ new Map();
833
- subContexts = /* @__PURE__ */ new Map();
834
- eventSourceRuntimes = /* @__PURE__ */ new Map();
835
- parentEventForwarder;
836
- timers = /* @__PURE__ */ new Set();
892
+ getState;
893
+ emit;
894
+ runtimes = /* @__PURE__ */ new Map();
837
895
  initialReconnectDelayMs;
838
896
  maxReconnectDelayMs;
839
- logHandler;
840
- state = "idle";
841
- startPromise;
842
- stopPromise;
843
- constructor(client, options, name, parent, filter) {
844
- this.client = client;
845
- this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
846
- this.maxReconnectDelayMs = options?.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
847
- this.logHandler = options?.logHandler ?? parent?.logHandler;
848
- this.name = name ?? "root";
849
- this.logger = new Logger((message) => this.logHandler?.(message), `context:${this.name}`);
850
- this.routeActivationResolver = createContextRouteActivationResolver(options?.routing, parent?.routeActivationResolver);
851
- this.router.setActivationResolver(this.routeActivationResolver);
852
- this.parent = parent;
853
- this.filter = filter;
854
- if (parent) {
855
- this.parentEventForwarder = (type, event) => {
856
- if (!this.acceptsParentEvent(type, event)) return;
857
- this.eventBus.emit(type, event);
858
- };
859
- parent.eventBus.on("*", this.parentEventForwarder);
897
+ constructor(options, logger, getState, emit) {
898
+ this.logger = logger;
899
+ this.getState = getState;
900
+ this.emit = emit;
901
+ this.initialReconnectDelayMs = options?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
902
+ this.maxReconnectDelayMs = options?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
903
+ }
904
+ install(eventSource) {
905
+ if (this.runtimes.has(eventSource)) return;
906
+ this.runtimes.set(eventSource, {});
907
+ if (this.getState() === "started") this.startEventSource(eventSource);
908
+ }
909
+ startAll() {
910
+ for (const eventSource of this.runtimes.keys()) this.startEventSource(eventSource);
911
+ }
912
+ async stop() {
913
+ const errors = [];
914
+ for (const runtime of this.runtimes.values()) {
915
+ this.resolveReconnectDelay(runtime);
916
+ const subscription = runtime.subscription;
917
+ if (!subscription) continue;
918
+ try {
919
+ await subscription.stop();
920
+ } catch (error) {
921
+ errors.push(error);
922
+ }
923
+ runtime.subscription = void 0;
860
924
  }
861
- this.eventBus.on("message_receive", async ({ self_id, data: message }) => {
925
+ for (const runtime of this.runtimes.values()) {
926
+ if (!runtime.task) continue;
862
927
  try {
863
- if (this.state === "stopping" || this.state === "stopped") return;
864
- await this.router.dispatch(this.createSession(self_id, message), message);
928
+ await runtime.task;
865
929
  } catch (error) {
866
- this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
930
+ errors.push(error);
931
+ } finally {
932
+ runtime.task = void 0;
867
933
  }
868
- });
934
+ }
935
+ return errors;
869
936
  }
870
- on(type, handler) {
871
- const wrappedHandler = async (event) => {
937
+ startEventSource(eventSource) {
938
+ const runtime = this.runtimes.get(eventSource);
939
+ if (!runtime) return;
940
+ runtime.task = this.runEventSource(eventSource, runtime);
941
+ }
942
+ async runEventSource(eventSource, runtime) {
943
+ let reconnectDelay = this.initialReconnectDelayMs;
944
+ let reconnectAttempt = 1;
945
+ while (this.getState() === "started") {
872
946
  try {
873
- if (this.state === "stopping" || this.state === "stopped") return;
874
- await handler(event);
947
+ this.logger.debug(`Connecting ${eventSource.name ?? "event source"} (attempt=${reconnectAttempt})`);
948
+ const subscription = await eventSource.start((event) => {
949
+ try {
950
+ if (this.getState() !== "started") return;
951
+ this.emit(event);
952
+ } catch (error) {
953
+ this.logger.error("Error handling event stream event", error);
954
+ }
955
+ });
956
+ if (this.getState() !== "started") {
957
+ await subscription.stop();
958
+ break;
959
+ }
960
+ runtime.subscription = subscription;
961
+ this.logger.info(`${eventSource.name ?? "Event source"} connected`);
962
+ reconnectDelay = this.initialReconnectDelayMs;
963
+ reconnectAttempt = 1;
964
+ await subscription.closed;
965
+ if (runtime.subscription === subscription) runtime.subscription = void 0;
966
+ if (this.getState() !== "started") break;
967
+ this.logger.warn(`${eventSource.name ?? "Event source"} disconnected; reconnecting in ${reconnectDelay}ms`);
875
968
  } catch (error) {
876
- this.logger.error(`Error handling event ${type}`, error);
969
+ if (this.getState() !== "started") break;
970
+ this.logger.error(`Error connecting ${eventSource.name ?? "event source"}; reconnecting in ${reconnectDelay}ms`, error);
877
971
  }
878
- };
879
- this.eventBus.on(type, wrappedHandler);
880
- return () => {
881
- this.eventBus.off(type, wrappedHandler);
882
- };
972
+ await this.waitForReconnectDelay(runtime, reconnectDelay);
973
+ reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
974
+ reconnectAttempt += 1;
975
+ }
883
976
  }
884
- install(plugin, ...args) {
885
- this.plugins.push({
886
- plugin,
887
- args
977
+ waitForReconnectDelay(runtime, delay) {
978
+ return new Promise((resolve) => {
979
+ runtime.resolveReconnectTimer = resolve;
980
+ runtime.reconnectTimer = setTimeout(() => {
981
+ runtime.reconnectTimer = void 0;
982
+ runtime.resolveReconnectTimer = void 0;
983
+ resolve();
984
+ }, delay);
888
985
  });
889
986
  }
890
- installEventSource(eventSource) {
891
- if (this.eventSourceRuntimes.has(eventSource)) return;
892
- this.eventSourceRuntimes.set(eventSource, {});
893
- if (this.state === "started") this.startEventSource(eventSource);
894
- }
895
- provide(service, instance) {
896
- if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
897
- if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
898
- Service ${service.name} implements ESNext Disposable but not Fraq Disposable.
899
- Please explicitly import the interface like this:
900
-
901
- import type { Disposable } from '@fraqjs/fraq';
902
-
903
- and implement the dispose method to clean up resources when the context stops.
904
- `.trim());
905
- this.services.set(service, instance);
906
- }
907
- resolve(service) {
908
- const instance = this.tryResolve(service);
909
- if (instance === void 0) throw new Error(`Service ${service.name} has not been provided.`);
910
- return instance;
911
- }
912
- tryResolve(service) {
913
- if (this.services.has(service)) return this.services.get(service);
914
- return this.parent?.tryResolve(service);
915
- }
916
- isProvided(service) {
917
- return this.tryResolve(service) !== void 0;
918
- }
919
- fork(name, filter) {
920
- if (this.subContexts.has(name)) {
921
- 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
- return this.subContexts.get(name);
987
+ resolveReconnectDelay(runtime) {
988
+ if (runtime.reconnectTimer) {
989
+ clearTimeout(runtime.reconnectTimer);
990
+ runtime.reconnectTimer = void 0;
923
991
  }
924
- const subContext = new Context(this.client, void 0, name, this, filter);
925
- this.subContexts.set(name, subContext);
926
- return subContext;
992
+ const resolve = runtime.resolveReconnectTimer;
993
+ runtime.resolveReconnectTimer = void 0;
994
+ resolve?.();
927
995
  }
928
- timeout(delayMs, callback) {
929
- this.assertCanScheduleTimer();
930
- const timeout = setTimeout(() => {
931
- this.timers.delete(timeout);
932
- this.runTimerCallback(callback);
933
- }, delayMs);
934
- this.timers.add(timeout);
935
- return timeout;
996
+ };
997
+ //#endregion
998
+ //#region src/core/context/lifecycle.ts
999
+ var LifecycleManager = class {
1000
+ contextName;
1001
+ plugins;
1002
+ timers;
1003
+ eventSources;
1004
+ apiHooks;
1005
+ detachParentEvents;
1006
+ children = /* @__PURE__ */ new Set();
1007
+ currentState = "idle";
1008
+ startPromise;
1009
+ stopPromise;
1010
+ constructor(contextName, plugins, timers, eventSources, apiHooks, detachParentEvents) {
1011
+ this.contextName = contextName;
1012
+ this.plugins = plugins;
1013
+ this.timers = timers;
1014
+ this.eventSources = eventSources;
1015
+ this.apiHooks = apiHooks;
1016
+ this.detachParentEvents = detachParentEvents;
936
1017
  }
937
- interval(intervalMs, callback) {
938
- this.assertCanScheduleTimer();
939
- const interval = setInterval(() => {
940
- this.runTimerCallback(callback);
941
- }, intervalMs);
942
- this.timers.add(interval);
943
- return interval;
1018
+ get state() {
1019
+ return this.currentState;
944
1020
  }
945
- createSession(selfId, message) {
946
- return {
947
- selfId,
948
- raw: message,
949
- reply: async (textOrSegments, options) => {
950
- const actualSegments = [];
951
- if (typeof textOrSegments === "string") actualSegments.push({
952
- type: "text",
953
- data: { text: textOrSegments }
954
- });
955
- else actualSegments.push(...textOrSegments);
956
- if (options?.withMention && message.message_scene === "group") actualSegments.unshift(seg.mention(message.sender_id));
957
- if (options?.withQuote) actualSegments.unshift(seg.reply(message.message_seq));
958
- switch (message.message_scene) {
959
- case "friend": {
960
- const { message_seq } = await this.client.send_private_message({
961
- user_id: message.peer_id,
962
- message: actualSegments
963
- });
964
- return { messageSeq: message_seq };
965
- }
966
- case "group": {
967
- const { message_seq } = await this.client.send_group_message({
968
- group_id: message.peer_id,
969
- message: actualSegments
970
- });
971
- return { messageSeq: message_seq };
972
- }
973
- }
974
- return { messageSeq: 0 };
975
- },
976
- reaction: async (type, reactionId) => {
977
- if (message.message_scene === "group") await this.client.send_group_message_reaction({
978
- group_id: message.peer_id,
979
- message_seq: message.message_seq,
980
- reaction_type: type,
981
- reaction: reactionId
982
- });
983
- }
984
- };
1021
+ addChild(child) {
1022
+ this.children.add(child);
985
1023
  }
986
1024
  async start() {
987
- if (this.state === "started") return;
988
- if (this.state === "starting") {
1025
+ if (this.currentState === "started") return;
1026
+ if (this.currentState === "starting") {
989
1027
  await this.startPromise;
990
1028
  return;
991
1029
  }
992
- if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot be started while it is stopping.`);
993
- if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot be restarted after it has been stopped.`);
994
- this.state = "starting";
1030
+ if (this.currentState === "stopping") throw new Error(`Context "${this.contextName}" cannot be started while it is stopping.`);
1031
+ if (this.currentState === "stopped") throw new Error(`Context "${this.contextName}" cannot be restarted after it has been stopped.`);
1032
+ this.currentState = "starting";
995
1033
  this.startPromise = this.startInternal();
996
1034
  try {
997
1035
  await this.startPromise;
@@ -1000,20 +1038,23 @@ and implement the dispose method to clean up resources when the context stops.
1000
1038
  }
1001
1039
  }
1002
1040
  async stop() {
1003
- if (this.state === "idle" && this.timers.size === 0 || this.state === "stopped") return;
1004
- if (this.state === "starting") await this.startPromise;
1005
- const stateAfterStart = this.getState();
1006
- if (stateAfterStart === "idle" && this.timers.size === 0 || stateAfterStart === "stopped") return;
1041
+ if (this.currentState === "idle" && !this.timers.hasTimers || this.currentState === "stopped") return;
1042
+ let stateAfterStart = this.currentState;
1043
+ if (stateAfterStart === "starting") {
1044
+ await this.startPromise;
1045
+ stateAfterStart = this.currentState;
1046
+ }
1047
+ if (stateAfterStart === "idle" && !this.timers.hasTimers || stateAfterStart === "stopped") return;
1007
1048
  if (stateAfterStart === "stopping") {
1008
1049
  await this.stopPromise;
1009
1050
  return;
1010
1051
  }
1011
- this.state = "stopping";
1052
+ this.currentState = "stopping";
1012
1053
  this.stopPromise = this.stopInternal();
1013
1054
  try {
1014
1055
  await this.stopPromise;
1015
1056
  } finally {
1016
- this.state = "stopped";
1057
+ this.currentState = "stopped";
1017
1058
  this.stopPromise = void 0;
1018
1059
  }
1019
1060
  }
@@ -1022,85 +1063,128 @@ and implement the dispose method to clean up resources when the context stops.
1022
1063
  const startingContexts = [];
1023
1064
  try {
1024
1065
  await this.recursiveApplyPlugins(appliedContextPlugins, startingContexts);
1025
- await this.recursiveStartPlugins(appliedContextPlugins);
1066
+ for (const { lifecycle, sortedPlugins } of appliedContextPlugins) await lifecycle.plugins.start(sortedPlugins);
1026
1067
  } catch (error) {
1027
- for (const context of startingContexts) if (context.state === "starting") context.state = "idle";
1068
+ for (const lifecycle of startingContexts) if (lifecycle.currentState === "starting") lifecycle.currentState = "idle";
1028
1069
  throw error;
1029
1070
  }
1030
- for (const { context } of appliedContextPlugins) {
1031
- context.state = "started";
1032
- for (const eventSource of context.eventSourceRuntimes.keys()) context.startEventSource(eventSource);
1071
+ for (const { lifecycle } of appliedContextPlugins) {
1072
+ lifecycle.currentState = "started";
1073
+ lifecycle.eventSources.startAll();
1033
1074
  }
1034
1075
  }
1035
- getState() {
1036
- return this.state;
1037
- }
1038
1076
  async stopInternal() {
1039
1077
  const errors = [];
1040
- this.clearTimers();
1041
- for (const subContext of [...this.subContexts.values()].reverse()) try {
1042
- await subContext.stop();
1078
+ this.timers.clear();
1079
+ for (const child of [...this.children].reverse()) try {
1080
+ await child.stop();
1043
1081
  } catch (error) {
1044
1082
  errors.push(error);
1045
1083
  }
1046
- await this.stopEventSources(errors);
1047
- if (this.parent && this.parentEventForwarder) this.parent.eventBus.off("*", this.parentEventForwarder);
1048
- for (const service of [...this.services.values()].reverse()) {
1049
- if (!isDisposable(service)) continue;
1050
- try {
1051
- await service.dispose();
1052
- } catch (error) {
1053
- errors.push(error);
1054
- }
1055
- }
1084
+ errors.push(...await this.eventSources.stop());
1085
+ this.detachParentEvents();
1086
+ errors.push(...await this.plugins.disposeServices());
1087
+ this.apiHooks.clear();
1056
1088
  if (errors.length === 1) throw errors[0];
1057
- if (errors.length > 1) throw new AggregateError(errors, `Context "${this.name}" failed to stop cleanly.`);
1058
- }
1059
- assertCanScheduleTimer() {
1060
- if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot schedule timers while it is stopping.`);
1061
- if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot schedule timers after it has stopped.`);
1089
+ if (errors.length > 1) throw new AggregateError(errors, `Context "${this.contextName}" failed to stop cleanly.`);
1062
1090
  }
1063
- async runTimerCallback(callback) {
1064
- if (this.state === "stopping" || this.state === "stopped") return;
1065
- try {
1066
- await callback();
1067
- } catch (error) {
1068
- this.logger.error("Error handling timer callback", error);
1091
+ async recursiveApplyPlugins(appliedContextPlugins, startingContexts) {
1092
+ if (this.currentState === "started") return;
1093
+ if (this.currentState === "starting" && this.startPromise) {
1094
+ await this.startPromise;
1095
+ return;
1069
1096
  }
1097
+ if (this.currentState === "stopping") throw new Error(`Context "${this.contextName}" cannot be started while it is stopping.`);
1098
+ if (this.currentState === "stopped") throw new Error(`Context "${this.contextName}" cannot be restarted after it has been stopped.`);
1099
+ if (this.currentState === "idle") this.currentState = "starting";
1100
+ startingContexts.push(this);
1101
+ const sortedPlugins = await this.plugins.apply();
1102
+ appliedContextPlugins.push({
1103
+ lifecycle: this,
1104
+ sortedPlugins
1105
+ });
1106
+ for (const child of this.children) await child.recursiveApplyPlugins(appliedContextPlugins, startingContexts);
1070
1107
  }
1071
- clearTimers() {
1072
- for (const timer of this.timers) clearTimeout(timer);
1073
- this.timers.clear();
1108
+ };
1109
+ //#endregion
1110
+ //#region src/core/service.ts
1111
+ function isDisposable(service) {
1112
+ return "dispose" in service && typeof service.dispose === "function";
1113
+ }
1114
+ function implementsESNextDisposable(service) {
1115
+ return Symbol.dispose in service && typeof service[Symbol.dispose] === "function";
1116
+ }
1117
+ //#endregion
1118
+ //#region src/core/context/plugins.ts
1119
+ function areRequiredServicesAvailable(plugin, available) {
1120
+ return (plugin.requires ?? []).every((service) => available.has(service));
1121
+ }
1122
+ function areOptionalServicesReady(plugin, available, pendingProviders) {
1123
+ return (plugin.optionalRequires ?? []).every((service) => {
1124
+ if (available.has(service)) return true;
1125
+ const pendingProvider = pendingProviders.get(service);
1126
+ return pendingProvider === void 0 || pendingProvider.plugin === plugin;
1127
+ });
1128
+ }
1129
+ function createUnresolvablePluginError(pending, available) {
1130
+ const missingRequirements = /* @__PURE__ */ new Map();
1131
+ const pendingProviders = /* @__PURE__ */ new Set();
1132
+ for (const { plugin } of pending) for (const service of plugin.provides ?? []) pendingProviders.add(service);
1133
+ for (const { plugin } of pending) for (const service of plugin.requires ?? []) if (!available.has(service)) {
1134
+ const plugins = missingRequirements.get(service) ?? [];
1135
+ plugins.push(plugin);
1136
+ missingRequirements.set(service, plugins);
1137
+ }
1138
+ const lines = [...missingRequirements].map(([service, plugins]) => {
1139
+ const dependents = plugins.map((plugin) => plugin.name).join(", ");
1140
+ const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
1141
+ return `${service.name} required by ${dependents} (${reason})`;
1142
+ });
1143
+ return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
1144
+ }
1145
+ var PluginRegistry = class {
1146
+ context;
1147
+ parent;
1148
+ logHandler;
1149
+ plugins = [];
1150
+ services = /* @__PURE__ */ new Map();
1151
+ constructor(context, parent, logHandler) {
1152
+ this.context = context;
1153
+ this.parent = parent;
1154
+ this.logHandler = logHandler;
1074
1155
  }
1075
- async stopEventSources(errors) {
1076
- for (const [_, runtime] of this.eventSourceRuntimes) {
1077
- this.resolveReconnectDelay(runtime);
1078
- const subscription = runtime.subscription;
1079
- if (!subscription) continue;
1080
- try {
1081
- await subscription.stop();
1082
- } catch (error) {
1083
- errors.push(error);
1084
- }
1085
- runtime.subscription = void 0;
1086
- }
1087
- for (const runtime of this.eventSourceRuntimes.values()) {
1088
- if (!runtime.task) continue;
1089
- try {
1090
- await runtime.task;
1091
- } catch (error) {
1092
- errors.push(error);
1093
- } finally {
1094
- runtime.task = void 0;
1095
- }
1096
- }
1156
+ install(plugin, ...args) {
1157
+ this.plugins.push({
1158
+ plugin,
1159
+ args
1160
+ });
1097
1161
  }
1098
- acceptsParentEvent(type, event) {
1099
- if (!this.filter) return true;
1100
- const predicate = this.filter[type];
1101
- return predicate?.(event) === true;
1162
+ provide(service, instance) {
1163
+ if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
1164
+ if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
1165
+ Service ${service.name} implements ESNext Disposable but not Fraq Disposable.
1166
+ Please explicitly import the interface like this:
1167
+
1168
+ import type { Disposable } from '@fraqjs/fraq';
1169
+
1170
+ and implement the dispose method to clean up resources when the context stops.
1171
+ `.trim());
1172
+ this.services.set(service, instance);
1173
+ }
1174
+ resolve(service) {
1175
+ const instance = this.tryResolve(service);
1176
+ if (instance === void 0) throw new Error(`Service ${service.name} has not been provided.`);
1177
+ return instance;
1178
+ }
1179
+ tryResolve(service) {
1180
+ if (this.services.has(service)) return this.services.get(service);
1181
+ return this.parent?.tryResolve(service);
1102
1182
  }
1103
- async applyPlugins(sortedPlugins) {
1183
+ isProvided(service) {
1184
+ return this.tryResolve(service) !== void 0;
1185
+ }
1186
+ async apply() {
1187
+ const sortedPlugins = this.sortPlugins();
1104
1188
  for (const installedPlugin of sortedPlugins) {
1105
1189
  const { plugin, args } = installedPlugin;
1106
1190
  const providedBeforeApply = new Set(this.services.keys());
@@ -1112,39 +1196,31 @@ and implement the dispose method to clean up resources when the context stops.
1112
1196
  if (plugin.provides) for (const service of plugin.provides) providedServices.push(service.name);
1113
1197
  if (requiredServices.length > 0) applyingMessage += `, requires: [${requiredServices.join(", ")}]`;
1114
1198
  if (providedServices.length > 0) applyingMessage += `, provides: [${providedServices.join(", ")}]`;
1115
- this.logger.info(applyingMessage);
1199
+ this.context.logger.info(applyingMessage);
1116
1200
  await plugin.apply(this.getPluginContext(installedPlugin), ...args);
1117
1201
  for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${plugin.name} declares service ${service.name} but did not provide it.`);
1118
1202
  }
1203
+ return sortedPlugins;
1119
1204
  }
1120
- async recursiveApplyPlugins(appliedContextPlugins, startingContexts) {
1121
- if (this.state === "started") return;
1122
- if (this.state === "starting" && this.startPromise) {
1123
- await this.startPromise;
1124
- return;
1125
- }
1126
- if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot be started while it is stopping.`);
1127
- if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot be restarted after it has been stopped.`);
1128
- if (this.state === "idle") this.state = "starting";
1129
- startingContexts.push(this);
1130
- const sortedPlugins = this.sortPlugins();
1131
- await this.applyPlugins(sortedPlugins);
1132
- appliedContextPlugins.push({
1133
- context: this,
1134
- sortedPlugins
1135
- });
1136
- for (const subContext of this.subContexts.values()) await subContext.recursiveApplyPlugins(appliedContextPlugins, startingContexts);
1137
- }
1138
- async startPlugins(sortedPlugins) {
1205
+ async start(sortedPlugins) {
1139
1206
  for (const installedPlugin of sortedPlugins) {
1140
1207
  const { plugin } = installedPlugin;
1141
1208
  if (!plugin.start) continue;
1142
- this.logger.debug(`Plugin ${plugin.name} is starting...`);
1209
+ this.context.logger.debug(`Plugin ${plugin.name} is starting...`);
1143
1210
  await plugin.start(this.getPluginContext(installedPlugin));
1144
1211
  }
1145
1212
  }
1146
- async recursiveStartPlugins(appliedContextPlugins) {
1147
- for (const { context, sortedPlugins } of appliedContextPlugins) await context.startPlugins(sortedPlugins);
1213
+ async disposeServices() {
1214
+ const errors = [];
1215
+ for (const service of [...this.services.values()].reverse()) {
1216
+ if (!isDisposable(service)) continue;
1217
+ try {
1218
+ await service.dispose();
1219
+ } catch (error) {
1220
+ errors.push(error);
1221
+ }
1222
+ }
1223
+ return errors;
1148
1224
  }
1149
1225
  sortPlugins() {
1150
1226
  const providers = /* @__PURE__ */ new Map();
@@ -1155,64 +1231,33 @@ and implement the dispose method to clean up resources when the context stops.
1155
1231
  }
1156
1232
  const pending = [...this.plugins];
1157
1233
  const sorted = [];
1158
- const available = /* @__PURE__ */ new Set();
1159
- for (const service of this.collectAvailableServices()) available.add(service);
1234
+ const available = new Set(this.collectAvailableServices());
1160
1235
  while (pending.length > 0) {
1161
- const availableByPendingPlugins = this.collectPendingProvidedServices(pending);
1162
- const requiredReadyIndex = pending.findIndex(({ plugin }) => this.areRequiredServicesAvailable(plugin, available));
1163
- const optionalReadyIndex = pending.findIndex(({ plugin }) => this.areRequiredServicesAvailable(plugin, available) && this.areOptionalServicesReady(plugin, available, availableByPendingPlugins));
1236
+ const availableByPendingPlugins = /* @__PURE__ */ new Map();
1237
+ for (const installedPlugin of pending) for (const service of installedPlugin.plugin.provides ?? []) availableByPendingPlugins.set(service, installedPlugin);
1238
+ const requiredReadyIndex = pending.findIndex(({ plugin }) => areRequiredServicesAvailable(plugin, available));
1239
+ const optionalReadyIndex = pending.findIndex(({ plugin }) => areRequiredServicesAvailable(plugin, available) && areOptionalServicesReady(plugin, available, availableByPendingPlugins));
1164
1240
  const nextIndex = optionalReadyIndex === -1 ? requiredReadyIndex : optionalReadyIndex;
1165
- if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
1241
+ if (nextIndex === -1) throw createUnresolvablePluginError(pending, available);
1166
1242
  const [next] = pending.splice(nextIndex, 1);
1167
1243
  sorted.push(next);
1168
1244
  for (const service of next.plugin.provides ?? []) available.add(service);
1169
1245
  }
1170
1246
  return sorted;
1171
1247
  }
1172
- collectPendingProvidedServices(pending) {
1173
- const providedServices = /* @__PURE__ */ new Map();
1174
- for (const installedPlugin of pending) for (const service of installedPlugin.plugin.provides ?? []) providedServices.set(service, installedPlugin);
1175
- return providedServices;
1176
- }
1177
- areRequiredServicesAvailable(plugin, available) {
1178
- return (plugin.requires ?? []).every((service) => available.has(service));
1179
- }
1180
- areOptionalServicesReady(plugin, available, pendingProviders) {
1181
- return (plugin.optionalRequires ?? []).every((service) => {
1182
- if (available.has(service)) return true;
1183
- const pendingProvider = pendingProviders.get(service);
1184
- return pendingProvider === void 0 || pendingProvider.plugin === plugin;
1185
- });
1186
- }
1187
1248
  collectAvailableServices() {
1188
1249
  const services = [...this.services.keys()];
1189
1250
  if (this.parent) services.push(...this.parent.collectAvailableServices());
1190
1251
  return services;
1191
1252
  }
1192
- createUnresolvablePluginError(pending, available) {
1193
- const missingRequirements = /* @__PURE__ */ new Map();
1194
- const pendingProviders = /* @__PURE__ */ new Set();
1195
- for (const { plugin } of pending) for (const service of plugin.provides ?? []) pendingProviders.add(service);
1196
- for (const { plugin } of pending) for (const service of plugin.requires ?? []) if (!available.has(service)) {
1197
- const plugins = missingRequirements.get(service) ?? [];
1198
- plugins.push(plugin);
1199
- missingRequirements.set(service, plugins);
1200
- }
1201
- const lines = [...missingRequirements].map(([service, plugins]) => {
1202
- const dependents = plugins.map((plugin) => plugin.name).join(", ");
1203
- const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
1204
- return `${service.name} required by ${dependents} (${reason})`;
1205
- });
1206
- return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
1207
- }
1208
1253
  getPluginContext(installedPlugin) {
1209
1254
  installedPlugin.proxy ??= this.createProxyContextForPlugin(installedPlugin.plugin);
1210
1255
  return installedPlugin.proxy;
1211
1256
  }
1212
1257
  createProxyContextForPlugin(plugin) {
1213
- const proxyLogger = new Logger((message) => this.logHandler?.(message), `plugin:${this.name ? `${this.name}/` : ""}${plugin.name}`);
1214
- const proxyRouter = this.router.withMeta({
1215
- context: this.name,
1258
+ const proxyLogger = new Logger((message) => this.logHandler?.(message), `plugin:${this.context.name ? `${this.context.name}/` : ""}${plugin.name}`);
1259
+ const proxyRouter = this.context.router.withMeta({
1260
+ context: this.context.name,
1216
1261
  plugin: plugin.name
1217
1262
  });
1218
1263
  let proxyInjections;
@@ -1224,74 +1269,225 @@ and implement the dispose method to clean up resources when the context stops.
1224
1269
  proxyInjections ??= {};
1225
1270
  for (const [key, service] of Object.entries(plugin.optionalInject)) proxyInjections[key] = this.tryResolve(service);
1226
1271
  }
1227
- return new Proxy(this, { get(target, prop, receiver) {
1272
+ return new Proxy(this.context, { get(target, prop, receiver) {
1228
1273
  if (prop === "logger") return proxyLogger;
1229
- else if (prop === "router") return proxyRouter;
1230
- else if (proxyInjections && prop in proxyInjections) return proxyInjections[prop];
1231
- else return Reflect.get(target, prop, receiver);
1274
+ if (prop === "router") return proxyRouter;
1275
+ if (proxyInjections && prop in proxyInjections) return proxyInjections[prop];
1276
+ return Reflect.get(target, prop, receiver);
1232
1277
  } });
1233
1278
  }
1234
- startEventSource(eventSource) {
1235
- const runtime = this.eventSourceRuntimes.get(eventSource);
1236
- if (!runtime) return;
1237
- runtime.task = this.runEventSource(eventSource, runtime);
1279
+ };
1280
+ //#endregion
1281
+ //#region src/core/context/timers.ts
1282
+ var TimerRegistry = class {
1283
+ contextName;
1284
+ logger;
1285
+ getState;
1286
+ timers = /* @__PURE__ */ new Set();
1287
+ constructor(contextName, logger, getState) {
1288
+ this.contextName = contextName;
1289
+ this.logger = logger;
1290
+ this.getState = getState;
1238
1291
  }
1239
- async runEventSource(eventSource, runtime) {
1240
- let reconnectDelay = this.initialReconnectDelayMs;
1241
- let reconnectAttempt = 1;
1242
- while (this.state === "started") {
1243
- try {
1244
- this.logger.debug(`Connecting ${eventSource.name ?? "event source"} (attempt=${reconnectAttempt})`);
1245
- const subscription = await eventSource.start((event) => {
1246
- try {
1247
- if (this.state !== "started") return;
1248
- this.eventBus.emit(event.event_type, event);
1249
- } catch (error) {
1250
- this.logger.error("Error handling event stream event", error);
1251
- }
1252
- });
1253
- if (this.state !== "started") {
1254
- await subscription.stop();
1255
- break;
1292
+ get hasTimers() {
1293
+ return this.timers.size > 0;
1294
+ }
1295
+ timeout(delayMs, callback) {
1296
+ this.assertCanScheduleTimer();
1297
+ const timeout = setTimeout(() => {
1298
+ this.timers.delete(timeout);
1299
+ this.runTimerCallback(callback);
1300
+ }, delayMs);
1301
+ this.timers.add(timeout);
1302
+ return timeout;
1303
+ }
1304
+ interval(intervalMs, callback) {
1305
+ this.assertCanScheduleTimer();
1306
+ const interval = setInterval(() => {
1307
+ this.runTimerCallback(callback);
1308
+ }, intervalMs);
1309
+ this.timers.add(interval);
1310
+ return interval;
1311
+ }
1312
+ clear() {
1313
+ for (const timer of this.timers) clearTimeout(timer);
1314
+ this.timers.clear();
1315
+ }
1316
+ assertCanScheduleTimer() {
1317
+ const state = this.getState();
1318
+ if (state === "stopping") throw new Error(`Context "${this.contextName}" cannot schedule timers while it is stopping.`);
1319
+ if (state === "stopped") throw new Error(`Context "${this.contextName}" cannot schedule timers after it has stopped.`);
1320
+ }
1321
+ async runTimerCallback(callback) {
1322
+ const state = this.getState();
1323
+ if (state === "stopping" || state === "stopped") return;
1324
+ try {
1325
+ await callback();
1326
+ } catch (error) {
1327
+ this.logger.error("Error handling timer callback", error);
1328
+ }
1329
+ }
1330
+ };
1331
+ //#endregion
1332
+ //#region src/core/context/index.ts
1333
+ var Context = class Context {
1334
+ router = new Router();
1335
+ logger;
1336
+ name;
1337
+ routeActivationResolver;
1338
+ client;
1339
+ baseClient;
1340
+ parent;
1341
+ filter;
1342
+ eventBus = mitt();
1343
+ subContexts = /* @__PURE__ */ new Map();
1344
+ parentEventForwarder;
1345
+ logHandler;
1346
+ plugins;
1347
+ apiHooks;
1348
+ eventSources;
1349
+ timers;
1350
+ lifecycle;
1351
+ constructor(baseClient, options, name, parent, filter) {
1352
+ this.baseClient = baseClient;
1353
+ this.logHandler = options?.logHandler ?? parent?.logHandler;
1354
+ this.name = name ?? "root";
1355
+ this.logger = new Logger((message) => this.logHandler?.(message), `context:${this.name}`);
1356
+ this.routeActivationResolver = createContextRouteActivationResolver(options?.routing, parent?.routeActivationResolver);
1357
+ this.router.setActivationResolver(this.routeActivationResolver);
1358
+ this.parent = parent;
1359
+ this.filter = filter;
1360
+ if (parent) {
1361
+ this.parentEventForwarder = (type, event) => {
1362
+ if (this.filter) {
1363
+ const predicate = this.filter[type];
1364
+ if (predicate?.(event) !== true) return;
1256
1365
  }
1257
- runtime.subscription = subscription;
1258
- this.logger.info(`${eventSource.name ?? "Event source"} connected`);
1259
- reconnectDelay = this.initialReconnectDelayMs;
1260
- reconnectAttempt = 1;
1261
- await subscription.closed;
1262
- if (runtime.subscription === subscription) runtime.subscription = void 0;
1263
- if (this.state !== "started") break;
1264
- this.logger.warn(`${eventSource.name ?? "Event source"} disconnected; reconnecting in ${reconnectDelay}ms`);
1366
+ this.eventBus.emit(type, event);
1367
+ };
1368
+ parent.eventBus.on("*", this.parentEventForwarder);
1369
+ }
1370
+ const getState = () => this.lifecycle.state;
1371
+ this.plugins = new PluginRegistry(this, parent?.plugins, this.logHandler);
1372
+ this.apiHooks = new ApiHookRegistry(baseClient, parent?.apiHooks, this.name, getState);
1373
+ this.client = this.apiHooks.client;
1374
+ this.timers = new TimerRegistry(this.name, this.logger, getState);
1375
+ this.eventSources = new EventSourceRegistry(options?.reconnect, this.logger, getState, (event) => {
1376
+ this.eventBus.emit(event.event_type, event);
1377
+ });
1378
+ this.lifecycle = new LifecycleManager(this.name, this.plugins, this.timers, this.eventSources, this.apiHooks, () => {
1379
+ if (this.parent && this.parentEventForwarder) this.parent.eventBus.off("*", this.parentEventForwarder);
1380
+ });
1381
+ parent?.lifecycle.addChild(this.lifecycle);
1382
+ this.eventBus.on("message_receive", async ({ self_id, data: message }) => {
1383
+ try {
1384
+ if (this.lifecycle.state === "stopping" || this.lifecycle.state === "stopped") return;
1385
+ await this.router.dispatch(this.createSession(self_id, message), message);
1265
1386
  } catch (error) {
1266
- if (this.state !== "started") break;
1267
- this.logger.error(`Error connecting ${eventSource.name ?? "event source"}; reconnecting in ${reconnectDelay}ms`, error);
1387
+ this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
1268
1388
  }
1269
- await this.waitForReconnectDelay(runtime, reconnectDelay);
1270
- reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
1271
- reconnectAttempt += 1;
1272
- }
1273
- }
1274
- waitForReconnectDelay(runtime, delay) {
1275
- return new Promise((resolve) => {
1276
- runtime.resolveReconnectTimer = resolve;
1277
- runtime.reconnectTimer = setTimeout(() => {
1278
- runtime.reconnectTimer = void 0;
1279
- runtime.resolveReconnectTimer = void 0;
1280
- resolve();
1281
- }, delay);
1282
1389
  });
1283
1390
  }
1284
- resolveReconnectDelay(runtime) {
1285
- if (runtime.reconnectTimer) {
1286
- clearTimeout(runtime.reconnectTimer);
1287
- runtime.reconnectTimer = void 0;
1391
+ on(type, handler) {
1392
+ const wrappedHandler = async (event) => {
1393
+ try {
1394
+ if (this.lifecycle.state === "stopping" || this.lifecycle.state === "stopped") return;
1395
+ await handler(event);
1396
+ } catch (error) {
1397
+ this.logger.error(`Error handling event ${type}`, error);
1398
+ }
1399
+ };
1400
+ this.eventBus.on(type, wrappedHandler);
1401
+ return () => {
1402
+ this.eventBus.off(type, wrappedHandler);
1403
+ };
1404
+ }
1405
+ install(plugin, ...args) {
1406
+ this.plugins.install(plugin, ...args);
1407
+ }
1408
+ installEventSource(eventSource) {
1409
+ this.eventSources.install(eventSource);
1410
+ }
1411
+ hookApi(endpointOrHook, hook) {
1412
+ return this.apiHooks.register(endpointOrHook, hook);
1413
+ }
1414
+ provide(service, instance) {
1415
+ this.plugins.provide(service, instance);
1416
+ }
1417
+ resolve(service) {
1418
+ return this.plugins.resolve(service);
1419
+ }
1420
+ tryResolve(service) {
1421
+ return this.plugins.tryResolve(service);
1422
+ }
1423
+ isProvided(service) {
1424
+ return this.plugins.isProvided(service);
1425
+ }
1426
+ fork(name, filter) {
1427
+ if (this.subContexts.has(name)) {
1428
+ 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.`);
1429
+ return this.subContexts.get(name);
1288
1430
  }
1289
- const resolve = runtime.resolveReconnectTimer;
1290
- runtime.resolveReconnectTimer = void 0;
1291
- resolve?.();
1431
+ const subContext = new Context(this.baseClient, void 0, name, this, filter);
1432
+ this.subContexts.set(name, subContext);
1433
+ return subContext;
1434
+ }
1435
+ timeout(delayMs, callback) {
1436
+ return this.timers.timeout(delayMs, callback);
1437
+ }
1438
+ interval(intervalMs, callback) {
1439
+ return this.timers.interval(intervalMs, callback);
1440
+ }
1441
+ createSession(selfId, message) {
1442
+ return {
1443
+ selfId,
1444
+ raw: message,
1445
+ reply: async (textOrSegments, options) => {
1446
+ const actualSegments = [];
1447
+ if (typeof textOrSegments === "string") actualSegments.push({
1448
+ type: "text",
1449
+ data: { text: textOrSegments }
1450
+ });
1451
+ else actualSegments.push(...textOrSegments);
1452
+ if (options?.withMention && message.message_scene === "group") actualSegments.unshift(seg.mention(message.sender_id));
1453
+ if (options?.withQuote) actualSegments.unshift(seg.reply(message.message_seq));
1454
+ switch (message.message_scene) {
1455
+ case "friend": {
1456
+ const { message_seq } = await this.client.send_private_message({
1457
+ user_id: message.peer_id,
1458
+ message: actualSegments
1459
+ });
1460
+ return { messageSeq: message_seq };
1461
+ }
1462
+ case "group": {
1463
+ const { message_seq } = await this.client.send_group_message({
1464
+ group_id: message.peer_id,
1465
+ message: actualSegments
1466
+ });
1467
+ return { messageSeq: message_seq };
1468
+ }
1469
+ }
1470
+ return { messageSeq: 0 };
1471
+ },
1472
+ reaction: async (type, reactionId) => {
1473
+ if (message.message_scene === "group") await this.client.send_group_message_reaction({
1474
+ group_id: message.peer_id,
1475
+ message_seq: message.message_seq,
1476
+ reaction_type: type,
1477
+ reaction: reactionId
1478
+ });
1479
+ }
1480
+ };
1481
+ }
1482
+ async start() {
1483
+ await this.lifecycle.start();
1484
+ }
1485
+ async stop() {
1486
+ await this.lifecycle.stop();
1292
1487
  }
1293
1488
  static fromUrl(baseUrl, options) {
1294
- const context = new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
1489
+ const client = createMilkyClient(baseUrl, { accessToken: options?.accessToken });
1490
+ const context = new Context(client, options);
1295
1491
  if (options?.installEventSource ?? true) context.installEventSource(createMilkyWebSocketEventSource(baseUrl, { accessToken: options?.accessToken }));
1296
1492
  return context;
1297
1493
  }
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.1",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"