@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -0
  3. package/dist/src/GGLocator.d.ts +12 -0
  4. package/dist/src/GGLocator.d.ts.map +1 -0
  5. package/dist/src/GGLocator.js +35 -0
  6. package/dist/src/GGLocator.js.map +1 -0
  7. package/dist/src/GGLocatorKey.d.ts +10 -0
  8. package/dist/src/GGLocatorKey.d.ts.map +1 -0
  9. package/dist/src/GGLocatorKey.js +23 -0
  10. package/dist/src/GGLocatorKey.js.map +1 -0
  11. package/dist/src/GGLocatorScope.d.ts +79 -0
  12. package/dist/src/GGLocatorScope.d.ts.map +1 -0
  13. package/dist/src/GGLocatorScope.js +160 -0
  14. package/dist/src/GGLocatorScope.js.map +1 -0
  15. package/dist/src/GGLocatorScopeDebug.d.ts +32 -0
  16. package/dist/src/GGLocatorScopeDebug.d.ts.map +1 -0
  17. package/dist/src/GGLocatorScopeDebug.js +116 -0
  18. package/dist/src/GGLocatorScopeDebug.js.map +1 -0
  19. package/dist/src/GGLocatorStorage.d.ts +5 -0
  20. package/dist/src/GGLocatorStorage.d.ts.map +1 -0
  21. package/dist/src/GGLocatorStorage.js +7 -0
  22. package/dist/src/GGLocatorStorage.js.map +1 -0
  23. package/dist/src/GG_ENV.d.ts +2 -0
  24. package/dist/src/GG_ENV.d.ts.map +1 -0
  25. package/dist/src/GG_ENV.js +2 -0
  26. package/dist/src/GG_ENV.js.map +1 -0
  27. package/dist/src/index-browser.d.ts +5 -0
  28. package/dist/src/index-browser.d.ts.map +1 -0
  29. package/dist/src/index-browser.js +4 -0
  30. package/dist/src/index-browser.js.map +1 -0
  31. package/dist/src/index-node.d.ts +5 -0
  32. package/dist/src/index-node.d.ts.map +1 -0
  33. package/dist/src/index-node.js +7 -0
  34. package/dist/src/index-node.js.map +1 -0
  35. package/dist/src/tsconfig.json +17 -0
  36. package/dist/tsconfig.publish.tsbuildinfo +1 -0
  37. package/package.json +55 -0
  38. package/src/GGLocator.ts +46 -0
  39. package/src/GGLocatorKey.ts +30 -0
  40. package/src/GGLocatorScope.ts +206 -0
  41. package/src/GGLocatorScopeDebug.ts +153 -0
  42. package/src/GGLocatorStorage.ts +9 -0
  43. package/src/GG_ENV.ts +0 -0
  44. package/src/index-browser.ts +4 -0
  45. package/src/index-node.ts +8 -0
  46. 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,4 @@
1
+ export {GGLocatorScope, GGLocatorServiceType} from "./GGLocatorScope";
2
+ export type {GGLocatorLifecycleCallbacks} from "./GGLocatorScope";
3
+ export {GGLocatorKey} from "./GGLocatorKey";
4
+ export {GGLocator} from "./GGLocator";
@@ -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
+ }