@fraqjs/fraq 0.5.1 → 0.6.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 +25 -3
- package/dist/index.mjs +155 -23
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -5620,6 +5620,11 @@ 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;
|
|
5627
|
+
declare function implementsESNextDisposable(service: object): boolean;
|
|
5623
5628
|
//#endregion
|
|
5624
5629
|
//#region src/core/plugin.d.ts
|
|
5625
5630
|
type ParameterList = Array<any>;
|
|
@@ -5671,12 +5676,19 @@ declare class Context {
|
|
|
5671
5676
|
private readonly plugins;
|
|
5672
5677
|
private readonly services;
|
|
5673
5678
|
private readonly subContexts;
|
|
5679
|
+
private readonly parentEventForwarder?;
|
|
5674
5680
|
private readonly initialReconnectDelayMs;
|
|
5675
5681
|
private readonly maxReconnectDelayMs;
|
|
5676
5682
|
private readonly logHandler?;
|
|
5677
|
-
private
|
|
5683
|
+
private state;
|
|
5684
|
+
private startPromise?;
|
|
5685
|
+
private stopPromise?;
|
|
5686
|
+
private eventSubscription?;
|
|
5687
|
+
private eventStreamTask?;
|
|
5688
|
+
private reconnectTimer?;
|
|
5689
|
+
private resolveReconnectTimer?;
|
|
5678
5690
|
private constructor();
|
|
5679
|
-
on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): void;
|
|
5691
|
+
on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): () => void;
|
|
5680
5692
|
install<T extends ParameterList, I extends Injection | undefined, OI extends Injection | undefined>(
|
|
5681
5693
|
plugin: Plugin<T, I, OI>,
|
|
5682
5694
|
...args: T
|
|
@@ -5688,6 +5700,11 @@ declare class Context {
|
|
|
5688
5700
|
fork(name: string, filter?: Filter): Context;
|
|
5689
5701
|
createSession(message: IncomingMessage): Session;
|
|
5690
5702
|
start(): Promise<void>;
|
|
5703
|
+
stop(): Promise<void>;
|
|
5704
|
+
private startInternal;
|
|
5705
|
+
private getState;
|
|
5706
|
+
private stopInternal;
|
|
5707
|
+
private stopEventStream;
|
|
5691
5708
|
private acceptsParentEvent;
|
|
5692
5709
|
private applyPlugins;
|
|
5693
5710
|
private sortPlugins;
|
|
@@ -5698,6 +5715,8 @@ declare class Context {
|
|
|
5698
5715
|
private createUnresolvablePluginError;
|
|
5699
5716
|
private createProxyContextForPlugin;
|
|
5700
5717
|
private runEventStream;
|
|
5718
|
+
private waitForReconnectDelay;
|
|
5719
|
+
private resolveReconnectDelay;
|
|
5701
5720
|
static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
|
|
5702
5721
|
static fromClient(client: MilkyClient, options?: ContextOptions): Context;
|
|
5703
5722
|
}
|
|
@@ -5748,6 +5767,7 @@ export {
|
|
|
5748
5767
|
Context,
|
|
5749
5768
|
ContextOptions,
|
|
5750
5769
|
ContextUrlOptions,
|
|
5770
|
+
Disposable,
|
|
5751
5771
|
EventMap,
|
|
5752
5772
|
Filter,
|
|
5753
5773
|
Handler,
|
|
@@ -5768,13 +5788,15 @@ export {
|
|
|
5768
5788
|
RouteEntry,
|
|
5769
5789
|
RouteMatchResult,
|
|
5770
5790
|
Router,
|
|
5771
|
-
|
|
5791
|
+
ServiceClass,
|
|
5772
5792
|
Session,
|
|
5773
5793
|
SessionPredicate,
|
|
5774
5794
|
combineLogHandlers,
|
|
5775
5795
|
createMilkyClient,
|
|
5776
5796
|
definePlugin,
|
|
5777
5797
|
filter,
|
|
5798
|
+
implementsESNextDisposable,
|
|
5799
|
+
isDisposable,
|
|
5778
5800
|
type types_d_exports as milky,
|
|
5779
5801
|
milkyPackageVersion,
|
|
5780
5802
|
milkyVersion,
|
package/dist/index.mjs
CHANGED
|
@@ -360,8 +360,11 @@ 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
|
+
}
|
|
366
|
+
function implementsESNextDisposable(service) {
|
|
367
|
+
return Symbol.dispose in service && typeof service[Symbol.dispose] === "function";
|
|
365
368
|
}
|
|
366
369
|
//#endregion
|
|
367
370
|
//#region src/core/context.ts
|
|
@@ -378,10 +381,17 @@ var Context = class Context {
|
|
|
378
381
|
plugins = [];
|
|
379
382
|
services = /* @__PURE__ */ new Map();
|
|
380
383
|
subContexts = [];
|
|
384
|
+
parentEventForwarder;
|
|
381
385
|
initialReconnectDelayMs;
|
|
382
386
|
maxReconnectDelayMs;
|
|
383
387
|
logHandler;
|
|
384
|
-
|
|
388
|
+
state = "idle";
|
|
389
|
+
startPromise;
|
|
390
|
+
stopPromise;
|
|
391
|
+
eventSubscription;
|
|
392
|
+
eventStreamTask;
|
|
393
|
+
reconnectTimer;
|
|
394
|
+
resolveReconnectTimer;
|
|
385
395
|
constructor(client, options, name, parent, filter) {
|
|
386
396
|
this.client = client;
|
|
387
397
|
this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
|
|
@@ -391,12 +401,16 @@ var Context = class Context {
|
|
|
391
401
|
this.logger = new Logger((message) => this.logHandler?.(message), this.name);
|
|
392
402
|
this.parent = parent;
|
|
393
403
|
this.filter = filter;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
404
|
+
if (parent) {
|
|
405
|
+
this.parentEventForwarder = (type, event) => {
|
|
406
|
+
if (!this.acceptsParentEvent(type, event)) return;
|
|
407
|
+
this.eventBus.emit(type, event);
|
|
408
|
+
};
|
|
409
|
+
parent.eventBus.on("*", this.parentEventForwarder);
|
|
410
|
+
}
|
|
398
411
|
this.eventBus.on("message_receive", async ({ data: message }) => {
|
|
399
412
|
try {
|
|
413
|
+
if (this.state === "stopping" || this.state === "stopped") return;
|
|
400
414
|
await this.router.dispatch(this.createSession(message), message);
|
|
401
415
|
} catch (error) {
|
|
402
416
|
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 +418,18 @@ var Context = class Context {
|
|
|
404
418
|
});
|
|
405
419
|
}
|
|
406
420
|
on(type, handler) {
|
|
407
|
-
|
|
421
|
+
const wrappedHandler = async (event) => {
|
|
408
422
|
try {
|
|
423
|
+
if (this.state === "stopping" || this.state === "stopped") return;
|
|
409
424
|
await handler(event);
|
|
410
425
|
} catch (error) {
|
|
411
426
|
this.logger.error(`Error handling event ${type}`, error);
|
|
412
427
|
}
|
|
413
|
-
}
|
|
428
|
+
};
|
|
429
|
+
this.eventBus.on(type, wrappedHandler);
|
|
430
|
+
return () => {
|
|
431
|
+
this.eventBus.off(type, wrappedHandler);
|
|
432
|
+
};
|
|
414
433
|
}
|
|
415
434
|
install(plugin, ...args) {
|
|
416
435
|
this.plugins.push({
|
|
@@ -419,12 +438,20 @@ var Context = class Context {
|
|
|
419
438
|
});
|
|
420
439
|
}
|
|
421
440
|
provide(service, instance) {
|
|
422
|
-
if (this.services.has(service)) throw new Error(`Service ${
|
|
441
|
+
if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
|
|
442
|
+
if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
|
|
443
|
+
Service ${service.name} implements ESNext Disposable but not Fraq Disposable.
|
|
444
|
+
Please explicitly import the interface like this:
|
|
445
|
+
|
|
446
|
+
import type { Disposable } from '@fraqjs/fraq';
|
|
447
|
+
|
|
448
|
+
and implement the dispose method to clean up resources when the context stops.
|
|
449
|
+
`.trim());
|
|
423
450
|
this.services.set(service, instance);
|
|
424
451
|
}
|
|
425
452
|
resolve(service) {
|
|
426
453
|
const instance = this.tryResolve(service);
|
|
427
|
-
if (instance === void 0) throw new Error(`Service ${
|
|
454
|
+
if (instance === void 0) throw new Error(`Service ${service.name} has not been provided.`);
|
|
428
455
|
return instance;
|
|
429
456
|
}
|
|
430
457
|
tryResolve(service) {
|
|
@@ -465,11 +492,88 @@ var Context = class Context {
|
|
|
465
492
|
};
|
|
466
493
|
}
|
|
467
494
|
async start() {
|
|
468
|
-
if (this.
|
|
469
|
-
|
|
495
|
+
if (this.state === "started") return;
|
|
496
|
+
if (this.state === "starting") {
|
|
497
|
+
await this.startPromise;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (this.state === "stopping") throw new Error(`Context "${this.name}" cannot be started while it is stopping.`);
|
|
501
|
+
if (this.state === "stopped") throw new Error(`Context "${this.name}" cannot be restarted after it has been stopped.`);
|
|
502
|
+
this.state = "starting";
|
|
503
|
+
this.startPromise = this.startInternal();
|
|
504
|
+
try {
|
|
505
|
+
await this.startPromise;
|
|
506
|
+
} finally {
|
|
507
|
+
this.startPromise = void 0;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async stop() {
|
|
511
|
+
if (this.state === "idle" || this.state === "stopped") return;
|
|
512
|
+
if (this.state === "starting") await this.startPromise;
|
|
513
|
+
const stateAfterStart = this.getState();
|
|
514
|
+
if (stateAfterStart === "idle" || stateAfterStart === "stopped") return;
|
|
515
|
+
if (stateAfterStart === "stopping") {
|
|
516
|
+
await this.stopPromise;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
this.state = "stopping";
|
|
520
|
+
this.stopPromise = this.stopInternal();
|
|
521
|
+
try {
|
|
522
|
+
await this.stopPromise;
|
|
523
|
+
} finally {
|
|
524
|
+
this.state = "stopped";
|
|
525
|
+
this.stopPromise = void 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async startInternal() {
|
|
529
|
+
try {
|
|
530
|
+
await this.applyPlugins();
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.state = "idle";
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
470
535
|
for (const subContext of this.subContexts) await subContext.start();
|
|
471
|
-
this.
|
|
472
|
-
if (!this.parent) this.runEventStream();
|
|
536
|
+
this.state = "started";
|
|
537
|
+
if (!this.parent) this.eventStreamTask = this.runEventStream();
|
|
538
|
+
}
|
|
539
|
+
getState() {
|
|
540
|
+
return this.state;
|
|
541
|
+
}
|
|
542
|
+
async stopInternal() {
|
|
543
|
+
const errors = [];
|
|
544
|
+
await this.stopEventStream(errors);
|
|
545
|
+
for (const subContext of [...this.subContexts].reverse()) try {
|
|
546
|
+
await subContext.stop();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
errors.push(error);
|
|
549
|
+
}
|
|
550
|
+
if (this.parent && this.parentEventForwarder) this.parent.eventBus.off("*", this.parentEventForwarder);
|
|
551
|
+
for (const service of [...this.services.values()].reverse()) {
|
|
552
|
+
if (!isDisposable(service)) continue;
|
|
553
|
+
try {
|
|
554
|
+
await service.dispose();
|
|
555
|
+
} catch (error) {
|
|
556
|
+
errors.push(error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (errors.length === 1) throw errors[0];
|
|
560
|
+
if (errors.length > 1) throw new AggregateError(errors, `Context "${this.name}" failed to stop cleanly.`);
|
|
561
|
+
}
|
|
562
|
+
async stopEventStream(errors) {
|
|
563
|
+
this.resolveReconnectDelay();
|
|
564
|
+
const subscription = this.eventSubscription;
|
|
565
|
+
if (subscription) try {
|
|
566
|
+
await subscription.stop();
|
|
567
|
+
} catch (error) {
|
|
568
|
+
errors.push(error);
|
|
569
|
+
}
|
|
570
|
+
if (this.eventStreamTask) try {
|
|
571
|
+
await this.eventStreamTask;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
errors.push(error);
|
|
574
|
+
} finally {
|
|
575
|
+
this.eventStreamTask = void 0;
|
|
576
|
+
}
|
|
473
577
|
}
|
|
474
578
|
acceptsParentEvent(type, event) {
|
|
475
579
|
if (!this.filter) return true;
|
|
@@ -479,17 +583,16 @@ var Context = class Context {
|
|
|
479
583
|
async applyPlugins() {
|
|
480
584
|
for (const { plugin, args } of this.sortPlugins()) {
|
|
481
585
|
const providedBeforeApply = new Set(this.services.keys());
|
|
482
|
-
this.logger.
|
|
586
|
+
this.logger.debug(`Applying plugin ${plugin.name}`);
|
|
483
587
|
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 ${
|
|
485
|
-
this.logger.debug(`Applied plugin ${plugin.name}`);
|
|
588
|
+
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.`);
|
|
486
589
|
}
|
|
487
590
|
}
|
|
488
591
|
sortPlugins() {
|
|
489
592
|
const providers = /* @__PURE__ */ new Map();
|
|
490
593
|
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
491
594
|
const existingProvider = providers.get(service);
|
|
492
|
-
if (existingProvider) throw new Error(`Service ${
|
|
595
|
+
if (existingProvider) throw new Error(`Service ${service.name} is declared by multiple plugins: ${existingProvider.name} and ${plugin.name}.`);
|
|
493
596
|
providers.set(service, plugin);
|
|
494
597
|
}
|
|
495
598
|
const pending = [...this.plugins];
|
|
@@ -540,7 +643,7 @@ var Context = class Context {
|
|
|
540
643
|
const lines = [...missingRequirements].map(([service, plugins]) => {
|
|
541
644
|
const dependents = plugins.map((plugin) => plugin.name).join(", ");
|
|
542
645
|
const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
|
|
543
|
-
return `${
|
|
646
|
+
return `${service.name} required by ${dependents} (${reason})`;
|
|
544
647
|
});
|
|
545
648
|
return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
|
|
546
649
|
}
|
|
@@ -564,29 +667,58 @@ var Context = class Context {
|
|
|
564
667
|
async runEventStream() {
|
|
565
668
|
let reconnectDelay = this.initialReconnectDelayMs;
|
|
566
669
|
let reconnectAttempt = 1;
|
|
567
|
-
while (this.
|
|
670
|
+
while (this.state === "started") {
|
|
568
671
|
try {
|
|
569
672
|
this.logger.debug(`Connecting event stream (attempt=${reconnectAttempt})`);
|
|
570
673
|
const subscription = await this.client.startEvents((event) => {
|
|
571
674
|
try {
|
|
675
|
+
if (this.state !== "started") return;
|
|
572
676
|
this.eventBus.emit(event.event_type, event);
|
|
573
677
|
} catch (error) {
|
|
574
678
|
this.logger.error("Error handling event stream event", error);
|
|
575
679
|
}
|
|
576
680
|
});
|
|
681
|
+
if (this.state !== "started") {
|
|
682
|
+
await subscription.stop();
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
this.eventSubscription = subscription;
|
|
577
686
|
this.logger.info("Event stream connected");
|
|
578
687
|
reconnectDelay = this.initialReconnectDelayMs;
|
|
579
688
|
reconnectAttempt = 1;
|
|
580
689
|
await subscription.closed;
|
|
690
|
+
this.eventSubscription = void 0;
|
|
691
|
+
if (this.state !== "started") break;
|
|
581
692
|
this.logger.warn(`Event stream disconnected; reconnecting in ${reconnectDelay}ms`);
|
|
582
693
|
} catch (error) {
|
|
694
|
+
this.eventSubscription = void 0;
|
|
695
|
+
if (this.state !== "started") break;
|
|
583
696
|
this.logger.error(`Error connecting event stream; reconnecting in ${reconnectDelay}ms`, error);
|
|
584
697
|
}
|
|
585
|
-
await
|
|
698
|
+
await this.waitForReconnectDelay(reconnectDelay);
|
|
586
699
|
reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
|
|
587
700
|
reconnectAttempt += 1;
|
|
588
701
|
}
|
|
589
702
|
}
|
|
703
|
+
waitForReconnectDelay(delay) {
|
|
704
|
+
return new Promise((resolve) => {
|
|
705
|
+
this.resolveReconnectTimer = resolve;
|
|
706
|
+
this.reconnectTimer = setTimeout(() => {
|
|
707
|
+
this.reconnectTimer = void 0;
|
|
708
|
+
this.resolveReconnectTimer = void 0;
|
|
709
|
+
resolve();
|
|
710
|
+
}, delay);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
resolveReconnectDelay() {
|
|
714
|
+
if (this.reconnectTimer) {
|
|
715
|
+
clearTimeout(this.reconnectTimer);
|
|
716
|
+
this.reconnectTimer = void 0;
|
|
717
|
+
}
|
|
718
|
+
const resolve = this.resolveReconnectTimer;
|
|
719
|
+
this.resolveReconnectTimer = void 0;
|
|
720
|
+
resolve?.();
|
|
721
|
+
}
|
|
590
722
|
static fromUrl(baseUrl, options) {
|
|
591
723
|
return new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
|
|
592
724
|
}
|
|
@@ -966,4 +1098,4 @@ let param;
|
|
|
966
1098
|
_param.segment = segment;
|
|
967
1099
|
})(param || (param = {}));
|
|
968
1100
|
//#endregion
|
|
969
|
-
export { Context, Logger, Parameter, Router, combineLogHandlers, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
|
|
1101
|
+
export { Context, Logger, Parameter, Router, combineLogHandlers, createMilkyClient, definePlugin, filter, implementsESNextDisposable, isDisposable, milkyPackageVersion, milkyVersion, msg, param, seg };
|