@fraqjs/fraq 0.5.1 → 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 +23 -3
- package/dist/index.mjs +143 -21
- 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
|
|
@@ -5688,6 +5699,11 @@ declare class Context {
|
|
|
5688
5699
|
fork(name: string, filter?: Filter): Context;
|
|
5689
5700
|
createSession(message: IncomingMessage): Session;
|
|
5690
5701
|
start(): Promise<void>;
|
|
5702
|
+
stop(): Promise<void>;
|
|
5703
|
+
private startInternal;
|
|
5704
|
+
private getState;
|
|
5705
|
+
private stopInternal;
|
|
5706
|
+
private stopEventStream;
|
|
5691
5707
|
private acceptsParentEvent;
|
|
5692
5708
|
private applyPlugins;
|
|
5693
5709
|
private sortPlugins;
|
|
@@ -5698,6 +5714,8 @@ declare class Context {
|
|
|
5698
5714
|
private createUnresolvablePluginError;
|
|
5699
5715
|
private createProxyContextForPlugin;
|
|
5700
5716
|
private runEventStream;
|
|
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) {
|
|
@@ -465,11 +481,88 @@ var Context = class Context {
|
|
|
465
481
|
};
|
|
466
482
|
}
|
|
467
483
|
async start() {
|
|
468
|
-
if (this.
|
|
469
|
-
|
|
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
|
+
}
|
|
470
524
|
for (const subContext of this.subContexts) await subContext.start();
|
|
471
|
-
this.
|
|
472
|
-
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
|
+
}
|
|
473
566
|
}
|
|
474
567
|
acceptsParentEvent(type, event) {
|
|
475
568
|
if (!this.filter) return true;
|
|
@@ -481,7 +574,7 @@ var Context = class Context {
|
|
|
481
574
|
const providedBeforeApply = new Set(this.services.keys());
|
|
482
575
|
this.logger.info(`Applying plugin ${plugin.name}`);
|
|
483
576
|
await plugin.apply(this.createProxyContextForPlugin(plugin), ...args);
|
|
484
|
-
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.`);
|
|
485
578
|
this.logger.debug(`Applied plugin ${plugin.name}`);
|
|
486
579
|
}
|
|
487
580
|
}
|
|
@@ -489,7 +582,7 @@ var Context = class Context {
|
|
|
489
582
|
const providers = /* @__PURE__ */ new Map();
|
|
490
583
|
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
491
584
|
const existingProvider = providers.get(service);
|
|
492
|
-
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}.`);
|
|
493
586
|
providers.set(service, plugin);
|
|
494
587
|
}
|
|
495
588
|
const pending = [...this.plugins];
|
|
@@ -540,7 +633,7 @@ var Context = class Context {
|
|
|
540
633
|
const lines = [...missingRequirements].map(([service, plugins]) => {
|
|
541
634
|
const dependents = plugins.map((plugin) => plugin.name).join(", ");
|
|
542
635
|
const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
|
|
543
|
-
return `${
|
|
636
|
+
return `${service.name} required by ${dependents} (${reason})`;
|
|
544
637
|
});
|
|
545
638
|
return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
|
|
546
639
|
}
|
|
@@ -564,29 +657,58 @@ var Context = class Context {
|
|
|
564
657
|
async runEventStream() {
|
|
565
658
|
let reconnectDelay = this.initialReconnectDelayMs;
|
|
566
659
|
let reconnectAttempt = 1;
|
|
567
|
-
while (this.
|
|
660
|
+
while (this.state === "started") {
|
|
568
661
|
try {
|
|
569
662
|
this.logger.debug(`Connecting event stream (attempt=${reconnectAttempt})`);
|
|
570
663
|
const subscription = await this.client.startEvents((event) => {
|
|
571
664
|
try {
|
|
665
|
+
if (this.state !== "started") return;
|
|
572
666
|
this.eventBus.emit(event.event_type, event);
|
|
573
667
|
} catch (error) {
|
|
574
668
|
this.logger.error("Error handling event stream event", error);
|
|
575
669
|
}
|
|
576
670
|
});
|
|
671
|
+
if (this.state !== "started") {
|
|
672
|
+
await subscription.stop();
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
this.eventSubscription = subscription;
|
|
577
676
|
this.logger.info("Event stream connected");
|
|
578
677
|
reconnectDelay = this.initialReconnectDelayMs;
|
|
579
678
|
reconnectAttempt = 1;
|
|
580
679
|
await subscription.closed;
|
|
680
|
+
this.eventSubscription = void 0;
|
|
681
|
+
if (this.state !== "started") break;
|
|
581
682
|
this.logger.warn(`Event stream disconnected; reconnecting in ${reconnectDelay}ms`);
|
|
582
683
|
} catch (error) {
|
|
684
|
+
this.eventSubscription = void 0;
|
|
685
|
+
if (this.state !== "started") break;
|
|
583
686
|
this.logger.error(`Error connecting event stream; reconnecting in ${reconnectDelay}ms`, error);
|
|
584
687
|
}
|
|
585
|
-
await
|
|
688
|
+
await this.waitForReconnectDelay(reconnectDelay);
|
|
586
689
|
reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
|
|
587
690
|
reconnectAttempt += 1;
|
|
588
691
|
}
|
|
589
692
|
}
|
|
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?.();
|
|
711
|
+
}
|
|
590
712
|
static fromUrl(baseUrl, options) {
|
|
591
713
|
return new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
|
|
592
714
|
}
|
|
@@ -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 };
|