@fraqjs/fraq 0.13.0 → 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
@@ -5828,7 +5828,7 @@ declare function definePlugin<
5828
5828
  OI extends Injection | undefined,
5829
5829
  >(plugin: Plugin<T, I, OI>): Plugin<T, I, OI>;
5830
5830
  //#endregion
5831
- //#region src/core/context.d.ts
5831
+ //#region src/core/context/index.d.ts
5832
5832
  interface ContextOptions {
5833
5833
  reconnect?: {
5834
5834
  initialDelayMs?: number;
@@ -5851,19 +5851,14 @@ declare class Context {
5851
5851
  private readonly parent?;
5852
5852
  private readonly filter?;
5853
5853
  private readonly eventBus;
5854
- private readonly plugins;
5855
- private readonly services;
5856
- private readonly apiHookEntries;
5857
5854
  private readonly subContexts;
5858
- private readonly eventSourceRuntimes;
5859
5855
  private readonly parentEventForwarder?;
5860
- private readonly timers;
5861
- private readonly initialReconnectDelayMs;
5862
- private readonly maxReconnectDelayMs;
5863
5856
  private readonly logHandler?;
5864
- private state;
5865
- private startPromise?;
5866
- private stopPromise?;
5857
+ private readonly plugins;
5858
+ private readonly apiHooks;
5859
+ private readonly eventSources;
5860
+ private readonly timers;
5861
+ private readonly lifecycle;
5867
5862
  private constructor();
5868
5863
  on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): () => void;
5869
5864
  install<T extends ParameterList, I extends Injection | undefined, OI extends Injection | undefined>(
@@ -5883,36 +5878,6 @@ declare class Context {
5883
5878
  createSession(selfId: number, message: IncomingMessage): Session;
5884
5879
  start(): Promise<void>;
5885
5880
  stop(): Promise<void>;
5886
- private startInternal;
5887
- private getState;
5888
- private stopInternal;
5889
- private assertCanRegisterApiHook;
5890
- private assertCanScheduleTimer;
5891
- private runTimerCallback;
5892
- private clearTimers;
5893
- private stopEventSources;
5894
- private acceptsParentEvent;
5895
- private applyPlugins;
5896
- private recursiveApplyPlugins;
5897
- private startPlugins;
5898
- private recursiveStartPlugins;
5899
- private sortPlugins;
5900
- private collectPendingProvidedServices;
5901
- private areRequiredServicesAvailable;
5902
- private areOptionalServicesReady;
5903
- private collectAvailableServices;
5904
- private createUnresolvablePluginError;
5905
- private getPluginContext;
5906
- private createProxyContextForPlugin;
5907
- private startEventSource;
5908
- private runEventSource;
5909
- private waitForReconnectDelay;
5910
- private resolveReconnectDelay;
5911
- private createHookClient;
5912
- private wrapAnyApiHook;
5913
- private callHookedApi;
5914
- private collectApiHooks;
5915
- private callBaseApi;
5916
5881
  static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
5917
5882
  static fromClient(client: MilkyClient, options?: ContextOptions): Context;
5918
5883
  }
package/dist/index.mjs CHANGED
@@ -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,97 +801,29 @@ 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
- }
818
- //#endregion
819
- //#region src/core/context.ts
820
- const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
821
- const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
822
- var Context = class Context {
823
- router = new Router();
824
- logger;
825
- name;
826
- routeActivationResolver;
827
- client;
804
+ //#region src/core/context/api-hooks.ts
805
+ var ApiHookRegistry = class {
828
806
  baseClient;
829
807
  parent;
830
- filter;
831
- eventBus = mitt();
832
- plugins = [];
833
- services = /* @__PURE__ */ new Map();
834
- apiHookEntries = [];
835
- subContexts = /* @__PURE__ */ new Map();
836
- eventSourceRuntimes = /* @__PURE__ */ new Map();
837
- parentEventForwarder;
838
- timers = /* @__PURE__ */ new Set();
839
- initialReconnectDelayMs;
840
- maxReconnectDelayMs;
841
- logHandler;
842
- state = "idle";
843
- startPromise;
844
- stopPromise;
845
- constructor(baseClient, options, name, parent, filter) {
808
+ contextName;
809
+ getState;
810
+ client;
811
+ entries = [];
812
+ constructor(baseClient, parent, contextName, getState) {
846
813
  this.baseClient = baseClient;
847
- this.client = this.createHookClient();
848
- this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
849
- this.maxReconnectDelayMs = options?.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
850
- this.logHandler = options?.logHandler ?? parent?.logHandler;
851
- this.name = name ?? "root";
852
- this.logger = new Logger((message) => this.logHandler?.(message), `context:${this.name}`);
853
- this.routeActivationResolver = createContextRouteActivationResolver(options?.routing, parent?.routeActivationResolver);
854
- this.router.setActivationResolver(this.routeActivationResolver);
855
814
  this.parent = parent;
856
- this.filter = filter;
857
- if (parent) {
858
- this.parentEventForwarder = (type, event) => {
859
- if (!this.acceptsParentEvent(type, event)) return;
860
- this.eventBus.emit(type, event);
861
- };
862
- parent.eventBus.on("*", this.parentEventForwarder);
863
- }
864
- this.eventBus.on("message_receive", async ({ self_id, data: message }) => {
865
- try {
866
- if (this.state === "stopping" || this.state === "stopped") return;
867
- await this.router.dispatch(this.createSession(self_id, message), message);
868
- } catch (error) {
869
- this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
870
- }
871
- });
872
- }
873
- on(type, handler) {
874
- const wrappedHandler = async (event) => {
875
- try {
876
- if (this.state === "stopping" || this.state === "stopped") return;
877
- await handler(event);
878
- } catch (error) {
879
- this.logger.error(`Error handling event ${type}`, error);
880
- }
881
- };
882
- this.eventBus.on(type, wrappedHandler);
883
- return () => {
884
- this.eventBus.off(type, wrappedHandler);
885
- };
886
- }
887
- install(plugin, ...args) {
888
- this.plugins.push({
889
- plugin,
890
- args
891
- });
892
- }
893
- installEventSource(eventSource) {
894
- if (this.eventSourceRuntimes.has(eventSource)) return;
895
- this.eventSourceRuntimes.set(eventSource, {});
896
- if (this.state === "started") this.startEventSource(eventSource);
815
+ this.contextName = contextName;
816
+ this.getState = getState;
817
+ this.client = this.createHookClient();
897
818
  }
898
- hookApi(endpointOrHook, hook) {
899
- this.assertCanRegisterApiHook();
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.`);
900
823
  let entry;
901
- if (typeof endpointOrHook === "function") entry = { hook: this.wrapAnyApiHook(endpointOrHook) };
824
+ if (typeof endpointOrHook === "function") entry = { hook: (params, next, call) => {
825
+ return endpointOrHook(call, (nextParams = params) => next(nextParams));
826
+ } };
902
827
  else {
903
828
  if (!hook) throw new Error(`API hook for endpoint ${endpointOrHook} is missing a handler.`);
904
829
  entry = {
@@ -906,213 +831,98 @@ var Context = class Context {
906
831
  hook
907
832
  };
908
833
  }
909
- this.apiHookEntries.push(entry);
834
+ this.entries.push(entry);
910
835
  let disposed = false;
911
836
  return () => {
912
837
  if (disposed) return;
913
838
  disposed = true;
914
- const index = this.apiHookEntries.indexOf(entry);
915
- if (index !== -1) this.apiHookEntries.splice(index, 1);
839
+ const index = this.entries.indexOf(entry);
840
+ if (index !== -1) this.entries.splice(index, 1);
916
841
  };
917
842
  }
918
- provide(service, instance) {
919
- if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
920
- if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
921
- Service ${service.name} implements ESNext Disposable but not Fraq Disposable.
922
- Please explicitly import the interface like this:
923
-
924
- import type { Disposable } from '@fraqjs/fraq';
925
-
926
- and implement the dispose method to clean up resources when the context stops.
927
- `.trim());
928
- this.services.set(service, instance);
929
- }
930
- resolve(service) {
931
- const instance = this.tryResolve(service);
932
- if (instance === void 0) throw new Error(`Service ${service.name} has not been provided.`);
933
- return instance;
843
+ clear() {
844
+ this.entries.length = 0;
934
845
  }
935
- tryResolve(service) {
936
- if (this.services.has(service)) return this.services.get(service);
937
- return this.parent?.tryResolve(service);
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
+ } });
938
851
  }
939
- isProvided(service) {
940
- return this.tryResolve(service) !== void 0;
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);
941
868
  }
942
- fork(name, filter) {
943
- if (this.subContexts.has(name)) {
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.`);
945
- return this.subContexts.get(name);
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;
946
875
  }
947
- const subContext = new Context(this.baseClient, void 0, name, this, filter);
948
- this.subContexts.set(name, subContext);
949
- return subContext;
876
+ return hooks;
950
877
  }
951
- timeout(delayMs, callback) {
952
- this.assertCanScheduleTimer();
953
- const timeout = setTimeout(() => {
954
- this.timers.delete(timeout);
955
- this.runTimerCallback(callback);
956
- }, delayMs);
957
- this.timers.add(timeout);
958
- return timeout;
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);
959
884
  }
960
- interval(intervalMs, callback) {
961
- this.assertCanScheduleTimer();
962
- const interval = setInterval(() => {
963
- this.runTimerCallback(callback);
964
- }, intervalMs);
965
- this.timers.add(interval);
966
- return interval;
885
+ };
886
+ //#endregion
887
+ //#region src/core/context/event-sources.ts
888
+ const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
889
+ const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
890
+ var EventSourceRegistry = class {
891
+ logger;
892
+ getState;
893
+ emit;
894
+ runtimes = /* @__PURE__ */ new Map();
895
+ initialReconnectDelayMs;
896
+ maxReconnectDelayMs;
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;
967
903
  }
968
- createSession(selfId, message) {
969
- return {
970
- selfId,
971
- raw: message,
972
- reply: async (textOrSegments, options) => {
973
- const actualSegments = [];
974
- if (typeof textOrSegments === "string") actualSegments.push({
975
- type: "text",
976
- data: { text: textOrSegments }
977
- });
978
- else actualSegments.push(...textOrSegments);
979
- if (options?.withMention && message.message_scene === "group") actualSegments.unshift(seg.mention(message.sender_id));
980
- if (options?.withQuote) actualSegments.unshift(seg.reply(message.message_seq));
981
- switch (message.message_scene) {
982
- case "friend": {
983
- const { message_seq } = await this.client.send_private_message({
984
- user_id: message.peer_id,
985
- message: actualSegments
986
- });
987
- return { messageSeq: message_seq };
988
- }
989
- case "group": {
990
- const { message_seq } = await this.client.send_group_message({
991
- group_id: message.peer_id,
992
- message: actualSegments
993
- });
994
- return { messageSeq: message_seq };
995
- }
996
- }
997
- return { messageSeq: 0 };
998
- },
999
- reaction: async (type, reactionId) => {
1000
- if (message.message_scene === "group") await this.client.send_group_message_reaction({
1001
- group_id: message.peer_id,
1002
- message_seq: message.message_seq,
1003
- reaction_type: type,
1004
- reaction: reactionId
1005
- });
1006
- }
1007
- };
904
+ install(eventSource) {
905
+ if (this.runtimes.has(eventSource)) return;
906
+ this.runtimes.set(eventSource, {});
907
+ if (this.getState() === "started") this.startEventSource(eventSource);
1008
908
  }
1009
- async start() {
1010
- if (this.state === "started") return;
1011
- if (this.state === "starting") {
1012
- await this.startPromise;
1013
- return;
1014
- }
1015
- if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot be started while it is stopping.`);
1016
- if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot be restarted after it has been stopped.`);
1017
- this.state = "starting";
1018
- this.startPromise = this.startInternal();
1019
- try {
1020
- await this.startPromise;
1021
- } finally {
1022
- this.startPromise = void 0;
1023
- }
909
+ startAll() {
910
+ for (const eventSource of this.runtimes.keys()) this.startEventSource(eventSource);
1024
911
  }
1025
912
  async stop() {
1026
- if (this.state === "idle" && this.timers.size === 0 || this.state === "stopped") return;
1027
- if (this.state === "starting") await this.startPromise;
1028
- const stateAfterStart = this.getState();
1029
- if (stateAfterStart === "idle" && this.timers.size === 0 || stateAfterStart === "stopped") return;
1030
- if (stateAfterStart === "stopping") {
1031
- await this.stopPromise;
1032
- return;
1033
- }
1034
- this.state = "stopping";
1035
- this.stopPromise = this.stopInternal();
1036
- try {
1037
- await this.stopPromise;
1038
- } finally {
1039
- this.state = "stopped";
1040
- this.stopPromise = void 0;
1041
- }
1042
- }
1043
- async startInternal() {
1044
- const appliedContextPlugins = [];
1045
- const startingContexts = [];
1046
- try {
1047
- await this.recursiveApplyPlugins(appliedContextPlugins, startingContexts);
1048
- await this.recursiveStartPlugins(appliedContextPlugins);
1049
- } catch (error) {
1050
- for (const context of startingContexts) if (context.state === "starting") context.state = "idle";
1051
- throw error;
1052
- }
1053
- for (const { context } of appliedContextPlugins) {
1054
- context.state = "started";
1055
- for (const eventSource of context.eventSourceRuntimes.keys()) context.startEventSource(eventSource);
1056
- }
1057
- }
1058
- getState() {
1059
- return this.state;
1060
- }
1061
- async stopInternal() {
1062
913
  const errors = [];
1063
- this.clearTimers();
1064
- for (const subContext of [...this.subContexts.values()].reverse()) try {
1065
- await subContext.stop();
1066
- } catch (error) {
1067
- errors.push(error);
1068
- }
1069
- await this.stopEventSources(errors);
1070
- if (this.parent && this.parentEventForwarder) this.parent.eventBus.off("*", this.parentEventForwarder);
1071
- for (const service of [...this.services.values()].reverse()) {
1072
- if (!isDisposable(service)) continue;
914
+ for (const runtime of this.runtimes.values()) {
915
+ this.resolveReconnectDelay(runtime);
916
+ const subscription = runtime.subscription;
917
+ if (!subscription) continue;
1073
918
  try {
1074
- await service.dispose();
919
+ await subscription.stop();
1075
920
  } catch (error) {
1076
921
  errors.push(error);
1077
922
  }
923
+ runtime.subscription = void 0;
1078
924
  }
1079
- this.apiHookEntries.length = 0;
1080
- if (errors.length === 1) throw errors[0];
1081
- if (errors.length > 1) throw new AggregateError(errors, `Context "${this.name}" failed to stop cleanly.`);
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
- }
1087
- assertCanScheduleTimer() {
1088
- if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot schedule timers while it is stopping.`);
1089
- if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot schedule timers after it has stopped.`);
1090
- }
1091
- async runTimerCallback(callback) {
1092
- if (this.state === "stopping" || this.state === "stopped") return;
1093
- try {
1094
- await callback();
1095
- } catch (error) {
1096
- this.logger.error("Error handling timer callback", error);
1097
- }
1098
- }
1099
- clearTimers() {
1100
- for (const timer of this.timers) clearTimeout(timer);
1101
- this.timers.clear();
1102
- }
1103
- async stopEventSources(errors) {
1104
- for (const [_, runtime] of this.eventSourceRuntimes) {
1105
- this.resolveReconnectDelay(runtime);
1106
- const subscription = runtime.subscription;
1107
- if (!subscription) continue;
1108
- try {
1109
- await subscription.stop();
1110
- } catch (error) {
1111
- errors.push(error);
1112
- }
1113
- runtime.subscription = void 0;
1114
- }
1115
- for (const runtime of this.eventSourceRuntimes.values()) {
925
+ for (const runtime of this.runtimes.values()) {
1116
926
  if (!runtime.task) continue;
1117
927
  try {
1118
928
  await runtime.task;
@@ -1122,13 +932,259 @@ and implement the dispose method to clean up resources when the context stops.
1122
932
  runtime.task = void 0;
1123
933
  }
1124
934
  }
935
+ return errors;
936
+ }
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") {
946
+ try {
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`);
968
+ } catch (error) {
969
+ if (this.getState() !== "started") break;
970
+ this.logger.error(`Error connecting ${eventSource.name ?? "event source"}; reconnecting in ${reconnectDelay}ms`, error);
971
+ }
972
+ await this.waitForReconnectDelay(runtime, reconnectDelay);
973
+ reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
974
+ reconnectAttempt += 1;
975
+ }
976
+ }
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);
985
+ });
986
+ }
987
+ resolveReconnectDelay(runtime) {
988
+ if (runtime.reconnectTimer) {
989
+ clearTimeout(runtime.reconnectTimer);
990
+ runtime.reconnectTimer = void 0;
991
+ }
992
+ const resolve = runtime.resolveReconnectTimer;
993
+ runtime.resolveReconnectTimer = void 0;
994
+ resolve?.();
995
+ }
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;
1017
+ }
1018
+ get state() {
1019
+ return this.currentState;
1020
+ }
1021
+ addChild(child) {
1022
+ this.children.add(child);
1023
+ }
1024
+ async start() {
1025
+ if (this.currentState === "started") return;
1026
+ if (this.currentState === "starting") {
1027
+ await this.startPromise;
1028
+ return;
1029
+ }
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";
1033
+ this.startPromise = this.startInternal();
1034
+ try {
1035
+ await this.startPromise;
1036
+ } finally {
1037
+ this.startPromise = void 0;
1038
+ }
1039
+ }
1040
+ async stop() {
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;
1048
+ if (stateAfterStart === "stopping") {
1049
+ await this.stopPromise;
1050
+ return;
1051
+ }
1052
+ this.currentState = "stopping";
1053
+ this.stopPromise = this.stopInternal();
1054
+ try {
1055
+ await this.stopPromise;
1056
+ } finally {
1057
+ this.currentState = "stopped";
1058
+ this.stopPromise = void 0;
1059
+ }
1060
+ }
1061
+ async startInternal() {
1062
+ const appliedContextPlugins = [];
1063
+ const startingContexts = [];
1064
+ try {
1065
+ await this.recursiveApplyPlugins(appliedContextPlugins, startingContexts);
1066
+ for (const { lifecycle, sortedPlugins } of appliedContextPlugins) await lifecycle.plugins.start(sortedPlugins);
1067
+ } catch (error) {
1068
+ for (const lifecycle of startingContexts) if (lifecycle.currentState === "starting") lifecycle.currentState = "idle";
1069
+ throw error;
1070
+ }
1071
+ for (const { lifecycle } of appliedContextPlugins) {
1072
+ lifecycle.currentState = "started";
1073
+ lifecycle.eventSources.startAll();
1074
+ }
1075
+ }
1076
+ async stopInternal() {
1077
+ const errors = [];
1078
+ this.timers.clear();
1079
+ for (const child of [...this.children].reverse()) try {
1080
+ await child.stop();
1081
+ } catch (error) {
1082
+ errors.push(error);
1083
+ }
1084
+ errors.push(...await this.eventSources.stop());
1085
+ this.detachParentEvents();
1086
+ errors.push(...await this.plugins.disposeServices());
1087
+ this.apiHooks.clear();
1088
+ if (errors.length === 1) throw errors[0];
1089
+ if (errors.length > 1) throw new AggregateError(errors, `Context "${this.contextName}" failed to stop cleanly.`);
1090
+ }
1091
+ async recursiveApplyPlugins(appliedContextPlugins, startingContexts) {
1092
+ if (this.currentState === "started") return;
1093
+ if (this.currentState === "starting" && this.startPromise) {
1094
+ await this.startPromise;
1095
+ return;
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);
1107
+ }
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;
1155
+ }
1156
+ install(plugin, ...args) {
1157
+ this.plugins.push({
1158
+ plugin,
1159
+ args
1160
+ });
1161
+ }
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);
1125
1173
  }
1126
- acceptsParentEvent(type, event) {
1127
- if (!this.filter) return true;
1128
- const predicate = this.filter[type];
1129
- return predicate?.(event) === true;
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;
1130
1178
  }
1131
- async applyPlugins(sortedPlugins) {
1179
+ tryResolve(service) {
1180
+ if (this.services.has(service)) return this.services.get(service);
1181
+ return this.parent?.tryResolve(service);
1182
+ }
1183
+ isProvided(service) {
1184
+ return this.tryResolve(service) !== void 0;
1185
+ }
1186
+ async apply() {
1187
+ const sortedPlugins = this.sortPlugins();
1132
1188
  for (const installedPlugin of sortedPlugins) {
1133
1189
  const { plugin, args } = installedPlugin;
1134
1190
  const providedBeforeApply = new Set(this.services.keys());
@@ -1140,39 +1196,31 @@ and implement the dispose method to clean up resources when the context stops.
1140
1196
  if (plugin.provides) for (const service of plugin.provides) providedServices.push(service.name);
1141
1197
  if (requiredServices.length > 0) applyingMessage += `, requires: [${requiredServices.join(", ")}]`;
1142
1198
  if (providedServices.length > 0) applyingMessage += `, provides: [${providedServices.join(", ")}]`;
1143
- this.logger.info(applyingMessage);
1199
+ this.context.logger.info(applyingMessage);
1144
1200
  await plugin.apply(this.getPluginContext(installedPlugin), ...args);
1145
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.`);
1146
1202
  }
1203
+ return sortedPlugins;
1147
1204
  }
1148
- async recursiveApplyPlugins(appliedContextPlugins, startingContexts) {
1149
- if (this.state === "started") return;
1150
- if (this.state === "starting" && this.startPromise) {
1151
- await this.startPromise;
1152
- return;
1153
- }
1154
- if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot be started while it is stopping.`);
1155
- if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot be restarted after it has been stopped.`);
1156
- if (this.state === "idle") this.state = "starting";
1157
- startingContexts.push(this);
1158
- const sortedPlugins = this.sortPlugins();
1159
- await this.applyPlugins(sortedPlugins);
1160
- appliedContextPlugins.push({
1161
- context: this,
1162
- sortedPlugins
1163
- });
1164
- for (const subContext of this.subContexts.values()) await subContext.recursiveApplyPlugins(appliedContextPlugins, startingContexts);
1165
- }
1166
- async startPlugins(sortedPlugins) {
1205
+ async start(sortedPlugins) {
1167
1206
  for (const installedPlugin of sortedPlugins) {
1168
1207
  const { plugin } = installedPlugin;
1169
1208
  if (!plugin.start) continue;
1170
- this.logger.debug(`Plugin ${plugin.name} is starting...`);
1209
+ this.context.logger.debug(`Plugin ${plugin.name} is starting...`);
1171
1210
  await plugin.start(this.getPluginContext(installedPlugin));
1172
1211
  }
1173
1212
  }
1174
- async recursiveStartPlugins(appliedContextPlugins) {
1175
- 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;
1176
1224
  }
1177
1225
  sortPlugins() {
1178
1226
  const providers = /* @__PURE__ */ new Map();
@@ -1183,64 +1231,33 @@ and implement the dispose method to clean up resources when the context stops.
1183
1231
  }
1184
1232
  const pending = [...this.plugins];
1185
1233
  const sorted = [];
1186
- const available = /* @__PURE__ */ new Set();
1187
- for (const service of this.collectAvailableServices()) available.add(service);
1234
+ const available = new Set(this.collectAvailableServices());
1188
1235
  while (pending.length > 0) {
1189
- const availableByPendingPlugins = this.collectPendingProvidedServices(pending);
1190
- const requiredReadyIndex = pending.findIndex(({ plugin }) => this.areRequiredServicesAvailable(plugin, available));
1191
- 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));
1192
1240
  const nextIndex = optionalReadyIndex === -1 ? requiredReadyIndex : optionalReadyIndex;
1193
- if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
1241
+ if (nextIndex === -1) throw createUnresolvablePluginError(pending, available);
1194
1242
  const [next] = pending.splice(nextIndex, 1);
1195
1243
  sorted.push(next);
1196
1244
  for (const service of next.plugin.provides ?? []) available.add(service);
1197
1245
  }
1198
1246
  return sorted;
1199
1247
  }
1200
- collectPendingProvidedServices(pending) {
1201
- const providedServices = /* @__PURE__ */ new Map();
1202
- for (const installedPlugin of pending) for (const service of installedPlugin.plugin.provides ?? []) providedServices.set(service, installedPlugin);
1203
- return providedServices;
1204
- }
1205
- areRequiredServicesAvailable(plugin, available) {
1206
- return (plugin.requires ?? []).every((service) => available.has(service));
1207
- }
1208
- areOptionalServicesReady(plugin, available, pendingProviders) {
1209
- return (plugin.optionalRequires ?? []).every((service) => {
1210
- if (available.has(service)) return true;
1211
- const pendingProvider = pendingProviders.get(service);
1212
- return pendingProvider === void 0 || pendingProvider.plugin === plugin;
1213
- });
1214
- }
1215
1248
  collectAvailableServices() {
1216
1249
  const services = [...this.services.keys()];
1217
1250
  if (this.parent) services.push(...this.parent.collectAvailableServices());
1218
1251
  return services;
1219
1252
  }
1220
- createUnresolvablePluginError(pending, available) {
1221
- const missingRequirements = /* @__PURE__ */ new Map();
1222
- const pendingProviders = /* @__PURE__ */ new Set();
1223
- for (const { plugin } of pending) for (const service of plugin.provides ?? []) pendingProviders.add(service);
1224
- for (const { plugin } of pending) for (const service of plugin.requires ?? []) if (!available.has(service)) {
1225
- const plugins = missingRequirements.get(service) ?? [];
1226
- plugins.push(plugin);
1227
- missingRequirements.set(service, plugins);
1228
- }
1229
- const lines = [...missingRequirements].map(([service, plugins]) => {
1230
- const dependents = plugins.map((plugin) => plugin.name).join(", ");
1231
- const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
1232
- return `${service.name} required by ${dependents} (${reason})`;
1233
- });
1234
- return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
1235
- }
1236
1253
  getPluginContext(installedPlugin) {
1237
1254
  installedPlugin.proxy ??= this.createProxyContextForPlugin(installedPlugin.plugin);
1238
1255
  return installedPlugin.proxy;
1239
1256
  }
1240
1257
  createProxyContextForPlugin(plugin) {
1241
- const proxyLogger = new Logger((message) => this.logHandler?.(message), `plugin:${this.name ? `${this.name}/` : ""}${plugin.name}`);
1242
- const proxyRouter = this.router.withMeta({
1243
- 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,
1244
1261
  plugin: plugin.name
1245
1262
  });
1246
1263
  let proxyInjections;
@@ -1252,115 +1269,221 @@ and implement the dispose method to clean up resources when the context stops.
1252
1269
  proxyInjections ??= {};
1253
1270
  for (const [key, service] of Object.entries(plugin.optionalInject)) proxyInjections[key] = this.tryResolve(service);
1254
1271
  }
1255
- return new Proxy(this, { get(target, prop, receiver) {
1272
+ return new Proxy(this.context, { get(target, prop, receiver) {
1256
1273
  if (prop === "logger") return proxyLogger;
1257
- else if (prop === "router") return proxyRouter;
1258
- else if (proxyInjections && prop in proxyInjections) return proxyInjections[prop];
1259
- 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);
1260
1277
  } });
1261
1278
  }
1262
- startEventSource(eventSource) {
1263
- const runtime = this.eventSourceRuntimes.get(eventSource);
1264
- if (!runtime) return;
1265
- 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;
1266
1291
  }
1267
- async runEventSource(eventSource, runtime) {
1268
- let reconnectDelay = this.initialReconnectDelayMs;
1269
- let reconnectAttempt = 1;
1270
- while (this.state === "started") {
1271
- try {
1272
- this.logger.debug(`Connecting ${eventSource.name ?? "event source"} (attempt=${reconnectAttempt})`);
1273
- const subscription = await eventSource.start((event) => {
1274
- try {
1275
- if (this.state !== "started") return;
1276
- this.eventBus.emit(event.event_type, event);
1277
- } catch (error) {
1278
- this.logger.error("Error handling event stream event", error);
1279
- }
1280
- });
1281
- if (this.state !== "started") {
1282
- await subscription.stop();
1283
- 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;
1284
1365
  }
1285
- runtime.subscription = subscription;
1286
- this.logger.info(`${eventSource.name ?? "Event source"} connected`);
1287
- reconnectDelay = this.initialReconnectDelayMs;
1288
- reconnectAttempt = 1;
1289
- await subscription.closed;
1290
- if (runtime.subscription === subscription) runtime.subscription = void 0;
1291
- if (this.state !== "started") break;
1292
- 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);
1293
1386
  } catch (error) {
1294
- if (this.state !== "started") break;
1295
- 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);
1296
1388
  }
1297
- await this.waitForReconnectDelay(runtime, reconnectDelay);
1298
- reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
1299
- reconnectAttempt += 1;
1300
- }
1301
- }
1302
- waitForReconnectDelay(runtime, delay) {
1303
- return new Promise((resolve) => {
1304
- runtime.resolveReconnectTimer = resolve;
1305
- runtime.reconnectTimer = setTimeout(() => {
1306
- runtime.reconnectTimer = void 0;
1307
- runtime.resolveReconnectTimer = void 0;
1308
- resolve();
1309
- }, delay);
1310
1389
  });
1311
1390
  }
1312
- resolveReconnectDelay(runtime) {
1313
- if (runtime.reconnectTimer) {
1314
- clearTimeout(runtime.reconnectTimer);
1315
- 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);
1316
1430
  }
1317
- const resolve = runtime.resolveReconnectTimer;
1318
- runtime.resolveReconnectTimer = void 0;
1319
- resolve?.();
1431
+ const subContext = new Context(this.baseClient, void 0, name, this, filter);
1432
+ this.subContexts.set(name, subContext);
1433
+ return subContext;
1320
1434
  }
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
- } });
1435
+ timeout(delayMs, callback) {
1436
+ return this.timers.timeout(delayMs, callback);
1326
1437
  }
1327
- wrapAnyApiHook(hook) {
1328
- return (params, next, call) => {
1329
- return hook(call, (nextParams = params) => next(nextParams));
1330
- };
1438
+ interval(intervalMs, callback) {
1439
+ return this.timers.interval(intervalMs, callback);
1331
1440
  }
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
- });
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
+ }
1346
1480
  };
1347
- return await dispatch(0, params);
1348
1481
  }
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;
1482
+ async start() {
1483
+ await this.lifecycle.start();
1357
1484
  }
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);
1485
+ async stop() {
1486
+ await this.lifecycle.stop();
1364
1487
  }
1365
1488
  static fromUrl(baseUrl, options) {
1366
1489
  const client = createMilkyClient(baseUrl, { accessToken: options?.accessToken });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.13.1",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"