@travetto/registry 7.0.0-rc.1 → 7.0.0-rc.3

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
@@ -20,9 +20,9 @@ Registration, within the framework flows throw two main use cases:
20
20
 
21
21
  ### Initial Flows
22
22
  The primary flow occurs on initialization of the application. At that point, the module will:
23
- 1. Initialize [Registry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/registry.ts#L9) and will automatically register/load all relevant files
23
+ 1. Initialize [Registry](https://github.com/travetto/travetto/tree/main/module/registry/src/registry.ts#L5) and will automatically register/load all relevant files
24
24
  1. As files are imported, decorators within the files will record various metadata relevant to the respective registries
25
- 1. When all files are processed, the [Registry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/registry.ts#L9) is finished, and it will signal to anything waiting on registered data that its free to use it.
25
+ 1. When all files are processed, the [Registry](https://github.com/travetto/travetto/tree/main/module/registry/src/registry.ts#L5) is finished, and it will signal to anything waiting on registered data that its free to use it.
26
26
 
27
27
  This flow ensures all files are loaded and processed before application starts. A sample registry could like:
28
28
 
@@ -54,13 +54,13 @@ class SampleRegistryAdapter implements RegistryAdapter<Group> {
54
54
  this.#class = cls;
55
55
  }
56
56
 
57
- register(...data: Partial<Partial<Group>>[]): Group {
58
- for (const d of data) {
57
+ register(...groups: Partial<Partial<Group>>[]): Group {
58
+ for (const group of groups) {
59
59
  Object.assign(this.#config, {
60
- ...d,
60
+ ...group,
61
61
  children: [
62
62
  ...(this.#config?.children ?? []),
63
- ...(d.children ?? [])
63
+ ...(group.children ?? [])
64
64
  ]
65
65
  });
66
66
  }
@@ -98,57 +98,7 @@ export class SampleRegistryIndex implements RegistryIndex {
98
98
  }
99
99
  ```
100
100
 
101
- The registry index is a [RegistryIndex](https://github.com/travetto/travetto/tree/main/module/registry/src/service/types.ts#L36) that similar to the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.")'s Schema registry and [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.")'s Dependency registry.
101
+ The registry index is a [RegistryIndex](https://github.com/travetto/travetto/tree/main/module/registry/src/types.ts#L32) that similar to the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.")'s Schema registry and [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.")'s Dependency registry.
102
102
 
103
103
  ### Live Flow
104
- At runtime, the registry is designed to listen for changes and to propagate the changes as necessary. In many cases the same file is handled by multiple registries.
105
-
106
- As the [DynamicFileLoader](https://github.com/travetto/travetto/tree/main/module/registry/src/internal/file-loader.ts#L17) notifies that a file has been changed, the [Registry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/registry.ts#L9) will pick it up, and process it accordingly.
107
-
108
- ## Supporting Metadata
109
- As mentioned in [Manifest](https://github.com/travetto/travetto/tree/main/module/manifest#readme "Support for project indexing, manifesting, along with file watching")'s readme, the framework produces hashes of methods, classes, and functions, to allow for detecting changes to individual parts of the codebase. During the live flow, various registries will inspect this information to determine if action should be taken.
110
-
111
- **Code: Sample Class Diffing**
112
- ```typescript
113
-
114
- #handleFileChanges(importFile: string, classes: Class[] = []): number {
115
- const next = new Map<string, Class>(classes.map(cls => [cls.Ⲑid, cls] as const));
116
- const sourceFile = RuntimeIndex.getSourceFile(importFile);
117
-
118
- let prev = new Map<string, Class>();
119
- if (this.#classes.has(sourceFile)) {
120
- prev = new Map(this.#classes.get(sourceFile)!.entries());
121
- }
122
-
123
- const keys = new Set([...Array.from(prev.keys()), ...Array.from(next.keys())]);
124
-
125
- if (!this.#classes.has(sourceFile)) {
126
- this.#classes.set(sourceFile, new Map());
127
- }
128
-
129
- let changes = 0;
130
-
131
- // Determine delta based on the various classes (if being added, removed or updated)
132
- for (const k of keys) {
133
- if (!next.has(k)) {
134
- changes += 1;
135
- this.emit({ type: 'removing', prev: prev.get(k)! });
136
- this.#classes.get(sourceFile)!.delete(k);
137
- } else {
138
- this.#classes.get(sourceFile)!.set(k, next.get(k)!);
139
- if (!prev.has(k)) {
140
- changes += 1;
141
- this.emit({ type: 'added', curr: next.get(k)! });
142
- } else {
143
- const prevHash = describeFunction(prev.get(k)!)?.hash;
144
- const nextHash = describeFunction(next.get(k)!)?.hash;
145
- if (prevHash !== nextHash) {
146
- changes += 1;
147
- this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k)! });
148
- }
149
- }
150
- }
151
- }
152
- return changes;
153
- }
154
- ```
104
+ At runtime, the framework is designed to listen for changes and restart any running processes as needed.
package/__index__.ts CHANGED
@@ -1,7 +1,3 @@
1
- export * from './src/service/registry.ts';
2
- export * from './src/service/types.ts';
3
- export * from './src/service/store.ts';
4
- export * from './src/proxy.ts';
5
- export * from './src/source/class-source.ts';
6
- export * from './src/source/method-source.ts';
7
- export * from './src/types.ts';
1
+ export * from './src/registry.ts';
2
+ export * from './src/types.ts';
3
+ export * from './src/store.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/registry",
3
- "version": "7.0.0-rc.1",
3
+ "version": "7.0.0-rc.3",
4
4
  "description": "Patterns and utilities for handling registration of metadata and functionality for run-time use",
5
5
  "keywords": [
6
6
  "ast-transformations",
@@ -27,11 +27,11 @@
27
27
  "directory": "module/registry"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/runtime": "^7.0.0-rc.1"
30
+ "@travetto/runtime": "^7.0.0-rc.3"
31
31
  },
32
32
  "peerDependencies": {
33
- "@travetto/cli": "^7.0.0-rc.1",
34
- "@travetto/transformer": "^7.0.0-rc.1"
33
+ "@travetto/cli": "^7.0.0-rc.3",
34
+ "@travetto/transformer": "^7.0.0-rc.3"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/transformer": {
@@ -0,0 +1,147 @@
1
+ import { AppError, castTo, type Class, Env, flushPendingFunctions, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
2
+
3
+ import type { RegistryIndex, RegistryIndexClass } from './types';
4
+
5
+ class $Registry {
6
+
7
+ #resolved = false;
8
+ #initialized?: Promise<unknown>;
9
+ #indexByClass = new Map<RegistryIndexClass, RegistryIndex>();
10
+ #indexes: RegistryIndex[] = [];
11
+
12
+ #finalizeItems(classes: Class[]): void {
13
+ for (const index of this.#indexes) {
14
+ for (const cls of classes) {
15
+ if (index.store.has(cls) && !index.store.finalized(cls)) {
16
+ if (index.finalize) {
17
+ index.finalize(cls);
18
+ } else {
19
+ index.store.finalize(cls);
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ trace = false;
27
+
28
+ validateConstructor(source: unknown): void {
29
+ if (source !== this) {
30
+ throw new AppError('constructor is private');
31
+ }
32
+ }
33
+
34
+ finalizeForIndex(indexCls: RegistryIndexClass): void {
35
+ const inst = this.instance(indexCls);
36
+ this.#finalizeItems(inst.store.getClasses());
37
+ }
38
+
39
+ /**
40
+ * Process change events
41
+ */
42
+ process(classes: Class[]): void {
43
+ this.#finalizeItems(classes);
44
+
45
+ const byIndex = new Map<RegistryIndex, Class[]>();
46
+ for (const index of this.#indexes) {
47
+ byIndex.set(index, classes.filter(cls => index.store.has(cls)));
48
+ }
49
+
50
+ for (const index of this.#indexes) {
51
+ for (const cls of byIndex.get(index)!) {
52
+ index.onCreate?.(cls);
53
+ }
54
+ index.beforeChangeSetComplete?.(byIndex.get(index)!);
55
+ }
56
+
57
+ // Call after everything is done
58
+ for (const index of this.#indexes) {
59
+ index.onChangeSetComplete?.(byIndex.get(index)!);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Run initialization
65
+ */
66
+ async #init(): Promise<void> {
67
+ try {
68
+ this.#resolved = false;
69
+
70
+ if (this.trace) {
71
+ console.debug('Initializing');
72
+ }
73
+
74
+ // Ensure everything is loaded
75
+ for (const entry of RuntimeIndex.find({
76
+ module: (mod) => {
77
+ const role = Env.TRV_ROLE.value;
78
+ return role !== 'test' && // Skip all modules when in test
79
+ mod.roles.includes('std') &&
80
+ (
81
+ !Runtime.production || mod.prod ||
82
+ (role === 'doc' && mod.roles.includes(role))
83
+ );
84
+ },
85
+ folder: folder => folder === 'src' || folder === '$index'
86
+ })) {
87
+ await Runtime.importFrom(entry.import);
88
+ }
89
+
90
+ // Flush all load events
91
+ const added = flushPendingFunctions().filter(isClass);
92
+ this.process(added);
93
+ } finally {
94
+ this.#resolved = true;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Verify initialized state
100
+ */
101
+ verifyInitialized(): void {
102
+ if (!this.#resolved) {
103
+ throw new AppError('Registry not initialized, call init() first');
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Register a new index
109
+ */
110
+ registerIndex<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
111
+ if (!this.#indexByClass.has(indexCls)) {
112
+ const instance = new indexCls(this);
113
+ this.#indexByClass.set(indexCls, instance);
114
+ this.#indexes.push(instance);
115
+ }
116
+ return castTo(this.#indexByClass.get(indexCls));
117
+ }
118
+
119
+ /**
120
+ * Initialize, with a built-in latch to prevent concurrent initializations
121
+ */
122
+ async init(): Promise<unknown> {
123
+ if (this.trace && this.#initialized) {
124
+ console.trace('Trying to re-initialize', { initialized: !!this.#initialized });
125
+ }
126
+ return this.#initialized ??= this.#init();
127
+ }
128
+
129
+ /**
130
+ * Manual init, not meant to be used directly
131
+ * @private
132
+ */
133
+ async manualInit(files: string[]): Promise<Class[]> {
134
+ for (const file of files) {
135
+ await Runtime.importFrom(file);
136
+ }
137
+ const imported = flushPendingFunctions().filter(isClass);
138
+ this.process(imported);
139
+ return imported;
140
+ }
141
+
142
+ instance<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
143
+ return castTo(this.#indexByClass.get(indexCls));
144
+ }
145
+ }
146
+
147
+ export const Registry = new $Registry();
@@ -1,27 +1,11 @@
1
- import { Any, AppError, castTo, Class, getParentClass, Runtime } from '@travetto/runtime';
1
+ import { AppError, castTo, type Class, getParentClass } from '@travetto/runtime';
2
2
 
3
- import { EXPIRED_CLASS, RegistrationMethods, RegistryAdapter } from './types';
4
-
5
- function ExchangeExpired<R = unknown>() {
6
- return function (
7
- target: Any,
8
- propertyKey: string | symbol,
9
- descriptor: TypedPropertyDescriptor<(this: RegistryIndexStore, cls: Class) => R>
10
- ): void {
11
- if (Runtime.dynamic) {
12
- const original = descriptor.value!;
13
- descriptor.value = function (this: RegistryIndexStore, cls: Class): R {
14
- const resolved = EXPIRED_CLASS in cls ? this.getClassById(cls.Ⲑid) : cls;
15
- return original.apply(this, [resolved]);
16
- };
17
- }
18
- };
19
- }
3
+ import type { RegistrationMethods, RegistryAdapter, RegistrySimpleStore } from './types';
20
4
 
21
5
  /**
22
6
  * Base registry index implementation
23
7
  */
24
- export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<{}>> {
8
+ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<{}>> implements RegistrySimpleStore {
25
9
 
26
10
  // Core data
27
11
  #adapters = new Map<Class, A>();
@@ -51,17 +35,10 @@ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<
51
35
  this.#finalized.set(cls, true);
52
36
  }
53
37
 
54
- remove(cls: Class): void {
55
- this.#adapters.delete(cls);
56
- this.#finalized.delete(cls);
57
- }
58
-
59
- @ExchangeExpired()
60
38
  has(cls: Class): boolean {
61
39
  return this.#adapters.has(cls);
62
40
  }
63
41
 
64
- @ExchangeExpired()
65
42
  adapter(cls: Class): A {
66
43
  if (!this.#adapters.has(cls)!) {
67
44
  const adapter = new this.#adapterCls(cls);
@@ -72,7 +49,6 @@ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<
72
49
  return castTo(this.#adapters.get(cls));
73
50
  }
74
51
 
75
- @ExchangeExpired()
76
52
  getForRegister(cls: Class, allowFinalized = false): A {
77
53
  if (this.#finalized.get(cls) && !allowFinalized) {
78
54
  throw new AppError(`Class ${cls.Ⲑid} is already finalized`);
@@ -80,7 +56,6 @@ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<
80
56
  return this.adapter(cls);
81
57
  }
82
58
 
83
- @ExchangeExpired()
84
59
  get(cls: Class): Omit<A, RegistrationMethods> {
85
60
  if (!this.has(cls)) {
86
61
  throw new AppError(`Class ${cls.Ⲑid} is not registered for ${this.#adapterCls.Ⲑid}`);
@@ -88,7 +63,6 @@ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<
88
63
  return this.adapter(cls);
89
64
  }
90
65
 
91
- @ExchangeExpired()
92
66
  getOptional(cls: Class): Omit<A, RegistrationMethods> | undefined {
93
67
  if (!this.has(cls)) {
94
68
  return undefined;
@@ -96,7 +70,6 @@ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<
96
70
  return this.adapter(cls);
97
71
  }
98
72
 
99
- @ExchangeExpired()
100
73
  finalized(cls: Class): boolean {
101
74
  return this.#finalized.has(cls);
102
75
  }
package/src/types.ts CHANGED
@@ -1,20 +1,38 @@
1
+ import type { Class } from '@travetto/runtime';
2
+
3
+ export type RegistrationMethods = `register${string}` | `finalize${string}`;
4
+
1
5
  /**
2
- * A change event
6
+ * Interface for registry adapters to implement
3
7
  */
4
- export type ChangeEvent<T> =
5
- { type: 'changed', prev: T, curr: T } |
6
- { type: 'added', curr: T } |
7
- { type: 'removing', prev: T };
8
+ export interface RegistryAdapter<C extends {} = {}> {
9
+ register(...data: Partial<C>[]): C;
10
+ finalize?(parent?: C): void;
11
+ get(): C;
12
+ }
13
+
14
+ export type RegistryIndexClass = {
15
+ new(source: unknown): RegistryIndex;
16
+ };
8
17
 
9
18
  /**
10
- * Change handler
19
+ * Simple store interface for registry indexes
11
20
  */
12
- export type ChangeHandler<T> = (e: ChangeEvent<T>) => unknown;
21
+ export interface RegistrySimpleStore {
22
+ has(cls: Class): boolean;
23
+ finalize(cls: Class): void;
24
+ finalized(cls: Class): boolean;
25
+ getClasses(): Class[];
26
+ };
13
27
 
14
28
  /**
15
- * Change source
29
+ * Registry index definition
30
+ * @concrete
16
31
  */
17
- export interface ChangeSource<T> {
18
- init(): Promise<unknown>;
19
- on(callback: ChangeHandler<T>): void;
32
+ export interface RegistryIndex {
33
+ store: RegistrySimpleStore;
34
+ finalize?(cls: Class): void;
35
+ onCreate?(cls: Class): void;
36
+ onChangeSetComplete?(events: Class[]): void;
37
+ beforeChangeSetComplete?(events: Class[]): void;
20
38
  }
@@ -1,88 +0,0 @@
1
- import { Module } from 'node:module';
2
-
3
- import { path } from '@travetto/manifest';
4
- import { Runtime, RuntimeIndex } from '@travetto/runtime';
5
-
6
- import { RetargettingProxy } from '../proxy.ts';
7
-
8
- declare module 'module' {
9
- // eslint-disable-next-line @typescript-eslint/naming-convention
10
- function _resolveFilename(req: string, parent: typeof Module): string;
11
- // eslint-disable-next-line @typescript-eslint/naming-convention
12
- function _load(req: string, parent: typeof Module): unknown;
13
- }
14
-
15
- type ModuleLoader = typeof Module['_load'];
16
- type ModuleProxy = <T>(file: string, mod?: T) => (T | undefined);
17
-
18
- const moduleLoad: ModuleLoader = Module._load.bind(Module);
19
-
20
- /**
21
- * Dynamic commonjs module loader. Hooks into module loading
22
- */
23
- export class DynamicCommonjsLoader {
24
-
25
- /**
26
- * Build a module loader
27
- */
28
- static buildModuleLoader(proxyModuleLoad?: ModuleProxy): ModuleLoader {
29
- return (request: string, parent: typeof Module): unknown => {
30
- let mod: unknown;
31
- try {
32
- mod = moduleLoad.apply(null, [request, parent]);
33
- } catch (err: unknown) {
34
- const name = Module._resolveFilename!(request, parent);
35
- if (err instanceof Error && Runtime.dynamic && !name.startsWith('test/')) {
36
- const errMsg = err.message;
37
- console.debug(`Unable to load ${name}: stubbing out with error proxy.`, errMsg);
38
- const e = (): never => { throw new Error(errMsg); };
39
- mod = new Proxy({}, { getOwnPropertyDescriptor: e, get: e, has: e });
40
- } else {
41
- throw err;
42
- }
43
- }
44
-
45
- const file = Module._resolveFilename!(request, parent);
46
- const src = RuntimeIndex.getEntry(file)?.sourceFile;
47
- // Only proxy workspace modules
48
- if (src && RuntimeIndex.getModuleFromSource(src)?.workspace) {
49
- return proxyModuleLoad ? proxyModuleLoad(file, mod) : mod;
50
- } else {
51
- return mod;
52
- }
53
- };
54
- }
55
-
56
- #loader: ModuleLoader;
57
- #modules = new Map<string, RetargettingProxy<unknown>>();
58
-
59
- #proxyModule<T>(file: string, mod?: T): T | undefined {
60
- if (!this.#modules.has(file)) {
61
- this.#modules.set(file, new RetargettingProxy<T>(mod!));
62
- } else {
63
- this.#modules.get(file)!.setTarget(mod);
64
- }
65
- return this.#modules.get(file)!.get<T>();
66
- }
67
-
68
- async init(): Promise<void> {
69
- this.#loader = DynamicCommonjsLoader.buildModuleLoader((file, mod) => this.#proxyModule(file, mod));
70
- }
71
-
72
- async unload(file: string): Promise<void> {
73
- const native = path.toNative(file);
74
- if (native in require.cache) {
75
- delete require.cache[native]; // Remove require cached element
76
- }
77
- this.#proxyModule(file, null);
78
- }
79
-
80
- async load(file: string): Promise<void> {
81
- try {
82
- Module._load = this.#loader;
83
- require(file);
84
- } finally {
85
- Module._load = moduleLoad;
86
- }
87
- }
88
- }
@@ -1,46 +0,0 @@
1
- import { ManifestModuleUtil } from '@travetto/manifest';
2
- import { watchCompiler, WatchEvent, Runtime, RuntimeIndex } from '@travetto/runtime';
3
-
4
- const VALID_FILE_TYPES = new Set(['js', 'ts']);
5
-
6
- const handle = (err: Error): void => {
7
- if (err && (err.message ?? '').includes('Cannot find module')) { // Handle module reloading
8
- console.error('Cannot find module', { error: err });
9
- } else {
10
- throw err;
11
- }
12
- };
13
-
14
- /**
15
- * Listens to file changes, and handles loading/unloading as needed (when supported)
16
- */
17
- export class DynamicFileLoader {
18
-
19
- static async * listen(): AsyncIterable<WatchEvent> {
20
- // TODO: ESM Support?
21
- const { DynamicCommonjsLoader } = await import('./commonjs-loader.ts');
22
- const loader = new DynamicCommonjsLoader();
23
- await loader.init?.();
24
-
25
- process
26
- .on('unhandledRejection', handle)
27
- .on('uncaughtException', handle);
28
-
29
- // Fire off, and let it run in the bg. Restart on exit
30
- for await (const ev of watchCompiler({ restartOnExit: true })) {
31
- if (ev.file && RuntimeIndex.hasModule(ev.module) && VALID_FILE_TYPES.has(ManifestModuleUtil.getFileType(ev.file))) {
32
- if (ev.action === 'update' || ev.action === 'delete') {
33
- await loader.unload(ev.output);
34
- }
35
- if (ev.action === 'create' || ev.action === 'delete') {
36
- RuntimeIndex.reinitForModule(Runtime.main.name);
37
- }
38
- if (ev.action === 'create' || ev.action === 'update') {
39
- await loader.load(ev.output);
40
- }
41
-
42
- yield ev;
43
- }
44
- }
45
- }
46
- }
package/src/proxy.ts DELETED
@@ -1,127 +0,0 @@
1
- /* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "none", "varsIgnorePattern": "^(_.*|[A-Z])$" } ] */
2
- import { Any, castKey, castTo, classConstruct } from '@travetto/runtime';
3
-
4
- const ProxyTargetSymbol = Symbol();
5
-
6
- const AsyncGeneratorFunction = Object.getPrototypeOf(async function* () { });
7
- const GeneratorFunction = Object.getPrototypeOf(function* () { });
8
- const AsyncFunction = Object.getPrototypeOf(async function () { });
9
-
10
- function isFunction(o: unknown): o is Function {
11
- const proto = o && Object.getPrototypeOf(o);
12
- return proto && (proto === Function.prototype || proto === AsyncFunction || proto === AsyncGeneratorFunction || proto === GeneratorFunction);
13
- }
14
-
15
- /**
16
- * Handler for for proxying modules while watching
17
- */
18
- export class RetargettingHandler<T> implements ProxyHandler<Any> {
19
- target: T;
20
- constructor(target: T) { this.target = target; }
21
-
22
- isExtensible(target: T): boolean {
23
- return !Object.isFrozen(this.target);
24
- }
25
-
26
- getOwnPropertyDescriptor(target: T, property: PropertyKey): PropertyDescriptor | undefined {
27
- if (property === '__esModule') {
28
- return undefined;
29
- } else {
30
- return Object.getOwnPropertyDescriptor(this.target, property);
31
- }
32
- }
33
-
34
- preventExtensions(target: T): boolean {
35
- return !!Object.preventExtensions(this.target);
36
- }
37
-
38
- apply(target: T, thisArg: T, argArray?: unknown[]): unknown {
39
- return castTo<Function>(this.target).apply(this.target, argArray);
40
- }
41
-
42
- construct(target: T, argArray: unknown[], newTarget?: unknown): object {
43
- return classConstruct(castTo(this.target), argArray);
44
- }
45
-
46
- setPrototypeOf(target: T, v: unknown): boolean {
47
- return Object.setPrototypeOf(this.target, castTo(v));
48
- }
49
-
50
- getPrototypeOf(target: T): object | null {
51
- return Object.getPrototypeOf(this.target);
52
- }
53
-
54
- get(target: T, prop: PropertyKey, receiver: unknown): Any {
55
- if (prop === ProxyTargetSymbol) {
56
- return this.target;
57
- }
58
- let result = this.target[castKey<T>(prop)];
59
- if (isFunction(result) && !/^class\s/.test(Function.prototype.toString.call(result))) {
60
- // Bind class members to class instance instead of proxy propagating
61
- result = result.bind(this.target);
62
- }
63
- return result;
64
- }
65
-
66
- has(target: T, prop: PropertyKey): boolean {
67
- return castTo<object>(this.target).hasOwnProperty(prop);
68
- }
69
-
70
- set(target: T, prop: PropertyKey, value: unknown): boolean {
71
- this.target[castKey<T>(prop)] = castTo(value);
72
- return true;
73
- }
74
-
75
- ownKeys(target: T): (string | symbol)[] {
76
- const keys: (string | symbol)[] = [];
77
- return keys
78
- .concat(Object.getOwnPropertyNames(this.target))
79
- .concat(Object.getOwnPropertySymbols(this.target));
80
- }
81
-
82
- deleteProperty(target: T, p: PropertyKey): boolean {
83
- return delete this.target[castKey<T>(p)];
84
- }
85
-
86
- defineProperty(target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean {
87
- Object.defineProperty(this.target, p, attributes);
88
- return true;
89
- }
90
- }
91
-
92
- interface Proxy<T> { }
93
-
94
- /**
95
- * Generate Retargetting Proxy
96
- */
97
- export class RetargettingProxy<T> {
98
-
99
- /**
100
- * Unwrap proxy
101
- */
102
- static unwrap<U>(el: U): U {
103
- return castTo<{ [ProxyTargetSymbol]: U }>(el)?.[ProxyTargetSymbol] ?? el;
104
- }
105
-
106
- #handler: RetargettingHandler<T>;
107
- #instance: Proxy<T>;
108
-
109
- constructor(initial: T) {
110
- this.#handler = new RetargettingHandler(initial);
111
- this.#instance = new Proxy({}, castTo(this.#handler));
112
- }
113
-
114
- setTarget(next: T): void {
115
- if (next !== this.#handler.target) {
116
- this.#handler.target = next;
117
- }
118
- }
119
-
120
- getTarget(): T {
121
- return this.#handler.target;
122
- }
123
-
124
- get<V extends T>(): V {
125
- return castTo(this.#instance);
126
- }
127
- }
@@ -1,166 +0,0 @@
1
- import { EventEmitter } from 'node:events';
2
- import { AppError, castTo, Class, Util } from '@travetto/runtime';
3
-
4
- import { ClassSource } from '../source/class-source';
5
- import { ChangeEvent } from '../types';
6
- import { MethodSource } from '../source/method-source';
7
- import { RegistryIndex, RegistryIndexClass, EXPIRED_CLASS } from './types';
8
-
9
- class $Registry {
10
-
11
- #resolved = false;
12
- #initialized?: Promise<unknown>;
13
- trace = false;
14
- #uid = Util.uuid();
15
-
16
- // Lookups
17
- #indexes = new Map<RegistryIndexClass, RegistryIndex>();
18
- #indexOrder: RegistryIndexClass[] = [];
19
-
20
- // Eventing
21
- #classSource = new ClassSource();
22
- #methodSource?: MethodSource;
23
- #emitter = new EventEmitter<{ event: [ChangeEvent<Class>] }>();
24
-
25
- #removeItems(classes: Class[]): void {
26
- for (const cls of classes) {
27
- for (const idx of this.#indexOrder) {
28
- this.instance(idx).store.remove(cls);
29
- }
30
- // Tag expired classes
31
- Object.assign(cls, { [EXPIRED_CLASS]: true });
32
- }
33
- }
34
-
35
- #finalizeItems(classes: Class[]): void {
36
- for (const idx of this.#indexOrder) {
37
- const inst = this.instance(idx);
38
- for (const cls of classes) {
39
- if (inst.store.has(cls) && !inst.store.finalized(cls)) {
40
- if (inst.finalize) {
41
- inst.finalize(cls);
42
- } else {
43
- inst.store.finalize(cls);
44
- }
45
- }
46
- }
47
- }
48
- }
49
-
50
- finalizeForIndex(indexCls: RegistryIndexClass): void {
51
- const inst = this.instance(indexCls);
52
- this.#finalizeItems(inst.store.getClasses());
53
- }
54
-
55
- process(events: ChangeEvent<Class>[]): void {
56
- this.#finalizeItems(events.filter(ev => 'curr' in ev).map(ev => ev.curr));
57
-
58
- for (const indexCls of this.#indexOrder) { // Visit every index, in order
59
- const inst = this.instance(indexCls);
60
- const matched = events.filter(e => inst.store.has('curr' in e ? e.curr : e.prev!));
61
- if (matched.length) {
62
- inst.process(matched);
63
- }
64
- }
65
-
66
- Util.queueMacroTask().then(() => {
67
- this.#removeItems(events.filter(ev => 'prev' in ev).map(ev => ev.prev!));
68
- });
69
-
70
- for (const e of events) {
71
- this.#emitter.emit('event', e);
72
- }
73
- }
74
-
75
- /**
76
- * Run initialization
77
- */
78
- async #init(): Promise<void> {
79
- try {
80
- this.#resolved = false;
81
-
82
- if (this.trace) {
83
- console.debug('Initializing', { uid: this.#uid });
84
- }
85
-
86
- const added = await this.#classSource.init();
87
- this.process(added.map(cls => ({ type: 'added', curr: cls })));
88
- this.#classSource.on(e => this.process([e]));
89
- } finally {
90
- this.#resolved = true;
91
- }
92
- }
93
-
94
- /**
95
- * Verify initialized state
96
- */
97
- verifyInitialized(): void {
98
- if (!this.#resolved) {
99
- throw new AppError('Registry not initialized, call init() first');
100
- }
101
- }
102
-
103
- /**
104
- * Register a new index
105
- */
106
- registerIndex<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
107
- if (!this.#indexes.has(indexCls)) {
108
- this.#indexes.set(indexCls, new indexCls());
109
- this.#indexOrder.push(indexCls);
110
- }
111
- return castTo(this.#indexes.get(indexCls));
112
- }
113
-
114
- /**
115
- * Initialize, with a built-in latch to prevent concurrent initializations
116
- */
117
- async init(): Promise<unknown> {
118
- if (this.trace && this.#initialized) {
119
- console.trace('Trying to re-initialize', { uid: this.#uid, initialized: !!this.#initialized });
120
- }
121
- return this.#initialized ??= this.#init();
122
- }
123
-
124
- instance<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
125
- return castTo(this.#indexes.get(indexCls));
126
- }
127
-
128
- /**
129
- * Listen for changes
130
- */
131
- onClassChange(handler: (event: ChangeEvent<Class>) => void, matches?: RegistryIndexClass): void {
132
- if (!matches) {
133
- this.#emitter.on('event', handler);
134
- } else {
135
- const inst = this.instance(matches);
136
- this.#emitter.on('event', (event) => {
137
- if (inst.store.has('curr' in event ? event.curr : event.prev!)) {
138
- handler(event);
139
- }
140
- });
141
- }
142
- }
143
-
144
- onNonClassChanges(handler: (file: string) => void): void {
145
- this.#classSource.onNonClassChanges(handler);
146
- }
147
-
148
- onMethodChange(
149
- handler: (event: ChangeEvent<[Class, Function]>) => void,
150
- matches?: RegistryIndexClass,
151
- ): void {
152
- const src = this.#methodSource ??= new MethodSource(this.#classSource);
153
- if (!matches) {
154
- src.on(handler);
155
- } else {
156
- const inst = this.instance(matches);
157
- src.on((event) => {
158
- if (inst.store.has('curr' in event ? event.curr[0] : event.prev[0])) {
159
- handler(event);
160
- }
161
- });
162
- }
163
- }
164
- }
165
-
166
- export const Registry = new $Registry();
@@ -1,40 +0,0 @@
1
- import { Class } from '@travetto/runtime';
2
- import { ChangeEvent } from '../types';
3
-
4
- export type RegistrationMethods = `register${string}` | `finalize${string}`;
5
-
6
- export const EXPIRED_CLASS = Symbol();
7
-
8
- /**
9
- * Interface for registry adapters to implement
10
- */
11
- export interface RegistryAdapter<C extends {} = {}> {
12
- register(...data: Partial<C>[]): C;
13
- finalize?(parent?: C): void;
14
- get(): C;
15
- }
16
-
17
- export type RegistryIndexClass = {
18
- new(): RegistryIndex;
19
- };
20
-
21
- /**
22
- * Simple store interface for registry indexes
23
- */
24
- export interface RegistrySimpleStore {
25
- has(cls: Class): boolean;
26
- finalize(cls: Class): void;
27
- finalized(cls: Class): boolean;
28
- remove(cls: Class): void;
29
- getClasses(): Class[];
30
- };
31
-
32
- /**
33
- * Registry index definition
34
- * @concrete
35
- */
36
- export interface RegistryIndex {
37
- store: RegistrySimpleStore;
38
- process(events: ChangeEvent<Class>[]): void;
39
- finalize?(cls: Class): void;
40
- }
@@ -1,196 +0,0 @@
1
- import { EventEmitter } from 'node:events';
2
-
3
- import { Class, Env, Runtime, RuntimeIndex, describeFunction, flushPendingFunctions } from '@travetto/runtime';
4
-
5
- import { DynamicFileLoader } from '../internal/file-loader.ts';
6
- import { ChangeSource, ChangeEvent, ChangeHandler } from '../types.ts';
7
-
8
- function isClass(cls: Function): cls is Class {
9
- return !!describeFunction(cls).class;
10
- }
11
-
12
- /**
13
- * A class change source. Meant to be hooked into the
14
- * compiler as a way to listen to changes via the compiler
15
- * watching.
16
- */
17
- export class ClassSource implements ChangeSource<Class> {
18
-
19
- #classes = new Map<string, Map<string, Class>>();
20
- #emitter = new EventEmitter<{
21
- change: [ChangeEvent<Class>];
22
- // eslint-disable-next-line @typescript-eslint/naming-convention
23
- 'unchanged-import': [string];
24
- }>();
25
- #listening: Promise<void> | undefined;
26
-
27
- /**
28
- * Are we in a mode that should have enhanced debug info
29
- */
30
- trace = Env.DEBUG.val?.includes('@travetto/registry');
31
-
32
- /**
33
- * Flush classes
34
- */
35
- #flush(): Class[] {
36
- const flushed = flushPendingFunctions().filter(isClass);
37
- for (const cls of flushed) {
38
- const src = Runtime.getSourceFile(cls);
39
- if (!this.#classes.has(src)) {
40
- this.#classes.set(src, new Map());
41
- }
42
- this.#classes.get(src)!.set(cls.Ⲑid, cls);
43
- this.emit({ type: 'added', curr: cls });
44
- }
45
- return flushed;
46
- }
47
-
48
- #removeFile(file: string): void {
49
- const data = this.#classes.get(file);
50
- if (data) {
51
- this.#classes.delete(file);
52
- for (const cls of data) {
53
- this.emit({ type: 'removing', prev: cls[1] });
54
- }
55
- }
56
- }
57
-
58
- /**
59
- * Process changes for a single file, looking for add/remove/update of classes
60
- */
61
- #handleFileChanges(importFile: string, classes: Class[] = []): number {
62
- const next = new Map<string, Class>(classes.map(cls => [cls.Ⲑid, cls] as const));
63
- const sourceFile = RuntimeIndex.getSourceFile(importFile);
64
-
65
- let prev = new Map<string, Class>();
66
- if (this.#classes.has(sourceFile)) {
67
- prev = new Map(this.#classes.get(sourceFile)!.entries());
68
- }
69
-
70
- const keys = new Set([...Array.from(prev.keys()), ...Array.from(next.keys())]);
71
-
72
- if (!this.#classes.has(sourceFile)) {
73
- this.#classes.set(sourceFile, new Map());
74
- }
75
-
76
- let changes = 0;
77
-
78
- // Determine delta based on the various classes (if being added, removed or updated)
79
- for (const k of keys) {
80
- if (!next.has(k)) {
81
- changes += 1;
82
- this.emit({ type: 'removing', prev: prev.get(k)! });
83
- this.#classes.get(sourceFile)!.delete(k);
84
- } else {
85
- this.#classes.get(sourceFile)!.set(k, next.get(k)!);
86
- if (!prev.has(k)) {
87
- changes += 1;
88
- this.emit({ type: 'added', curr: next.get(k)! });
89
- } else {
90
- const prevHash = describeFunction(prev.get(k)!)?.hash;
91
- const nextHash = describeFunction(next.get(k)!)?.hash;
92
- if (prevHash !== nextHash) {
93
- changes += 1;
94
- this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k)! });
95
- }
96
- }
97
- }
98
- }
99
- return changes;
100
- }
101
-
102
- /**
103
- * Process all class changes
104
- */
105
- #handleChanges(classes: Class[] = []): void {
106
- const classesByFile = new Map<string, Class[]>();
107
- for (const el of classes) {
108
- const imp = Runtime.getImport(el);
109
- if (!classesByFile.has(imp)) {
110
- classesByFile.set(imp, []);
111
- }
112
- classesByFile.get(imp)!.push(el);
113
- }
114
-
115
- for (const [imp, els] of classesByFile.entries()) {
116
- if (!this.#handleFileChanges(imp, els)) {
117
- this.#emitter.emit('unchanged-import', imp);
118
- }
119
- }
120
- }
121
-
122
- /**
123
- * Emit a change event
124
- */
125
- emit(e: ChangeEvent<Class>): void {
126
- if (this.trace) {
127
- console.debug('Emitting change', {
128
- type: e.type,
129
- curr: (e.type !== 'removing' ? e.curr?.Ⲑid : undefined),
130
- prev: (e.type !== 'added' ? e.prev?.Ⲑid : undefined)
131
- });
132
- }
133
- this.#emitter.emit('change', e);
134
- }
135
-
136
- /**
137
- * Initialize
138
- */
139
- async init(): Promise<Class[]> {
140
- if (Runtime.dynamic && !this.#listening) {
141
- this.#listening = (async (): Promise<void> => {
142
- for await (const ev of await DynamicFileLoader.listen()) {
143
- if (ev.action === 'delete') {
144
- this.#removeFile(ev.file); // File no longer exists
145
- } else {
146
- this.#handleChanges(flushPendingFunctions().filter(isClass));
147
- }
148
-
149
- if (ev.action === 'create') {
150
- this.#flush();
151
- }
152
- }
153
- })();
154
- }
155
-
156
- // Ensure everything is loaded
157
- for (const entry of RuntimeIndex.find({
158
- module: (m) => {
159
- const role = Env.TRV_ROLE.val;
160
- return role !== 'test' && // Skip all modules when in test
161
- m.roles.includes('std') &&
162
- (
163
- !Runtime.production || m.prod ||
164
- (role === 'doc' && m.roles.includes(role))
165
- );
166
- },
167
- folder: f => f === 'src' || f === '$index'
168
- })) {
169
- await Runtime.importFrom(entry.import);
170
- }
171
-
172
- // Flush all load events
173
- return this.#flush();
174
- }
175
-
176
- /**
177
- * Add callback for change events
178
- */
179
- on(callback: ChangeHandler<Class>): void {
180
- this.#emitter.on('change', callback);
181
- }
182
-
183
- /**
184
- * Add callback for change events
185
- */
186
- off(callback: ChangeHandler<Class>): void {
187
- this.#emitter.off('change', callback);
188
- }
189
-
190
- /**
191
- * Add callback for when a import is changed, but emits no class changes
192
- */
193
- onNonClassChanges(callback: (imp: string) => void): void {
194
- this.#emitter.on('unchanged-import', callback);
195
- }
196
- }
@@ -1,60 +0,0 @@
1
- import { EventEmitter } from 'node:events';
2
-
3
- import { Class, describeFunction } from '@travetto/runtime';
4
-
5
- import { ChangeSource, ChangeEvent, ChangeHandler } from '../types.ts';
6
-
7
- /**
8
- * Change source specific to individual methods of classes. Useful
9
- * for method based registries
10
- */
11
- export class MethodSource implements ChangeSource<[Class, Function]> {
12
-
13
- #emitter = new EventEmitter();
14
-
15
- /**
16
- * Define parent change source, generally will be the class source
17
- */
18
- constructor(classSource: ChangeSource<Class>) {
19
- classSource.on(e => this.onClassEvent(e));
20
- }
21
-
22
- async init(): Promise<void> { }
23
-
24
- emit(ev: ChangeEvent<[Class, Function]>): void {
25
- this.#emitter.emit('change', ev);
26
- }
27
-
28
- /**
29
- * On a class being emitted, check methods
30
- */
31
- onClassEvent(e: ChangeEvent<Class>): void {
32
- const next = (e.type !== 'removing' ? describeFunction(e.curr!)?.methods : null) ?? {};
33
- const prev = (e.type !== 'added' ? describeFunction(e.prev!)?.methods : null) ?? {};
34
-
35
- /**
36
- * Go through each method, comparing hashes. To see added/removed and changed
37
- */
38
- for (const k of Object.keys(next)) {
39
- if ((!prev[k] || !('prev' in e)) && e.type !== 'removing') {
40
- this.emit({ type: 'added', curr: [e.curr!, e.curr!.prototype[k]] });
41
- } else if (next[k].hash !== prev[k].hash && e.type === 'changed') {
42
- // FIXME: Why is e.prev undefined sometimes?
43
- this.emit({ type: 'changed', curr: [e.curr, e.curr.prototype[k]], prev: [e.prev, e.prev.prototype[k]] });
44
- } else {
45
- // Method Unchanged
46
- }
47
- }
48
-
49
- for (const k of Object.keys(prev)) {
50
- if (!next[k] && e.type !== 'added') {
51
- this.emit({ type: 'removing', prev: [e.prev, e.prev.prototype[k]] });
52
- }
53
- }
54
- }
55
-
56
- on(callback: ChangeHandler<[Class, Function]>): this {
57
- this.#emitter.on('change', callback);
58
- return this;
59
- }
60
- }