@grest-ts/discovery-local 0.0.5 → 0.0.7

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.
@@ -1,143 +1,143 @@
1
- import {GGLog} from "@grest-ts/logger";
2
- import {IPCServer} from "@grest-ts/ipc";
3
- import {RoutingStrategy} from "./routing/RoutingStrategy";
4
- import {FirstStrategy} from "./routing/strategies/FirstStrategy";
5
- import {LastStrategy} from "./routing/strategies/LastStrategy";
6
- import {RandomStrategy} from "./routing/strategies/RandomStrategy";
7
- import {RoundRobinStrategy} from "./routing/strategies/RoundRobinStrategy";
8
- import {GGDiscoveryIPC} from "./GGDiscoveryIPC";
9
- import {GGServiceDiscoveryEntry} from "./GGLocalDiscoveryClient";
10
-
11
- export const ROUTING_STRATEGIES = {
12
- first: new FirstStrategy(),
13
- last: new LastStrategy(),
14
- random: new RandomStrategy(),
15
- roundRobin: new RoundRobinStrategy(),
16
- } as const;
17
-
18
- export type RoutingStrategyName = "first" | "last" | "roundRobin" | "random" | RoutingStrategy;
19
-
20
- export class GGLocalDiscoveryServer {
21
-
22
- private readonly server: IPCServer;
23
- private readonly routes: Map<string, GGServiceDiscoveryEntry[]> = new Map();
24
- private readonly routingStrategies: Map<string, RoutingStrategy> = new Map();
25
-
26
- constructor(server: IPCServer) {
27
- this.server = server;
28
-
29
- // Socket handlers for framework communication
30
- this.server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.register, async (routes) => {
31
- routes.forEach(route => this.addRoute(route));
32
- });
33
-
34
- this.server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.unregister, async (routes) => {
35
- routes.forEach(route => this.removeRoute(route.api));
36
- });
37
-
38
- this.server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.discoverApi, async (apiName) => {
39
- if (this.getRoute(apiName)) {
40
- return {success: true, url: this.server.getUrl()};
41
- }
42
- return {success: false, error: "Service '" + apiName + "' is not registered! Did you forget to start it?"};
43
- });
44
-
45
- this.server.setRouteProxyResolver((path) => {
46
- return this.matchRoute(path)?.baseUrl || undefined;
47
- })
48
- }
49
-
50
- public async start(): Promise<boolean> {
51
- const result = await this.server.start();
52
- if (result) {
53
- GGLog.info(this, `Router started on ${this.server.getUrl()}`);
54
- }
55
- return result;
56
- }
57
-
58
- public async teardown(): Promise<void> {
59
- this.routes.clear();
60
- this.routingStrategies.clear();
61
- await this.server.teardown();
62
- GGLog.info(this, 'Router stopped');
63
- }
64
-
65
- public setRoutingStrategy(api: string, strategy: RoutingStrategyName | RoutingStrategy): void {
66
- const strategyInstance = typeof strategy === 'string' ? ROUTING_STRATEGIES[strategy] : strategy;
67
- this.routingStrategies.set(api, strategyInstance);
68
- GGLog.debug(this, `Set routing strategy for ${api}: ${strategyInstance.constructor.name}`);
69
- }
70
-
71
- private getRoutingStrategy(api: string): RoutingStrategy {
72
- return this.routingStrategies.get(api) ?? ROUTING_STRATEGIES.roundRobin;
73
- }
74
-
75
- public getRoute(api: string): GGServiceDiscoveryEntry | undefined {
76
- const instances = this.routes.get(api);
77
- if (!instances || instances.length === 0) {
78
- return undefined;
79
- }
80
- const strategy = this.getRoutingStrategy(api);
81
- return strategy.select(instances, api);
82
- }
83
-
84
- public addRoute(route: GGServiceDiscoveryEntry): void {
85
- const existing = this.routes.get(route.api) ?? [];
86
- existing.push(route);
87
- this.routes.set(route.api, existing);
88
- GGLog.info(this, `Added route: ${route.pathPrefix} (${route.api}) -> ${route.baseUrl}`);
89
- }
90
-
91
- public removeRoute(api: string, baseUrl?: string): void {
92
- const instances = this.routes.get(api);
93
- if (!instances) return;
94
-
95
- if (baseUrl) {
96
- // Remove specific instance
97
- const filtered = instances.filter(r => r.baseUrl !== baseUrl);
98
- if (filtered.length === 0) {
99
- this.routes.delete(api);
100
- } else {
101
- this.routes.set(api, filtered);
102
- }
103
- GGLog.info(this, `Removed route instance: ${api} -> ${baseUrl}`);
104
- } else {
105
- // Remove all instances for this API
106
- const route = instances[0];
107
- this.routes.delete(api);
108
- GGLog.info(this, `Removed route: ${route?.pathPrefix} (${api})`);
109
- }
110
- }
111
-
112
- public getRoutingUrl(api: string): string {
113
- if (!this.getRoute(api)) {
114
- throw new Error(`API '${api}' not registered in router`);
115
- }
116
- if (!this.server.getUrl()) {
117
- throw new Error("Router not started yet!");
118
- }
119
- return this.server.getUrl();
120
- }
121
-
122
- private matchRoute(path: string): GGServiceDiscoveryEntry | undefined {
123
- let bestApi: string | undefined;
124
- let longestMatch = 0;
125
-
126
- // Find the best matching API based on path prefix
127
- for (const [api, instances] of this.routes.entries()) {
128
- if (instances.length === 0) continue;
129
- const pathPrefix = instances[0].pathPrefix;
130
- if (path.startsWith(pathPrefix) && pathPrefix.length > longestMatch) {
131
- bestApi = api;
132
- longestMatch = pathPrefix.length;
133
- }
134
- }
135
-
136
- if (!bestApi) return undefined;
137
-
138
- // Use routing strategy to select which instance, passing the full path
139
- const instances = this.routes.get(bestApi)!;
140
- const strategy = this.getRoutingStrategy(bestApi);
141
- return strategy.select(instances, path);
142
- }
143
- }
1
+ import {GGLog} from "@grest-ts/logger";
2
+ import {IPCServer} from "@grest-ts/ipc";
3
+ import {RoutingStrategy} from "./routing/RoutingStrategy";
4
+ import {FirstStrategy} from "./routing/strategies/FirstStrategy";
5
+ import {LastStrategy} from "./routing/strategies/LastStrategy";
6
+ import {RandomStrategy} from "./routing/strategies/RandomStrategy";
7
+ import {RoundRobinStrategy} from "./routing/strategies/RoundRobinStrategy";
8
+ import {GGDiscoveryIPC} from "./GGDiscoveryIPC";
9
+ import {GGServiceDiscoveryEntry} from "./GGLocalDiscoveryClient";
10
+
11
+ export const ROUTING_STRATEGIES = {
12
+ first: new FirstStrategy(),
13
+ last: new LastStrategy(),
14
+ random: new RandomStrategy(),
15
+ roundRobin: new RoundRobinStrategy(),
16
+ } as const;
17
+
18
+ export type RoutingStrategyName = "first" | "last" | "roundRobin" | "random" | RoutingStrategy;
19
+
20
+ export class GGLocalDiscoveryServer {
21
+
22
+ private readonly server: IPCServer;
23
+ private readonly routes: Map<string, GGServiceDiscoveryEntry[]> = new Map();
24
+ private readonly routingStrategies: Map<string, RoutingStrategy> = new Map();
25
+
26
+ constructor(server: IPCServer) {
27
+ this.server = server;
28
+
29
+ // Socket handlers for framework communication
30
+ this.server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.register, async (routes) => {
31
+ routes.forEach(route => this.addRoute(route));
32
+ });
33
+
34
+ this.server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.unregister, async (routes) => {
35
+ routes.forEach(route => this.removeRoute(route.api));
36
+ });
37
+
38
+ this.server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.discoverApi, async (apiName) => {
39
+ if (this.getRoute(apiName)) {
40
+ return {success: true, url: this.server.getUrl()};
41
+ }
42
+ return {success: false, error: "Service '" + apiName + "' is not registered! Did you forget to start it?"};
43
+ });
44
+
45
+ this.server.setRouteProxyResolver((path) => {
46
+ return this.matchRoute(path)?.baseUrl || undefined;
47
+ })
48
+ }
49
+
50
+ public async start(): Promise<boolean> {
51
+ const result = await this.server.start();
52
+ if (result) {
53
+ GGLog.info(this, `Router started on ${this.server.getUrl()}`);
54
+ }
55
+ return result;
56
+ }
57
+
58
+ public async teardown(): Promise<void> {
59
+ this.routes.clear();
60
+ this.routingStrategies.clear();
61
+ await this.server.teardown();
62
+ GGLog.info(this, 'Router stopped');
63
+ }
64
+
65
+ public setRoutingStrategy(api: string, strategy: RoutingStrategyName | RoutingStrategy): void {
66
+ const strategyInstance = typeof strategy === 'string' ? ROUTING_STRATEGIES[strategy] : strategy;
67
+ this.routingStrategies.set(api, strategyInstance);
68
+ GGLog.debug(this, `Set routing strategy for ${api}: ${strategyInstance.constructor.name}`);
69
+ }
70
+
71
+ private getRoutingStrategy(api: string): RoutingStrategy {
72
+ return this.routingStrategies.get(api) ?? ROUTING_STRATEGIES.roundRobin;
73
+ }
74
+
75
+ public getRoute(api: string): GGServiceDiscoveryEntry | undefined {
76
+ const instances = this.routes.get(api);
77
+ if (!instances || instances.length === 0) {
78
+ return undefined;
79
+ }
80
+ const strategy = this.getRoutingStrategy(api);
81
+ return strategy.select(instances, api);
82
+ }
83
+
84
+ public addRoute(route: GGServiceDiscoveryEntry): void {
85
+ const existing = this.routes.get(route.api) ?? [];
86
+ existing.push(route);
87
+ this.routes.set(route.api, existing);
88
+ GGLog.info(this, `Added route: ${route.pathPrefix} (${route.api}) -> ${route.baseUrl}`);
89
+ }
90
+
91
+ public removeRoute(api: string, baseUrl?: string): void {
92
+ const instances = this.routes.get(api);
93
+ if (!instances) return;
94
+
95
+ if (baseUrl) {
96
+ // Remove specific instance
97
+ const filtered = instances.filter(r => r.baseUrl !== baseUrl);
98
+ if (filtered.length === 0) {
99
+ this.routes.delete(api);
100
+ } else {
101
+ this.routes.set(api, filtered);
102
+ }
103
+ GGLog.info(this, `Removed route instance: ${api} -> ${baseUrl}`);
104
+ } else {
105
+ // Remove all instances for this API
106
+ const route = instances[0];
107
+ this.routes.delete(api);
108
+ GGLog.info(this, `Removed route: ${route?.pathPrefix} (${api})`);
109
+ }
110
+ }
111
+
112
+ public getRoutingUrl(api: string): string {
113
+ if (!this.getRoute(api)) {
114
+ throw new Error(`API '${api}' not registered in router`);
115
+ }
116
+ if (!this.server.getUrl()) {
117
+ throw new Error("Router not started yet!");
118
+ }
119
+ return this.server.getUrl();
120
+ }
121
+
122
+ private matchRoute(path: string): GGServiceDiscoveryEntry | undefined {
123
+ let bestApi: string | undefined;
124
+ let longestMatch = 0;
125
+
126
+ // Find the best matching API based on path prefix
127
+ for (const [api, instances] of this.routes.entries()) {
128
+ if (instances.length === 0) continue;
129
+ const pathPrefix = instances[0].pathPrefix;
130
+ if (path.startsWith(pathPrefix) && pathPrefix.length > longestMatch) {
131
+ bestApi = api;
132
+ longestMatch = pathPrefix.length;
133
+ }
134
+ }
135
+
136
+ if (!bestApi) return undefined;
137
+
138
+ // Use routing strategy to select which instance, passing the full path
139
+ const instances = this.routes.get(bestApi)!;
140
+ const strategy = this.getRoutingStrategy(bestApi);
141
+ return strategy.select(instances, path);
142
+ }
143
+ }
@@ -1,7 +1,7 @@
1
- import {GGServiceDiscoveryEntry} from "../GGLocalDiscoveryClient";
2
-
3
- export interface RoutingStrategy {
4
-
5
- select(instances: GGServiceDiscoveryEntry[], path: string): GGServiceDiscoveryEntry;
6
- }
7
-
1
+ import {GGServiceDiscoveryEntry} from "../GGLocalDiscoveryClient";
2
+
3
+ export interface RoutingStrategy {
4
+
5
+ select(instances: GGServiceDiscoveryEntry[], path: string): GGServiceDiscoveryEntry;
6
+ }
7
+
@@ -1,11 +1,11 @@
1
- import {RoutingStrategy} from "../RoutingStrategy";
2
- import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
-
4
- /**
5
- * Always route to the first registered instance.
6
- */
7
- export class FirstStrategy implements RoutingStrategy {
8
- select(instances: GGServiceDiscoveryEntry[]): GGServiceDiscoveryEntry {
9
- return instances[0];
10
- }
1
+ import {RoutingStrategy} from "../RoutingStrategy";
2
+ import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
+
4
+ /**
5
+ * Always route to the first registered instance.
6
+ */
7
+ export class FirstStrategy implements RoutingStrategy {
8
+ select(instances: GGServiceDiscoveryEntry[]): GGServiceDiscoveryEntry {
9
+ return instances[0];
10
+ }
11
11
  }
@@ -1,11 +1,11 @@
1
- import {RoutingStrategy} from "../RoutingStrategy";
2
- import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
-
4
- /**
5
- * Always route to the last registered instance.
6
- */
7
- export class LastStrategy implements RoutingStrategy {
8
- select(instances: GGServiceDiscoveryEntry[]): GGServiceDiscoveryEntry {
9
- return instances[instances.length - 1];
10
- }
1
+ import {RoutingStrategy} from "../RoutingStrategy";
2
+ import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
+
4
+ /**
5
+ * Always route to the last registered instance.
6
+ */
7
+ export class LastStrategy implements RoutingStrategy {
8
+ select(instances: GGServiceDiscoveryEntry[]): GGServiceDiscoveryEntry {
9
+ return instances[instances.length - 1];
10
+ }
11
11
  }
@@ -1,11 +1,11 @@
1
- import {RoutingStrategy} from "../RoutingStrategy";
2
- import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
-
4
- /**
5
- * Randomly select an instance for each request.
6
- */
7
- export class RandomStrategy implements RoutingStrategy {
8
- select(instances: GGServiceDiscoveryEntry[]): GGServiceDiscoveryEntry {
9
- return instances[Math.floor(Math.random() * instances.length)];
10
- }
1
+ import {RoutingStrategy} from "../RoutingStrategy";
2
+ import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
+
4
+ /**
5
+ * Randomly select an instance for each request.
6
+ */
7
+ export class RandomStrategy implements RoutingStrategy {
8
+ select(instances: GGServiceDiscoveryEntry[]): GGServiceDiscoveryEntry {
9
+ return instances[Math.floor(Math.random() * instances.length)];
10
+ }
11
11
  }
@@ -1,17 +1,17 @@
1
- import {RoutingStrategy} from "../RoutingStrategy";
2
- import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
-
4
- /**
5
- * Cycle through instances in order, per path.
6
- * Each unique path maintains its own counter.
7
- */
8
- export class RoundRobinStrategy implements RoutingStrategy {
9
- private readonly counters: Map<string, number> = new Map();
10
-
11
- select(instances: GGServiceDiscoveryEntry[], path: string): GGServiceDiscoveryEntry {
12
- const counter = this.counters.get(path) ?? 0;
13
- const instance = instances[counter % instances.length];
14
- this.counters.set(path, counter + 1);
15
- return instance;
16
- }
1
+ import {RoutingStrategy} from "../RoutingStrategy";
2
+ import {GGServiceDiscoveryEntry} from "../../GGLocalDiscoveryClient";
3
+
4
+ /**
5
+ * Cycle through instances in order, per path.
6
+ * Each unique path maintains its own counter.
7
+ */
8
+ export class RoundRobinStrategy implements RoutingStrategy {
9
+ private readonly counters: Map<string, number> = new Map();
10
+
11
+ select(instances: GGServiceDiscoveryEntry[], path: string): GGServiceDiscoveryEntry {
12
+ const counter = this.counters.get(path) ?? 0;
13
+ const instance = instances[counter % instances.length];
14
+ this.counters.set(path, counter + 1);
15
+ return instance;
16
+ }
17
17
  }