@fraqjs/fraq 0.2.0 → 0.3.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/README.md CHANGED
@@ -23,11 +23,8 @@ Fraq 需要在 Node.js 22+ 环境(或其他的支持 WebSocket API 的服务
23
23
  ```typescript
24
24
  import { Context, msg, param } from '@fraqjs/fraq';
25
25
 
26
- const ctx = Context.create({
27
- connect: {
28
- baseUrl: '<替换成你的 Milky 协议端地址>',
29
- // accessToken: '<如果你的 Milky 协议端配置了 token,请在这里添加>',
30
- },
26
+ const ctx = Context.fromUrl('<替换成你的 Milky 协议端地址>', {
27
+ // accessToken: '<如果你的 Milky 协议端配置了 token,请在这里添加>',
31
28
  });
32
29
 
33
30
  ctx.router.command(
package/dist/index.d.mts CHANGED
@@ -5396,21 +5396,27 @@ type EventMap = {
5396
5396
  };
5397
5397
  //#endregion
5398
5398
  //#region src/protocol/client.d.ts
5399
+ interface MilkyEventSubscription {
5400
+ closed: Promise<void>;
5401
+ stop(): void | Promise<void>;
5402
+ }
5403
+ interface MilkyClient extends ApiEndpoints {
5404
+ startEvents(onEvent: (event: Event) => void | Promise<void>): Promise<MilkyEventSubscription>;
5405
+ }
5399
5406
  declare class MilkyClientBase {
5400
5407
  readonly baseUrl: string;
5401
5408
  readonly wsBaseUrl: string;
5402
5409
  private readonly accessToken?;
5403
5410
  private readonly baseHeaders;
5404
5411
  constructor(
5405
- baseUrl: string,
5412
+ baseUrl: string | URL,
5406
5413
  options?: {
5407
5414
  accessToken?: string;
5408
5415
  },
5409
5416
  );
5410
5417
  callApi(endpoint: string, params?: unknown): Promise<unknown>;
5411
- openEventWebSocket(onEvent: (event: MessageEvent) => void): Promise<WebSocket>;
5418
+ startEvents(onEvent: (event: Event) => void | Promise<void>): Promise<MilkyEventSubscription>;
5412
5419
  }
5413
- type MilkyClient = MilkyClientBase & ApiEndpoints;
5414
5420
  declare function createMilkyClient(...params: ConstructorParameters<typeof MilkyClientBase>): MilkyClient;
5415
5421
  //#endregion
5416
5422
  //#region src/routing/tokenizer.d.ts
@@ -5568,19 +5574,22 @@ declare function definePlugin<T extends ParameterList>(plugin: Plugin<T>): Plugi
5568
5574
  //#endregion
5569
5575
  //#region src/core/context.d.ts
5570
5576
  interface ContextOptions {
5571
- connect: {
5572
- baseUrl: string;
5573
- accessToken?: string;
5574
- initialReconnectDelay?: number;
5575
- maxReconnectDelay?: number;
5577
+ reconnect?: {
5578
+ initialDelayMs?: number;
5579
+ maxDelayMs?: number;
5576
5580
  };
5577
5581
  logHandler?: (message: LogMessage) => void;
5578
5582
  }
5583
+ interface ContextUrlOptions {
5584
+ accessToken?: string;
5585
+ }
5579
5586
  declare class Context {
5580
5587
  readonly client: MilkyClient;
5581
5588
  readonly router: Router;
5582
5589
  readonly logger: Logger;
5590
+ readonly name: string;
5583
5591
  private readonly parent?;
5592
+ private readonly filter?;
5584
5593
  private readonly eventBus;
5585
5594
  private readonly plugins;
5586
5595
  private readonly services;
@@ -5596,16 +5605,18 @@ declare class Context {
5596
5605
  resolve<T extends object>(service: ServiceClass<T>): T;
5597
5606
  tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
5598
5607
  isProvided<T extends object>(service: ServiceClass<T>): boolean;
5599
- fork(filter?: Filter): Context;
5608
+ fork(name: string, filter?: Filter): Context;
5600
5609
  start(): Promise<void>;
5610
+ private acceptsParentEvent;
5601
5611
  private applyPlugins;
5602
5612
  private sortPlugins;
5603
5613
  private collectAvailableServices;
5604
5614
  private createUnresolvablePluginError;
5605
5615
  private createProxyContextForPlugin;
5606
- private connectEventWebSocket;
5616
+ private runEventStream;
5607
5617
  private createSession;
5608
- static create(options: ContextOptions): Context;
5618
+ static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
5619
+ static fromClient(client: MilkyClient, options?: ContextOptions): Context;
5609
5620
  }
5610
5621
  //#endregion
5611
5622
  //#region src/protocol/segment.d.ts
@@ -5652,11 +5663,13 @@ export {
5652
5663
  ApiEndpoints,
5653
5664
  Context,
5654
5665
  ContextOptions,
5666
+ ContextUrlOptions,
5655
5667
  EventMap,
5656
5668
  Filter,
5657
5669
  LogMessage,
5658
5670
  Logger,
5659
5671
  MilkyClient,
5672
+ MilkyEventSubscription,
5660
5673
  Parameter,
5661
5674
  ParameterList,
5662
5675
  Plugin,
package/dist/index.mjs CHANGED
@@ -6,7 +6,8 @@ var MilkyClientBase = class {
6
6
  accessToken;
7
7
  baseHeaders;
8
8
  constructor(baseUrl, options) {
9
- this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
9
+ const normalizedBaseUrl = baseUrl.toString();
10
+ this.baseUrl = normalizedBaseUrl.endsWith("/") ? normalizedBaseUrl.slice(0, -1) : normalizedBaseUrl;
10
11
  this.wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
11
12
  this.accessToken = options?.accessToken;
12
13
  this.baseHeaders = { "Content-Type": "application/json" };
@@ -18,18 +19,41 @@ var MilkyClientBase = class {
18
19
  body: JSON.stringify(params ?? {}),
19
20
  headers: this.baseHeaders
20
21
  });
21
- if (!response.ok) throw new Error(`API call failed with status ${response.status}`);
22
+ if (!response.ok) throw new Error(`API call ${endpoint} failed with HTTP status ${response.status}`);
22
23
  const json = await response.json();
23
- if (json.status === "failed") throw new Error(`API call failed with retcode ${json.retcode}: ${json.message}`);
24
+ if (json.status === "failed") throw new Error(`API call ${endpoint} failed with retcode ${json.retcode}: ${json.message}`);
24
25
  return json.data;
25
26
  }
26
- async openEventWebSocket(onEvent) {
27
+ async startEvents(onEvent) {
27
28
  const ws = new WebSocket(`${this.wsBaseUrl}/event${this.accessToken ? `?access_token=${this.accessToken}` : ""}`);
28
- ws.addEventListener("message", onEvent);
29
- return new Promise((resolve, reject) => {
30
- ws.addEventListener("open", () => resolve(ws));
29
+ let closeSubscription = () => {};
30
+ const closed = new Promise((resolve, reject) => {
31
+ closeSubscription = (error) => {
32
+ if (error) reject(error);
33
+ else resolve();
34
+ };
35
+ });
36
+ ws.addEventListener("message", async (event) => {
37
+ try {
38
+ if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
39
+ await onEvent(JSON.parse(event.data));
40
+ } catch (error) {
41
+ closeSubscription(error);
42
+ ws.close();
43
+ }
44
+ });
45
+ await new Promise((resolve, reject) => {
46
+ ws.addEventListener("open", () => resolve(), { once: true });
31
47
  ws.addEventListener("error", (event) => reject(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
32
48
  });
49
+ ws.addEventListener("error", (event) => closeSubscription(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
50
+ ws.addEventListener("close", () => closeSubscription(), { once: true });
51
+ return {
52
+ closed,
53
+ stop() {
54
+ ws.close();
55
+ }
56
+ };
33
57
  }
34
58
  };
35
59
  function createMilkyClient(...params) {
@@ -288,8 +312,10 @@ const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
288
312
  var Context = class Context {
289
313
  client;
290
314
  router = new Router();
291
- logger = new Logger((message) => this.logHandler?.(message), "context");
315
+ logger;
316
+ name;
292
317
  parent;
318
+ filter;
293
319
  eventBus = mitt();
294
320
  plugins = [];
295
321
  services = /* @__PURE__ */ new Map();
@@ -298,24 +324,24 @@ var Context = class Context {
298
324
  maxReconnectDelayMs;
299
325
  logHandler;
300
326
  isStarted = false;
301
- constructor(client, options, parent, filter) {
327
+ constructor(client, options, name, parent, filter) {
302
328
  this.client = client;
303
- this.initialReconnectDelayMs = options?.connect.initialReconnectDelay ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
304
- this.maxReconnectDelayMs = options?.connect.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
329
+ this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
330
+ this.maxReconnectDelayMs = options?.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
305
331
  this.logHandler = options?.logHandler ?? parent?.logHandler;
332
+ this.name = name ?? "root";
333
+ this.logger = new Logger((message) => this.logHandler?.(message), this.name);
306
334
  this.parent = parent;
335
+ this.filter = filter;
307
336
  parent?.eventBus.on("*", (type, event) => {
308
- if (filter) {
309
- const predicate = filter[type];
310
- if (predicate && !predicate(event)) return;
311
- }
337
+ if (!this.acceptsParentEvent(type, event)) return;
312
338
  this.eventBus.emit(type, event);
313
339
  });
314
340
  this.eventBus.on("message_receive", async ({ data: message }) => {
315
341
  try {
316
- this.router.dispatch(this.createSession(message), message);
342
+ await this.router.dispatch(this.createSession(message), message);
317
343
  } catch (error) {
318
- console.error("Error routing command:", error);
344
+ this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
319
345
  }
320
346
  });
321
347
  }
@@ -324,7 +350,7 @@ var Context = class Context {
324
350
  try {
325
351
  await handler(event);
326
352
  } catch (error) {
327
- console.error(`Error handling event ${type}:`, error);
353
+ this.logger.error(`Error handling event ${type}`, error);
328
354
  }
329
355
  });
330
356
  }
@@ -350,8 +376,8 @@ var Context = class Context {
350
376
  isProvided(service) {
351
377
  return this.tryResolve(service) !== void 0;
352
378
  }
353
- fork(filter) {
354
- const subContext = new Context(this.client, void 0, this, filter);
379
+ fork(name, filter) {
380
+ const subContext = new Context(this.client, void 0, name, this, filter);
355
381
  this.subContexts.push(subContext);
356
382
  return subContext;
357
383
  }
@@ -360,13 +386,20 @@ var Context = class Context {
360
386
  await this.applyPlugins();
361
387
  for (const subContext of this.subContexts) await subContext.start();
362
388
  this.isStarted = true;
363
- if (!this.parent) this.connectEventWebSocket();
389
+ if (!this.parent) this.runEventStream();
390
+ }
391
+ acceptsParentEvent(type, event) {
392
+ if (!this.filter) return true;
393
+ const predicate = this.filter[type];
394
+ return predicate?.(event) === true;
364
395
  }
365
396
  async applyPlugins() {
366
397
  for (const { plugin, args } of this.sortPlugins()) {
367
398
  const providedBeforeApply = new Set(this.services.keys());
399
+ this.logger.info(`Applying plugin ${plugin.name}`);
368
400
  await plugin.apply(this.createProxyContextForPlugin(plugin), ...args);
369
401
  for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${plugin.name} declares service ${getServiceName(service)} but did not provide it.`);
402
+ this.logger.debug(`Applied plugin ${plugin.name}`);
370
403
  }
371
404
  }
372
405
  sortPlugins() {
@@ -417,25 +450,21 @@ var Context = class Context {
417
450
  else return target[prop];
418
451
  } });
419
452
  }
420
- async connectEventWebSocket() {
453
+ async runEventStream() {
421
454
  let reconnectDelay = this.initialReconnectDelayMs;
422
455
  while (this.isStarted) {
423
456
  try {
424
- const ws = await this.client.openEventWebSocket((event) => {
457
+ const subscription = await this.client.startEvents((event) => {
425
458
  try {
426
- if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
427
- const payload = JSON.parse(event.data);
428
- this.eventBus.emit(payload.event_type, payload);
459
+ this.eventBus.emit(event.event_type, event);
429
460
  } catch (error) {
430
- console.error("Error handling WebSocket event:", error);
461
+ this.logger.error("Error handling event stream event", error);
431
462
  }
432
463
  });
433
464
  reconnectDelay = this.initialReconnectDelayMs;
434
- await new Promise((resolve) => {
435
- ws.addEventListener("close", () => resolve(), { once: true });
436
- });
465
+ await subscription.closed;
437
466
  } catch (error) {
438
- console.error("Error connecting event WebSocket:", error);
467
+ this.logger.error("Error connecting event stream", error);
439
468
  }
440
469
  await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
441
470
  reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
@@ -461,13 +490,16 @@ var Context = class Context {
461
490
  break;
462
491
  }
463
492
  } catch (error) {
464
- console.error("Error sending reply:", error);
493
+ 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);
465
494
  }
466
495
  }
467
496
  };
468
497
  }
469
- static create(options) {
470
- return new Context(createMilkyClient(options.connect.baseUrl, { accessToken: options.connect.accessToken }), options);
498
+ static fromUrl(baseUrl, options) {
499
+ return new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
500
+ }
501
+ static fromClient(client, options) {
502
+ return new Context(client, options);
471
503
  }
472
504
  };
473
505
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"
@@ -23,7 +23,7 @@
23
23
  "node": ">=22"
24
24
  },
25
25
  "scripts": {
26
- "build": "tsdown && node scripts/format-dts-comments.mjs && biome format --vcs-enabled=false --write dist/index.d.mts",
26
+ "build": "tsdown && tsx scripts/format-dts-comments.ts && biome format --vcs-enabled=false --write dist/index.d.mts",
27
27
  "test": "tsx --test test/*.test.ts"
28
28
  }
29
29
  }