@fraqjs/fraq 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +24 -4
- package/dist/index.mjs +167 -45
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -5620,6 +5620,10 @@ declare function combineLogHandlers(...handlers: LogHandler[]): LogHandler;
|
|
|
5620
5620
|
//#region src/core/service.d.ts
|
|
5621
5621
|
/** biome-ignore-all lint/suspicious/noExplicitAny: Service constructors may accept any arguments. */
|
|
5622
5622
|
type ServiceClass<T extends object = object> = abstract new (...args: any[]) => T;
|
|
5623
|
+
interface Disposable {
|
|
5624
|
+
dispose(): void | Promise<void>;
|
|
5625
|
+
}
|
|
5626
|
+
declare function isDisposable(service: object): service is Disposable;
|
|
5623
5627
|
//#endregion
|
|
5624
5628
|
//#region src/core/plugin.d.ts
|
|
5625
5629
|
type ParameterList = Array<any>;
|
|
@@ -5671,12 +5675,19 @@ declare class Context {
|
|
|
5671
5675
|
private readonly plugins;
|
|
5672
5676
|
private readonly services;
|
|
5673
5677
|
private readonly subContexts;
|
|
5678
|
+
private readonly parentEventForwarder?;
|
|
5674
5679
|
private readonly initialReconnectDelayMs;
|
|
5675
5680
|
private readonly maxReconnectDelayMs;
|
|
5676
5681
|
private readonly logHandler?;
|
|
5677
|
-
private
|
|
5682
|
+
private state;
|
|
5683
|
+
private startPromise?;
|
|
5684
|
+
private stopPromise?;
|
|
5685
|
+
private eventSubscription?;
|
|
5686
|
+
private eventStreamTask?;
|
|
5687
|
+
private reconnectTimer?;
|
|
5688
|
+
private resolveReconnectTimer?;
|
|
5678
5689
|
private constructor();
|
|
5679
|
-
on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): void;
|
|
5690
|
+
on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): () => void;
|
|
5680
5691
|
install<T extends ParameterList, I extends Injection | undefined, OI extends Injection | undefined>(
|
|
5681
5692
|
plugin: Plugin<T, I, OI>,
|
|
5682
5693
|
...args: T
|
|
@@ -5686,7 +5697,13 @@ declare class Context {
|
|
|
5686
5697
|
tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
|
|
5687
5698
|
isProvided<T extends object>(service: ServiceClass<T>): boolean;
|
|
5688
5699
|
fork(name: string, filter?: Filter): Context;
|
|
5700
|
+
createSession(message: IncomingMessage): Session;
|
|
5689
5701
|
start(): Promise<void>;
|
|
5702
|
+
stop(): Promise<void>;
|
|
5703
|
+
private startInternal;
|
|
5704
|
+
private getState;
|
|
5705
|
+
private stopInternal;
|
|
5706
|
+
private stopEventStream;
|
|
5690
5707
|
private acceptsParentEvent;
|
|
5691
5708
|
private applyPlugins;
|
|
5692
5709
|
private sortPlugins;
|
|
@@ -5697,7 +5714,8 @@ declare class Context {
|
|
|
5697
5714
|
private createUnresolvablePluginError;
|
|
5698
5715
|
private createProxyContextForPlugin;
|
|
5699
5716
|
private runEventStream;
|
|
5700
|
-
private
|
|
5717
|
+
private waitForReconnectDelay;
|
|
5718
|
+
private resolveReconnectDelay;
|
|
5701
5719
|
static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
|
|
5702
5720
|
static fromClient(client: MilkyClient, options?: ContextOptions): Context;
|
|
5703
5721
|
}
|
|
@@ -5748,6 +5766,7 @@ export {
|
|
|
5748
5766
|
Context,
|
|
5749
5767
|
ContextOptions,
|
|
5750
5768
|
ContextUrlOptions,
|
|
5769
|
+
Disposable,
|
|
5751
5770
|
EventMap,
|
|
5752
5771
|
Filter,
|
|
5753
5772
|
Handler,
|
|
@@ -5768,13 +5787,14 @@ export {
|
|
|
5768
5787
|
RouteEntry,
|
|
5769
5788
|
RouteMatchResult,
|
|
5770
5789
|
Router,
|
|
5771
|
-
|
|
5790
|
+
ServiceClass,
|
|
5772
5791
|
Session,
|
|
5773
5792
|
SessionPredicate,
|
|
5774
5793
|
combineLogHandlers,
|
|
5775
5794
|
createMilkyClient,
|
|
5776
5795
|
definePlugin,
|
|
5777
5796
|
filter,
|
|
5797
|
+
isDisposable,
|
|
5778
5798
|
type types_d_exports as milky,
|
|
5779
5799
|
milkyPackageVersion,
|
|
5780
5800
|
milkyVersion,
|
package/dist/index.mjs
CHANGED
|
@@ -360,8 +360,8 @@ function combineLogHandlers(...handlers) {
|
|
|
360
360
|
}
|
|
361
361
|
//#endregion
|
|
362
362
|
//#region src/core/service.ts
|
|
363
|
-
function
|
|
364
|
-
return service
|
|
363
|
+
function isDisposable(service) {
|
|
364
|
+
return "dispose" in service && typeof service.dispose === "function";
|
|
365
365
|
}
|
|
366
366
|
//#endregion
|
|
367
367
|
//#region src/core/context.ts
|
|
@@ -378,10 +378,17 @@ var Context = class Context {
|
|
|
378
378
|
plugins = [];
|
|
379
379
|
services = /* @__PURE__ */ new Map();
|
|
380
380
|
subContexts = [];
|
|
381
|
+
parentEventForwarder;
|
|
381
382
|
initialReconnectDelayMs;
|
|
382
383
|
maxReconnectDelayMs;
|
|
383
384
|
logHandler;
|
|
384
|
-
|
|
385
|
+
state = "idle";
|
|
386
|
+
startPromise;
|
|
387
|
+
stopPromise;
|
|
388
|
+
eventSubscription;
|
|
389
|
+
eventStreamTask;
|
|
390
|
+
reconnectTimer;
|
|
391
|
+
resolveReconnectTimer;
|
|
385
392
|
constructor(client, options, name, parent, filter) {
|
|
386
393
|
this.client = client;
|
|
387
394
|
this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
|
|
@@ -391,12 +398,16 @@ var Context = class Context {
|
|
|
391
398
|
this.logger = new Logger((message) => this.logHandler?.(message), this.name);
|
|
392
399
|
this.parent = parent;
|
|
393
400
|
this.filter = filter;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
401
|
+
if (parent) {
|
|
402
|
+
this.parentEventForwarder = (type, event) => {
|
|
403
|
+
if (!this.acceptsParentEvent(type, event)) return;
|
|
404
|
+
this.eventBus.emit(type, event);
|
|
405
|
+
};
|
|
406
|
+
parent.eventBus.on("*", this.parentEventForwarder);
|
|
407
|
+
}
|
|
398
408
|
this.eventBus.on("message_receive", async ({ data: message }) => {
|
|
399
409
|
try {
|
|
410
|
+
if (this.state === "stopping" || this.state === "stopped") return;
|
|
400
411
|
await this.router.dispatch(this.createSession(message), message);
|
|
401
412
|
} catch (error) {
|
|
402
413
|
this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
|
|
@@ -404,13 +415,18 @@ var Context = class Context {
|
|
|
404
415
|
});
|
|
405
416
|
}
|
|
406
417
|
on(type, handler) {
|
|
407
|
-
|
|
418
|
+
const wrappedHandler = async (event) => {
|
|
408
419
|
try {
|
|
420
|
+
if (this.state === "stopping" || this.state === "stopped") return;
|
|
409
421
|
await handler(event);
|
|
410
422
|
} catch (error) {
|
|
411
423
|
this.logger.error(`Error handling event ${type}`, error);
|
|
412
424
|
}
|
|
413
|
-
}
|
|
425
|
+
};
|
|
426
|
+
this.eventBus.on(type, wrappedHandler);
|
|
427
|
+
return () => {
|
|
428
|
+
this.eventBus.off(type, wrappedHandler);
|
|
429
|
+
};
|
|
414
430
|
}
|
|
415
431
|
install(plugin, ...args) {
|
|
416
432
|
this.plugins.push({
|
|
@@ -419,12 +435,12 @@ var Context = class Context {
|
|
|
419
435
|
});
|
|
420
436
|
}
|
|
421
437
|
provide(service, instance) {
|
|
422
|
-
if (this.services.has(service)) throw new Error(`Service ${
|
|
438
|
+
if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
|
|
423
439
|
this.services.set(service, instance);
|
|
424
440
|
}
|
|
425
441
|
resolve(service) {
|
|
426
442
|
const instance = this.tryResolve(service);
|
|
427
|
-
if (instance === void 0) throw new Error(`Service ${
|
|
443
|
+
if (instance === void 0) throw new Error(`Service ${service.name} has not been provided.`);
|
|
428
444
|
return instance;
|
|
429
445
|
}
|
|
430
446
|
tryResolve(service) {
|
|
@@ -439,12 +455,114 @@ var Context = class Context {
|
|
|
439
455
|
this.subContexts.push(subContext);
|
|
440
456
|
return subContext;
|
|
441
457
|
}
|
|
458
|
+
createSession(message) {
|
|
459
|
+
return {
|
|
460
|
+
raw: message,
|
|
461
|
+
reply: async (segments) => {
|
|
462
|
+
try {
|
|
463
|
+
switch (message.message_scene) {
|
|
464
|
+
case "friend":
|
|
465
|
+
await this.client.send_private_message({
|
|
466
|
+
user_id: message.peer_id,
|
|
467
|
+
message: segments
|
|
468
|
+
});
|
|
469
|
+
break;
|
|
470
|
+
case "group":
|
|
471
|
+
await this.client.send_group_message({
|
|
472
|
+
group_id: message.peer_id,
|
|
473
|
+
message: segments
|
|
474
|
+
});
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
this.logger.error(`Error sending reply (source msg: scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
442
483
|
async start() {
|
|
443
|
-
if (this.
|
|
444
|
-
|
|
484
|
+
if (this.state === "started") return;
|
|
485
|
+
if (this.state === "starting") {
|
|
486
|
+
await this.startPromise;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot be started while it is stopping.`);
|
|
490
|
+
if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot be restarted after it has been stopped.`);
|
|
491
|
+
this.state = "starting";
|
|
492
|
+
this.startPromise = this.startInternal();
|
|
493
|
+
try {
|
|
494
|
+
await this.startPromise;
|
|
495
|
+
} finally {
|
|
496
|
+
this.startPromise = void 0;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async stop() {
|
|
500
|
+
if (this.state === "idle" || this.state === "stopped") return;
|
|
501
|
+
if (this.state === "starting") await this.startPromise;
|
|
502
|
+
const stateAfterStart = this.getState();
|
|
503
|
+
if (stateAfterStart === "idle" || stateAfterStart === "stopped") return;
|
|
504
|
+
if (stateAfterStart === "stopping") {
|
|
505
|
+
await this.stopPromise;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.state = "stopping";
|
|
509
|
+
this.stopPromise = this.stopInternal();
|
|
510
|
+
try {
|
|
511
|
+
await this.stopPromise;
|
|
512
|
+
} finally {
|
|
513
|
+
this.state = "stopped";
|
|
514
|
+
this.stopPromise = void 0;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async startInternal() {
|
|
518
|
+
try {
|
|
519
|
+
await this.applyPlugins();
|
|
520
|
+
} catch (error) {
|
|
521
|
+
this.state = "idle";
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
445
524
|
for (const subContext of this.subContexts) await subContext.start();
|
|
446
|
-
this.
|
|
447
|
-
if (!this.parent) this.runEventStream();
|
|
525
|
+
this.state = "started";
|
|
526
|
+
if (!this.parent) this.eventStreamTask = this.runEventStream();
|
|
527
|
+
}
|
|
528
|
+
getState() {
|
|
529
|
+
return this.state;
|
|
530
|
+
}
|
|
531
|
+
async stopInternal() {
|
|
532
|
+
const errors = [];
|
|
533
|
+
await this.stopEventStream(errors);
|
|
534
|
+
for (const subContext of [...this.subContexts].reverse()) try {
|
|
535
|
+
await subContext.stop();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
errors.push(error);
|
|
538
|
+
}
|
|
539
|
+
if (this.parent && this.parentEventForwarder) this.parent.eventBus.off("*", this.parentEventForwarder);
|
|
540
|
+
for (const service of [...this.services.values()].reverse()) {
|
|
541
|
+
if (!isDisposable(service)) continue;
|
|
542
|
+
try {
|
|
543
|
+
await service.dispose();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
errors.push(error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (errors.length === 1) throw errors[0];
|
|
549
|
+
if (errors.length > 1) throw new AggregateError(errors, `Context "${this.name}" failed to stop cleanly.`);
|
|
550
|
+
}
|
|
551
|
+
async stopEventStream(errors) {
|
|
552
|
+
this.resolveReconnectDelay();
|
|
553
|
+
const subscription = this.eventSubscription;
|
|
554
|
+
if (subscription) try {
|
|
555
|
+
await subscription.stop();
|
|
556
|
+
} catch (error) {
|
|
557
|
+
errors.push(error);
|
|
558
|
+
}
|
|
559
|
+
if (this.eventStreamTask) try {
|
|
560
|
+
await this.eventStreamTask;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
errors.push(error);
|
|
563
|
+
} finally {
|
|
564
|
+
this.eventStreamTask = void 0;
|
|
565
|
+
}
|
|
448
566
|
}
|
|
449
567
|
acceptsParentEvent(type, event) {
|
|
450
568
|
if (!this.filter) return true;
|
|
@@ -456,7 +574,7 @@ var Context = class Context {
|
|
|
456
574
|
const providedBeforeApply = new Set(this.services.keys());
|
|
457
575
|
this.logger.info(`Applying plugin ${plugin.name}`);
|
|
458
576
|
await plugin.apply(this.createProxyContextForPlugin(plugin), ...args);
|
|
459
|
-
for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${plugin.name} declares service ${
|
|
577
|
+
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.`);
|
|
460
578
|
this.logger.debug(`Applied plugin ${plugin.name}`);
|
|
461
579
|
}
|
|
462
580
|
}
|
|
@@ -464,7 +582,7 @@ var Context = class Context {
|
|
|
464
582
|
const providers = /* @__PURE__ */ new Map();
|
|
465
583
|
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
466
584
|
const existingProvider = providers.get(service);
|
|
467
|
-
if (existingProvider) throw new Error(`Service ${
|
|
585
|
+
if (existingProvider) throw new Error(`Service ${service.name} is declared by multiple plugins: ${existingProvider.name} and ${plugin.name}.`);
|
|
468
586
|
providers.set(service, plugin);
|
|
469
587
|
}
|
|
470
588
|
const pending = [...this.plugins];
|
|
@@ -515,7 +633,7 @@ var Context = class Context {
|
|
|
515
633
|
const lines = [...missingRequirements].map(([service, plugins]) => {
|
|
516
634
|
const dependents = plugins.map((plugin) => plugin.name).join(", ");
|
|
517
635
|
const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
|
|
518
|
-
return `${
|
|
636
|
+
return `${service.name} required by ${dependents} (${reason})`;
|
|
519
637
|
});
|
|
520
638
|
return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
|
|
521
639
|
}
|
|
@@ -539,53 +657,57 @@ var Context = class Context {
|
|
|
539
657
|
async runEventStream() {
|
|
540
658
|
let reconnectDelay = this.initialReconnectDelayMs;
|
|
541
659
|
let reconnectAttempt = 1;
|
|
542
|
-
while (this.
|
|
660
|
+
while (this.state === "started") {
|
|
543
661
|
try {
|
|
544
662
|
this.logger.debug(`Connecting event stream (attempt=${reconnectAttempt})`);
|
|
545
663
|
const subscription = await this.client.startEvents((event) => {
|
|
546
664
|
try {
|
|
665
|
+
if (this.state !== "started") return;
|
|
547
666
|
this.eventBus.emit(event.event_type, event);
|
|
548
667
|
} catch (error) {
|
|
549
668
|
this.logger.error("Error handling event stream event", error);
|
|
550
669
|
}
|
|
551
670
|
});
|
|
671
|
+
if (this.state !== "started") {
|
|
672
|
+
await subscription.stop();
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
this.eventSubscription = subscription;
|
|
552
676
|
this.logger.info("Event stream connected");
|
|
553
677
|
reconnectDelay = this.initialReconnectDelayMs;
|
|
554
678
|
reconnectAttempt = 1;
|
|
555
679
|
await subscription.closed;
|
|
680
|
+
this.eventSubscription = void 0;
|
|
681
|
+
if (this.state !== "started") break;
|
|
556
682
|
this.logger.warn(`Event stream disconnected; reconnecting in ${reconnectDelay}ms`);
|
|
557
683
|
} catch (error) {
|
|
684
|
+
this.eventSubscription = void 0;
|
|
685
|
+
if (this.state !== "started") break;
|
|
558
686
|
this.logger.error(`Error connecting event stream; reconnecting in ${reconnectDelay}ms`, error);
|
|
559
687
|
}
|
|
560
|
-
await
|
|
688
|
+
await this.waitForReconnectDelay(reconnectDelay);
|
|
561
689
|
reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
|
|
562
690
|
reconnectAttempt += 1;
|
|
563
691
|
}
|
|
564
692
|
}
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
} catch (error) {
|
|
585
|
-
this.logger.error(`Error sending reply (source msg: scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
};
|
|
693
|
+
waitForReconnectDelay(delay) {
|
|
694
|
+
return new Promise((resolve) => {
|
|
695
|
+
this.resolveReconnectTimer = resolve;
|
|
696
|
+
this.reconnectTimer = setTimeout(() => {
|
|
697
|
+
this.reconnectTimer = void 0;
|
|
698
|
+
this.resolveReconnectTimer = void 0;
|
|
699
|
+
resolve();
|
|
700
|
+
}, delay);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
resolveReconnectDelay() {
|
|
704
|
+
if (this.reconnectTimer) {
|
|
705
|
+
clearTimeout(this.reconnectTimer);
|
|
706
|
+
this.reconnectTimer = void 0;
|
|
707
|
+
}
|
|
708
|
+
const resolve = this.resolveReconnectTimer;
|
|
709
|
+
this.resolveReconnectTimer = void 0;
|
|
710
|
+
resolve?.();
|
|
589
711
|
}
|
|
590
712
|
static fromUrl(baseUrl, options) {
|
|
591
713
|
return new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
|
|
@@ -966,4 +1088,4 @@ let param;
|
|
|
966
1088
|
_param.segment = segment;
|
|
967
1089
|
})(param || (param = {}));
|
|
968
1090
|
//#endregion
|
|
969
|
-
export { Context, Logger, Parameter, Router, combineLogHandlers, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
|
|
1091
|
+
export { Context, Logger, Parameter, Router, combineLogHandlers, createMilkyClient, definePlugin, filter, isDisposable, milkyPackageVersion, milkyVersion, msg, param, seg };
|