@grest-ts/locator 0.0.5
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/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/src/GGLocator.d.ts +12 -0
- package/dist/src/GGLocator.d.ts.map +1 -0
- package/dist/src/GGLocator.js +35 -0
- package/dist/src/GGLocator.js.map +1 -0
- package/dist/src/GGLocatorKey.d.ts +10 -0
- package/dist/src/GGLocatorKey.d.ts.map +1 -0
- package/dist/src/GGLocatorKey.js +23 -0
- package/dist/src/GGLocatorKey.js.map +1 -0
- package/dist/src/GGLocatorScope.d.ts +79 -0
- package/dist/src/GGLocatorScope.d.ts.map +1 -0
- package/dist/src/GGLocatorScope.js +160 -0
- package/dist/src/GGLocatorScope.js.map +1 -0
- package/dist/src/GGLocatorScopeDebug.d.ts +32 -0
- package/dist/src/GGLocatorScopeDebug.d.ts.map +1 -0
- package/dist/src/GGLocatorScopeDebug.js +116 -0
- package/dist/src/GGLocatorScopeDebug.js.map +1 -0
- package/dist/src/GGLocatorStorage.d.ts +5 -0
- package/dist/src/GGLocatorStorage.d.ts.map +1 -0
- package/dist/src/GGLocatorStorage.js +7 -0
- package/dist/src/GGLocatorStorage.js.map +1 -0
- package/dist/src/GG_ENV.d.ts +2 -0
- package/dist/src/GG_ENV.d.ts.map +1 -0
- package/dist/src/GG_ENV.js +2 -0
- package/dist/src/GG_ENV.js.map +1 -0
- package/dist/src/index-browser.d.ts +5 -0
- package/dist/src/index-browser.d.ts.map +1 -0
- package/dist/src/index-browser.js +4 -0
- package/dist/src/index-browser.js.map +1 -0
- package/dist/src/index-node.d.ts +5 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +7 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/tsconfig.json +17 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +55 -0
- package/src/GGLocator.ts +46 -0
- package/src/GGLocatorKey.ts +30 -0
- package/src/GGLocatorScope.ts +206 -0
- package/src/GGLocatorScopeDebug.ts +153 -0
- package/src/GGLocatorStorage.ts +9 -0
- package/src/GG_ENV.ts +0 -0
- package/src/index-browser.ts +4 -0
- package/src/index-node.ts +8 -0
- package/src/tsconfig.json +17 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type {GGLocatorKey} from "./GGLocatorKey";
|
|
2
|
+
import {GG_ASYNC_STORAGE} from "./GGLocatorStorage";
|
|
3
|
+
import {GGLocatorScopeDebug, GGLocatorScopeDebugList} from "./GGLocatorScopeDebug";
|
|
4
|
+
|
|
5
|
+
type ConstructorOf<T> = new (...args: any[]) => T;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* service-locator pattern for async context cases.
|
|
9
|
+
*/
|
|
10
|
+
export class GGLocatorScope {
|
|
11
|
+
|
|
12
|
+
public readonly name: string
|
|
13
|
+
public readonly serviceName?: string
|
|
14
|
+
private readonly parent: GGLocatorScope | undefined
|
|
15
|
+
private readonly values: Map<string, any>
|
|
16
|
+
private readonly registrationStacks: Map<string, string[]>
|
|
17
|
+
private readonly creationStack: string[]
|
|
18
|
+
|
|
19
|
+
constructor(name: string, parent?: GGLocatorScope, serviceName?: string) {
|
|
20
|
+
this.name = name
|
|
21
|
+
this.serviceName = serviceName ?? this.serviceName ?? "Unknown";
|
|
22
|
+
this.parent = parent
|
|
23
|
+
this.values = new Map(this.parent?.values ?? []);
|
|
24
|
+
this.registrationStacks = new Map(this.parent?.registrationStacks ?? []);
|
|
25
|
+
this.creationStack = new Error().stack?.split("\n").splice(2) ?? [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private _get<T>(name: string): T {
|
|
29
|
+
const value = this.values.get(name);
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Service "${name}" not found in async context!\n` +
|
|
33
|
+
`Scope tree: ${this.getScopeDebug().toString()}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public get<T>(token: GGLocatorKey<T>): T {
|
|
40
|
+
return this._get(token.name)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public tryGet<T>(token: GGLocatorKey<T>): T | undefined {
|
|
44
|
+
return this.values.get(token.name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public getByTokenName<T>(name: string, type: ConstructorOf<T>): T {
|
|
48
|
+
const value = this._get(name)
|
|
49
|
+
if (!(value instanceof type)) {
|
|
50
|
+
throw new Error("Service is not extending expected type! Expected '" + type?.name + "', but got '" + value?.constructor?.name + "' instead.")
|
|
51
|
+
}
|
|
52
|
+
return value as T;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public set<T>(token: GGLocatorKey<T>, value: T): void {
|
|
56
|
+
if (this.values.has(token.name)) {
|
|
57
|
+
throw new Error(`Scope already has service "${token.name}"!`)
|
|
58
|
+
}
|
|
59
|
+
this.overwrite(token, value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public overwrite<T>(token: GGLocatorKey<T>, value: T): void {
|
|
63
|
+
this.values.set(token.name, value);
|
|
64
|
+
this.registrationStacks.set(token.name, new Error().stack.split("\n").splice(2));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public has<T>(token: GGLocatorKey<T>): boolean {
|
|
68
|
+
return this.values.has(token.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns all registered key names in this scope.
|
|
73
|
+
* Used by testkit to report available services to test runner.
|
|
74
|
+
*/
|
|
75
|
+
public getKeys(): string[] {
|
|
76
|
+
return Array.from(this.values.keys());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -----------
|
|
80
|
+
|
|
81
|
+
private _lifecycleHandler?: (service: GGLocatorLifecycleCallbacks) => void;
|
|
82
|
+
|
|
83
|
+
public setLifecycleOwner(handler: (service: GGLocatorLifecycleCallbacks) => void | undefined): void {
|
|
84
|
+
this._lifecycleHandler = handler;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public setWithLifecycle<T>(token: GGLocatorKey<T>, value: T, lifecycle: GGLocatorLifecycleCallbacks): void {
|
|
88
|
+
if (!this._lifecycleHandler) {
|
|
89
|
+
throw new Error("No lifecycle handler - are you registering outside compose phase?");
|
|
90
|
+
}
|
|
91
|
+
this.set(token, value);
|
|
92
|
+
this._lifecycleHandler(lifecycle);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// -----------
|
|
96
|
+
|
|
97
|
+
public setImmediate<R>(fn: () => R): NodeJS.Immediate {
|
|
98
|
+
return setImmediate(() => this.run(fn))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public setTimeout<R>(fn: () => R, timeout: number): NodeJS.Timeout {
|
|
102
|
+
return setTimeout(() => this.run(fn), timeout)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public setInterval<R>(fn: () => R, timeout: number): NodeJS.Timeout {
|
|
106
|
+
return setInterval(() => this.run(fn), timeout)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wraps a function with a async context run() call. Safe for most cases.
|
|
111
|
+
* Consider wrapWithEnter in case you want same context for that function forever.
|
|
112
|
+
*/
|
|
113
|
+
public wrapWithRun<T extends (...args: any[]) => any>(fn: T): T {
|
|
114
|
+
return ((...args: any[]) => {
|
|
115
|
+
return this.run(() => {
|
|
116
|
+
return fn(...args)
|
|
117
|
+
})
|
|
118
|
+
}) as T;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wraps function to ensure this scope is active before each call.
|
|
123
|
+
* Unlike wrapWithRun, doesn't create a new async boundary - just ensures scope is entered.
|
|
124
|
+
* More performant for high-frequency event handlers where scope doesn't change.
|
|
125
|
+
*/
|
|
126
|
+
public wrapWithEnter<T extends (...args: any[]) => any>(fn: T): T {
|
|
127
|
+
return ((...args: any[]) => {
|
|
128
|
+
this.ensureEntered()
|
|
129
|
+
return fn(...args)
|
|
130
|
+
}) as T;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public getScopeDebug(): GGLocatorScopeDebug {
|
|
134
|
+
const debug = new GGLocatorScopeDebug(this.name, this.creationStack);
|
|
135
|
+
this.values.forEach((value, name) => {
|
|
136
|
+
debug.add(name, value, this.registrationStacks.get(name));
|
|
137
|
+
})
|
|
138
|
+
return debug;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public getScopeDebugFull(list: GGLocatorScopeDebugList = new GGLocatorScopeDebugList()): GGLocatorScopeDebugList {
|
|
142
|
+
if (this.parent) {
|
|
143
|
+
this.parent.getScopeDebugFull(list);
|
|
144
|
+
}
|
|
145
|
+
list.add(this.getScopeDebug())
|
|
146
|
+
return list;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// -----------
|
|
150
|
+
|
|
151
|
+
public branch(name: string, serviceName?: string): GGLocatorScope {
|
|
152
|
+
return new GGLocatorScope(name, this, serviceName);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public run<R>(fn: () => R): R {
|
|
156
|
+
return GG_ASYNC_STORAGE.run(this, fn);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public enter() {
|
|
160
|
+
GG_ASYNC_STORAGE.enterWith(this);
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public ensureEntered(): void {
|
|
165
|
+
const store = GG_ASYNC_STORAGE.getStore();
|
|
166
|
+
if (store !== this) {
|
|
167
|
+
GG_ASYNC_STORAGE.enterWith(this);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public reset(): void {
|
|
172
|
+
this.values.clear();
|
|
173
|
+
this.parent.values.forEach((value, name) => this.values.set(name, value))
|
|
174
|
+
this.registrationStacks.clear();
|
|
175
|
+
this.parent.registrationStacks.forEach((value, name) => this.registrationStacks.set(name, value))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Service initialization order - determines the sequence in which services start and stop.
|
|
182
|
+
* Services with lower values start first and stop last (reverse order for teardown).
|
|
183
|
+
*
|
|
184
|
+
* Example startup order: CONFIG → DATABASE → HTTP → INTL → BUSINESS → PERIODIC_JOBS → SERVICE_DISCOVERY
|
|
185
|
+
*/
|
|
186
|
+
export enum GGLocatorServiceType {
|
|
187
|
+
CONFIG = 0,
|
|
188
|
+
DATABASE = 10,
|
|
189
|
+
HTTP = 20,
|
|
190
|
+
INTL = 25,
|
|
191
|
+
BUSINESS = 30,
|
|
192
|
+
PERIODIC_JOBS = 40,
|
|
193
|
+
SERVICE_DISCOVERY = 999
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Service lifecycle callbacks registered with GGServiceRegistry.
|
|
198
|
+
*
|
|
199
|
+
* Services register these during construction (in GGRuntime.compose) to participate
|
|
200
|
+
* in the runtime lifecycle - startup, teardown, and per-request context hooks.
|
|
201
|
+
*/
|
|
202
|
+
export interface GGLocatorLifecycleCallbacks {
|
|
203
|
+
type: GGLocatorServiceType
|
|
204
|
+
start: () => any | Promise<any>
|
|
205
|
+
teardown?: () => any | Promise<any>
|
|
206
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Console ANSI colors - defined inline to avoid circular dependency with @grest-ts/logger
|
|
2
|
+
const LOG_COLORS = {
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
bright: '\x1b[1m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
cyan: '\x1b[36m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
gray: '\x1b[90m',
|
|
10
|
+
magenta: '\x1b[35m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface GGLocatorDebugService {
|
|
15
|
+
key: string;
|
|
16
|
+
className: string | undefined;
|
|
17
|
+
stack: string[];
|
|
18
|
+
composeLine: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GGLocatorRawServiceData {
|
|
22
|
+
key: string;
|
|
23
|
+
value: any;
|
|
24
|
+
rawStack: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class GGLocatorScopeDebugList {
|
|
28
|
+
|
|
29
|
+
public readonly scopes: GGLocatorScopeDebug[] = [];
|
|
30
|
+
|
|
31
|
+
public add(tree: GGLocatorScopeDebug) {
|
|
32
|
+
this.scopes.push(tree);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public toString(color: boolean = false): string {
|
|
36
|
+
return this.scopes.map(t => t.toString(color)).join("\n\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public toJSON(): object {
|
|
40
|
+
return {
|
|
41
|
+
scopes: this.scopes.map(s => s.toJSON())
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class GGLocatorScopeDebug {
|
|
48
|
+
|
|
49
|
+
public readonly scopeName: string;
|
|
50
|
+
public readonly creationStack: string[];
|
|
51
|
+
public readonly services: GGLocatorDebugService[] = [];
|
|
52
|
+
|
|
53
|
+
constructor(scopeName: string, rawCreationStack: string[]) {
|
|
54
|
+
this.scopeName = scopeName;
|
|
55
|
+
this.creationStack = this.processCreationStack(rawCreationStack);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private processCreationStack(rawStack: string[]): string[] {
|
|
59
|
+
return rawStack
|
|
60
|
+
.map(line => this.normalizeStackLine(line))
|
|
61
|
+
.filter((line): line is string => line !== undefined);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public add(key: string, value: any, rawStack: string[] | undefined): void {
|
|
65
|
+
this.services.push(this.processService({key: key, value, rawStack: rawStack ?? []}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private processService(raw: GGLocatorRawServiceData): GGLocatorDebugService {
|
|
69
|
+
const className = raw.value?.constructor?.name;
|
|
70
|
+
|
|
71
|
+
// Find compose index and trim stack up to and including compose
|
|
72
|
+
const composeIndex = raw.rawStack.findIndex(line => line.includes('.compose'));
|
|
73
|
+
const trimmedStack = composeIndex >= 0
|
|
74
|
+
? raw.rawStack.slice(0, composeIndex + 1)
|
|
75
|
+
: raw.rawStack;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
key: raw.key,
|
|
79
|
+
className: className !== 'Object' ? className : undefined,
|
|
80
|
+
stack: trimmedStack
|
|
81
|
+
.map(line => this.normalizeStackLine(line))
|
|
82
|
+
.filter((line): line is string => line !== undefined),
|
|
83
|
+
composeLine: this.findComposeLine(raw.rawStack)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private normalizeStackLine(line: string): string | undefined {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!trimmed) return undefined;
|
|
90
|
+
|
|
91
|
+
// Extract "at ClassName.method (file:line)" or just the location
|
|
92
|
+
const match = trimmed.match(/at\s+(.+)/);
|
|
93
|
+
const location = match ? match[1] : trimmed;
|
|
94
|
+
return this.normalizePathToFileUrl(location);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private findComposeLine(stack: string[]): string | undefined {
|
|
98
|
+
const composeLine = stack.find(line => line.includes('.compose'));
|
|
99
|
+
if (!composeLine) return undefined;
|
|
100
|
+
return this.normalizeStackLine(composeLine);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private normalizePathToFileUrl(location: string): string {
|
|
104
|
+
// Convert Windows paths to file:// URLs for better IDE support
|
|
105
|
+
// Matches: (C:\path\file.ts:10:5) or C:\path\file.ts:10:5
|
|
106
|
+
return location.replace(/([\w]:[\\/][^:)]+:\d+:\d+)/g, (path) => {
|
|
107
|
+
const normalizedPath = path.replace(/\\/g, '/');
|
|
108
|
+
return `file:///${normalizedPath}`;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public toString(color: boolean = false): string {
|
|
113
|
+
const C = color ? LOG_COLORS : {reset: "", bright: "", dim: "", cyan: "", yellow: "", green: "", gray: "", magenta: "", blue: ""};
|
|
114
|
+
const lines: string[] = [];
|
|
115
|
+
|
|
116
|
+
lines.push(`${C.bright}${C.cyan}Scope: ${this.scopeName}${C.reset}`);
|
|
117
|
+
|
|
118
|
+
if (this.creationStack.length > 0) {
|
|
119
|
+
lines.push(`\t${C.dim}Created at:${C.reset}`);
|
|
120
|
+
for (const frame of this.creationStack) {
|
|
121
|
+
lines.push(`\t\t${C.gray}${frame}${C.reset}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
lines.push(`\t${C.dim}---${C.reset}`);
|
|
126
|
+
|
|
127
|
+
for (const service of this.services) {
|
|
128
|
+
const className = service.className ? `${C.dim} (${service.className})${C.reset}` : "";
|
|
129
|
+
lines.push(`\t${C.yellow}${service.key}${C.reset}${className}`);
|
|
130
|
+
|
|
131
|
+
if (service.composeLine) {
|
|
132
|
+
lines.push(`\t\t${C.green}compose: ${C.reset}${C.gray}${service.composeLine}${C.reset}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (service.stack.length > 0) {
|
|
136
|
+
lines.push(`\t\t${C.dim}stack:${C.reset}`);
|
|
137
|
+
for (const frame of service.stack) {
|
|
138
|
+
lines.push(`\t\t\t${C.gray}${frame}${C.reset}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public toJSON(): object {
|
|
147
|
+
return {
|
|
148
|
+
scopeName: this.scopeName,
|
|
149
|
+
creationStack: this.creationStack,
|
|
150
|
+
services: this.services
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {GGLocatorScope} from "./GGLocatorScope";
|
|
2
|
+
import {BrowserAsyncStorage, type IAsyncStorage} from "@grest-ts/common";
|
|
3
|
+
|
|
4
|
+
// Browser-safe by default. Node.js entry replaces with real AsyncLocalStorage.
|
|
5
|
+
export let GG_ASYNC_STORAGE: IAsyncStorage<GGLocatorScope> = new BrowserAsyncStorage();
|
|
6
|
+
|
|
7
|
+
export function _initLocatorStorage(storage: IAsyncStorage<GGLocatorScope>): void {
|
|
8
|
+
GG_ASYNC_STORAGE = storage;
|
|
9
|
+
}
|
package/src/GG_ENV.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from "node:async_hooks";
|
|
2
|
+
import {_initLocatorStorage} from "./GGLocatorStorage";
|
|
3
|
+
_initLocatorStorage(new AsyncLocalStorage());
|
|
4
|
+
|
|
5
|
+
export {GGLocatorScope, GGLocatorServiceType} from "./GGLocatorScope";
|
|
6
|
+
export type {GGLocatorLifecycleCallbacks} from "./GGLocatorScope";
|
|
7
|
+
export {GGLocatorKey} from "./GGLocatorKey";
|
|
8
|
+
export {GGLocator} from "./GGLocator";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "THIS FILE IS GENERATED - DO NOT EDIT",
|
|
3
|
+
"extends": "../../../tsconfig.base.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"rootDir": ".",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022"
|
|
8
|
+
],
|
|
9
|
+
"types": [
|
|
10
|
+
"node",
|
|
11
|
+
"vitest/globals"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"**/*"
|
|
16
|
+
]
|
|
17
|
+
}
|