@fraqjs/fraq 0.3.3 → 0.5.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 CHANGED
@@ -5507,6 +5507,58 @@ type Pattern = Record<string, Parameter<any>>;
5507
5507
  type ParamsOf<P extends Pattern> = { [K in keyof P]: P[K] extends Parameter<infer T> ? T : never };
5508
5508
  type Handler<P extends Pattern> = (session: Session, params: ParamsOf<P>) => void | Promise<void>;
5509
5509
  type SessionPredicate = (session: Session) => boolean;
5510
+ interface Command {
5511
+ name: string;
5512
+ pattern: Pattern;
5513
+ handler: (session: Session, params: any) => void | Promise<void>;
5514
+ }
5515
+ interface RawPattern {
5516
+ pattern: Pattern;
5517
+ handler: (session: Session, params: any) => void | Promise<void>;
5518
+ }
5519
+ type RouteEntry =
5520
+ | {
5521
+ type: 'command';
5522
+ command: Command;
5523
+ }
5524
+ | {
5525
+ type: 'group';
5526
+ name: string;
5527
+ router: Router;
5528
+ }
5529
+ | {
5530
+ type: 'filter';
5531
+ predicate: SessionPredicate;
5532
+ router: Router;
5533
+ }
5534
+ | {
5535
+ type: 'rawPattern';
5536
+ rawPattern: RawPattern;
5537
+ };
5538
+ type RouteBranch =
5539
+ | {
5540
+ type: 'command';
5541
+ path: string[];
5542
+ command: Command;
5543
+ }
5544
+ | {
5545
+ type: 'rawPattern';
5546
+ path: string[];
5547
+ rawPattern: RawPattern;
5548
+ };
5549
+ type RouteMatchResult =
5550
+ | {
5551
+ type: 'command';
5552
+ path: string[];
5553
+ command: Command;
5554
+ params: any;
5555
+ }
5556
+ | {
5557
+ type: 'rawPattern';
5558
+ path: string[];
5559
+ rawPattern: RawPattern;
5560
+ params: any;
5561
+ };
5510
5562
  interface Session {
5511
5563
  raw: IncomingMessage;
5512
5564
  reply(segments: OutgoingSegment_ZodInput[]): Promise<void>;
@@ -5518,12 +5570,16 @@ declare class Router {
5518
5570
  rawPattern<P extends Pattern>(pattern: P, handler: Handler<P>): this;
5519
5571
  group(name: string): Router;
5520
5572
  filter(predicate: SessionPredicate): Router;
5573
+ routes(): RouteEntry[];
5574
+ branches(session: Session): RouteBranch[];
5575
+ match(session: Session, message: IncomingMessage): RouteMatchResult | undefined;
5521
5576
  dispatch(session: Session, message: IncomingMessage): Promise<boolean>;
5522
- private process;
5523
- private processEntry;
5524
- private processCommand;
5525
- private processGroup;
5526
- private processRawPattern;
5577
+ private matchFrom;
5578
+ private matchEntry;
5579
+ private matchCommand;
5580
+ private matchGroup;
5581
+ private matchRawPattern;
5582
+ private branchesFrom;
5527
5583
  private capturePattern;
5528
5584
  }
5529
5585
  //#endregion
@@ -5567,13 +5623,31 @@ type ServiceClass<T extends object = object> = abstract new (...args: any[]) =>
5567
5623
  //#endregion
5568
5624
  //#region src/core/plugin.d.ts
5569
5625
  type ParameterList = Array<any>;
5570
- interface Plugin<T extends ParameterList> {
5626
+ type Injection = Record<string, ServiceClass>;
5627
+ type InjectedServices<I extends Injection | undefined> = I extends Injection
5628
+ ? { [K in keyof I]: InstanceType<I[K]> }
5629
+ : {};
5630
+ type OptionalInjectedServices<I extends Injection | undefined> = I extends Injection
5631
+ ? { [K in keyof I]: InstanceType<I[K]> | undefined }
5632
+ : {};
5633
+ interface Plugin<
5634
+ T extends ParameterList,
5635
+ I extends Injection | undefined,
5636
+ OI extends Injection | undefined = undefined,
5637
+ > {
5571
5638
  name: string;
5572
5639
  requires?: readonly ServiceClass[];
5640
+ inject?: I;
5641
+ optionalRequires?: readonly ServiceClass[];
5642
+ optionalInject?: OI;
5573
5643
  provides?: readonly ServiceClass[];
5574
- apply(ctx: Context, ...args: T): void | Promise<void>;
5644
+ apply(ctx: Context & InjectedServices<I> & OptionalInjectedServices<OI>, ...args: T): void | Promise<void>;
5575
5645
  }
5576
- declare function definePlugin<T extends ParameterList>(plugin: Plugin<T>): Plugin<T>;
5646
+ declare function definePlugin<
5647
+ T extends ParameterList,
5648
+ I extends Injection | undefined,
5649
+ OI extends Injection | undefined,
5650
+ >(plugin: Plugin<T, I, OI>): Plugin<T, I, OI>;
5577
5651
  //#endregion
5578
5652
  //#region src/core/context.d.ts
5579
5653
  interface ContextOptions {
@@ -5603,7 +5677,10 @@ declare class Context {
5603
5677
  private isStarted;
5604
5678
  private constructor();
5605
5679
  on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): void;
5606
- install<T extends ParameterList>(plugin: Plugin<T>, ...args: T): void;
5680
+ install<T extends ParameterList, I extends Injection | undefined, OI extends Injection | undefined>(
5681
+ plugin: Plugin<T, I, OI>,
5682
+ ...args: T
5683
+ ): void;
5607
5684
  provide<T extends object>(service: ServiceClass<T>, instance: T): void;
5608
5685
  resolve<T extends object>(service: ServiceClass<T>): T;
5609
5686
  tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
@@ -5613,6 +5690,9 @@ declare class Context {
5613
5690
  private acceptsParentEvent;
5614
5691
  private applyPlugins;
5615
5692
  private sortPlugins;
5693
+ private collectPendingProvidedServices;
5694
+ private areRequiredServicesAvailable;
5695
+ private areOptionalServicesReady;
5616
5696
  private collectAvailableServices;
5617
5697
  private createUnresolvablePluginError;
5618
5698
  private createProxyContextForPlugin;
@@ -5664,11 +5744,14 @@ declare namespace seg {
5664
5744
  //#endregion
5665
5745
  export {
5666
5746
  ApiEndpoints,
5747
+ Command,
5667
5748
  Context,
5668
5749
  ContextOptions,
5669
5750
  ContextUrlOptions,
5670
5751
  EventMap,
5671
5752
  Filter,
5753
+ Handler,
5754
+ Injection,
5672
5755
  LogHandler,
5673
5756
  LogLevel,
5674
5757
  LogMessage,
@@ -5677,10 +5760,17 @@ export {
5677
5760
  MilkyEventSubscription,
5678
5761
  Parameter,
5679
5762
  ParameterList,
5763
+ ParamsOf,
5764
+ Pattern,
5680
5765
  Plugin,
5766
+ RawPattern,
5767
+ RouteBranch,
5768
+ RouteEntry,
5769
+ RouteMatchResult,
5681
5770
  Router,
5682
5771
  type ServiceClass,
5683
5772
  Session,
5773
+ SessionPredicate,
5684
5774
  combineLogHandlers,
5685
5775
  createMilkyClient,
5686
5776
  definePlugin,
package/dist/index.mjs CHANGED
@@ -211,49 +211,102 @@ var Router = class Router {
211
211
  });
212
212
  return router;
213
213
  }
214
- async dispatch(session, message) {
214
+ routes() {
215
+ return this.entries;
216
+ }
217
+ branches(session) {
218
+ return this.branchesFrom(session, []);
219
+ }
220
+ match(session, message) {
215
221
  const tokenizer = new Tokenizer(message.segments);
216
- return await this.process(session, tokenizer);
222
+ return this.matchFrom(session, tokenizer, []);
223
+ }
224
+ async dispatch(session, message) {
225
+ const match = this.match(session, message);
226
+ if (match === void 0) return false;
227
+ switch (match.type) {
228
+ case "command":
229
+ await match.command.handler(session, match.params);
230
+ break;
231
+ case "rawPattern":
232
+ await match.rawPattern.handler(session, match.params);
233
+ break;
234
+ }
235
+ return true;
217
236
  }
218
- async process(session, tokenizer) {
237
+ matchFrom(session, tokenizer, path) {
219
238
  const initialState = tokenizer.getState();
220
239
  for (const entry of this.entries) {
221
240
  tokenizer.setState(initialState);
222
- if (await this.processEntry(entry, session, tokenizer)) return true;
241
+ const match = this.matchEntry(entry, session, tokenizer, path);
242
+ if (match !== void 0) return match;
223
243
  }
224
244
  tokenizer.setState(initialState);
225
- return false;
226
245
  }
227
- async processEntry(entry, session, tokenizer) {
246
+ matchEntry(entry, session, tokenizer, path) {
228
247
  switch (entry.type) {
229
- case "command": return await this.processCommand(entry.command, session, tokenizer);
230
- case "group": return await this.processGroup(entry.name, entry.router, session, tokenizer);
248
+ case "command": return this.matchCommand(entry.command, tokenizer, path);
249
+ case "group": return this.matchGroup(entry.name, entry.router, session, tokenizer, path);
231
250
  case "filter":
232
- if (entry.predicate(session) !== true) return false;
233
- return await entry.router.process(session, tokenizer);
234
- case "rawPattern": return await this.processRawPattern(entry.rawPattern, session, tokenizer);
251
+ if (entry.predicate(session) !== true) return;
252
+ return entry.router.matchFrom(session, tokenizer, path);
253
+ case "rawPattern": return this.matchRawPattern(entry.rawPattern, tokenizer, path);
235
254
  }
236
255
  }
237
- async processCommand(command, session, tokenizer) {
256
+ matchCommand(command, tokenizer, path) {
238
257
  const token = tokenizer.peek();
239
- if (typeof token !== "string" || token !== command.name) return false;
258
+ if (typeof token !== "string" || token !== command.name) return;
240
259
  tokenizer.next();
241
260
  const params = this.capturePattern(command.pattern, tokenizer);
242
- if (params === void 0 || tokenizer.hasNext()) return false;
243
- await command.handler(session, params);
244
- return true;
261
+ if (params === void 0 || tokenizer.hasNext()) return;
262
+ return {
263
+ type: "command",
264
+ path: [...path],
265
+ command,
266
+ params
267
+ };
245
268
  }
246
- async processGroup(name, router, session, tokenizer) {
269
+ matchGroup(name, router, session, tokenizer, path) {
247
270
  const token = tokenizer.peek();
248
- if (typeof token !== "string" || token !== name) return false;
271
+ if (typeof token !== "string" || token !== name) return;
249
272
  tokenizer.next();
250
- return await router.process(session, tokenizer);
273
+ return router.matchFrom(session, tokenizer, [...path, name]);
251
274
  }
252
- async processRawPattern(rawPattern, session, tokenizer) {
275
+ matchRawPattern(rawPattern, tokenizer, path) {
253
276
  const params = this.capturePattern(rawPattern.pattern, tokenizer);
254
- if (params === void 0 || tokenizer.hasNext()) return false;
255
- await rawPattern.handler(session, params);
256
- return true;
277
+ if (params === void 0 || tokenizer.hasNext()) return;
278
+ return {
279
+ type: "rawPattern",
280
+ path: [...path],
281
+ rawPattern,
282
+ params
283
+ };
284
+ }
285
+ branchesFrom(session, path) {
286
+ const branches = [];
287
+ for (const entry of this.entries) switch (entry.type) {
288
+ case "command":
289
+ branches.push({
290
+ type: "command",
291
+ path: [...path],
292
+ command: entry.command
293
+ });
294
+ break;
295
+ case "group":
296
+ branches.push(...entry.router.branchesFrom(session, [...path, entry.name]));
297
+ break;
298
+ case "filter":
299
+ if (entry.predicate(session) === true) branches.push(...entry.router.branchesFrom(session, path));
300
+ break;
301
+ case "rawPattern":
302
+ branches.push({
303
+ type: "rawPattern",
304
+ path: [...path],
305
+ rawPattern: entry.rawPattern
306
+ });
307
+ break;
308
+ }
309
+ return branches;
257
310
  }
258
311
  capturePattern(pattern, tokenizer) {
259
312
  const initialState = tokenizer.getState();
@@ -419,7 +472,10 @@ var Context = class Context {
419
472
  const available = /* @__PURE__ */ new Set();
420
473
  for (const service of this.collectAvailableServices()) available.add(service);
421
474
  while (pending.length > 0) {
422
- const nextIndex = pending.findIndex(({ plugin }) => (plugin.requires ?? []).every((service) => available.has(service)));
475
+ const availableByPendingPlugins = this.collectPendingProvidedServices(pending);
476
+ const requiredReadyIndex = pending.findIndex(({ plugin }) => this.areRequiredServicesAvailable(plugin, available));
477
+ const optionalReadyIndex = pending.findIndex(({ plugin }) => this.areRequiredServicesAvailable(plugin, available) && this.areOptionalServicesReady(plugin, available, availableByPendingPlugins));
478
+ const nextIndex = optionalReadyIndex === -1 ? requiredReadyIndex : optionalReadyIndex;
423
479
  if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
424
480
  const [next] = pending.splice(nextIndex, 1);
425
481
  sorted.push(next);
@@ -427,6 +483,21 @@ var Context = class Context {
427
483
  }
428
484
  return sorted;
429
485
  }
486
+ collectPendingProvidedServices(pending) {
487
+ const providedServices = /* @__PURE__ */ new Map();
488
+ for (const installedPlugin of pending) for (const service of installedPlugin.plugin.provides ?? []) providedServices.set(service, installedPlugin);
489
+ return providedServices;
490
+ }
491
+ areRequiredServicesAvailable(plugin, available) {
492
+ return (plugin.requires ?? []).every((service) => available.has(service));
493
+ }
494
+ areOptionalServicesReady(plugin, available, pendingProviders) {
495
+ return (plugin.optionalRequires ?? []).every((service) => {
496
+ if (available.has(service)) return true;
497
+ const pendingProvider = pendingProviders.get(service);
498
+ return pendingProvider === void 0 || pendingProvider.plugin === plugin;
499
+ });
500
+ }
430
501
  collectAvailableServices() {
431
502
  const services = [...this.services.keys()];
432
503
  if (this.parent) services.push(...this.parent.collectAvailableServices());
@@ -450,8 +521,18 @@ var Context = class Context {
450
521
  }
451
522
  createProxyContextForPlugin(plugin) {
452
523
  const proxyLogger = new Logger((message) => this.logHandler?.(message), plugin.name);
524
+ let proxyInjections;
525
+ if (plugin.inject) {
526
+ proxyInjections = {};
527
+ for (const [key, service] of Object.entries(plugin.inject)) proxyInjections[key] = this.resolve(service);
528
+ }
529
+ if (plugin.optionalInject) {
530
+ proxyInjections ??= {};
531
+ for (const [key, service] of Object.entries(plugin.optionalInject)) proxyInjections[key] = this.tryResolve(service);
532
+ }
453
533
  return new Proxy(this, { get(target, prop) {
454
534
  if (prop === "logger") return proxyLogger;
535
+ else if (proxyInjections && prop in proxyInjections) return proxyInjections[prop];
455
536
  else return target[prop];
456
537
  } });
457
538
  }
@@ -615,6 +696,14 @@ let filter;
615
696
  //#endregion
616
697
  //#region src/core/plugin.ts
617
698
  function definePlugin(plugin) {
699
+ if (plugin.inject) {
700
+ if (plugin.requires) throw new Error(`Plugin "${plugin.name}" cannot have both "requires" and "inject" properties.`);
701
+ plugin.requires = Object.values(plugin.inject);
702
+ }
703
+ if (plugin.optionalInject) {
704
+ if (plugin.optionalRequires) throw new Error(`Plugin "${plugin.name}" cannot have both "optionalRequires" and "optionalInject" properties.`);
705
+ plugin.optionalRequires = Object.values(plugin.optionalInject);
706
+ }
618
707
  return plugin;
619
708
  }
620
709
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.3.3",
4
+ "version": "0.5.0",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"
@@ -19,6 +19,9 @@
19
19
  "dependencies": {
20
20
  "mitt": "^3.0.1"
21
21
  },
22
+ "devDependencies": {
23
+ "@fraqjs/mock": "^0.1.0"
24
+ },
22
25
  "engines": {
23
26
  "node": ">=22"
24
27
  },