@travetto/registry 7.0.0-rc.2 → 7.0.0-rc.4
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 +4 -54
- package/__index__.ts +3 -7
- package/package.json +4 -4
- package/src/registry.ts +147 -0
- package/src/{service/store.ts → store.ts} +3 -30
- package/src/types.ts +29 -11
- package/src/internal/commonjs-loader.ts +0 -88
- package/src/internal/file-loader.ts +0 -46
- package/src/proxy.ts +0 -127
- package/src/service/registry.ts +0 -166
- package/src/service/types.ts +0 -40
- package/src/source/class-source.ts +0 -196
- package/src/source/method-source.ts +0 -64
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/
|
|
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/
|
|
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
|
|
|
@@ -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/
|
|
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
|
|
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 previous = new Map<string, Class>();
|
|
119
|
-
if (this.#classes.has(sourceFile)) {
|
|
120
|
-
previous = new Map(this.#classes.get(sourceFile)!.entries());
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const keys = new Set([...Array.from(previous.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 key of keys) {
|
|
133
|
-
if (!next.has(key)) {
|
|
134
|
-
changes += 1;
|
|
135
|
-
this.emit({ type: 'removing', previous: previous.get(key)! });
|
|
136
|
-
this.#classes.get(sourceFile)!.delete(key);
|
|
137
|
-
} else {
|
|
138
|
-
this.#classes.get(sourceFile)!.set(key, next.get(key)!);
|
|
139
|
-
if (!previous.has(key)) {
|
|
140
|
-
changes += 1;
|
|
141
|
-
this.emit({ type: 'added', current: next.get(key)! });
|
|
142
|
-
} else {
|
|
143
|
-
const prevHash = describeFunction(previous.get(key)!)?.hash;
|
|
144
|
-
const nextHash = describeFunction(next.get(key)!)?.hash;
|
|
145
|
-
if (prevHash !== nextHash) {
|
|
146
|
-
changes += 1;
|
|
147
|
-
this.emit({ type: 'changed', current: next.get(key)!, previous: previous.get(key)! });
|
|
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/
|
|
2
|
-
export * from './src/
|
|
3
|
-
export * from './src/
|
|
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.
|
|
3
|
+
"version": "7.0.0-rc.4",
|
|
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.
|
|
30
|
+
"@travetto/runtime": "^7.0.0-rc.4"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
34
|
-
"@travetto/transformer": "^7.0.0-rc.
|
|
33
|
+
"@travetto/cli": "^7.0.0-rc.4",
|
|
34
|
+
"@travetto/transformer": "^7.0.0-rc.3"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/registry.ts
ADDED
|
@@ -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 {
|
|
1
|
+
import { AppError, castTo, type Class, getParentClass } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
function ExchangeExpired<R = unknown>() {
|
|
6
|
-
return function (
|
|
7
|
-
target: Any,
|
|
8
|
-
propertyKey: string,
|
|
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
|
-
*
|
|
6
|
+
* Interface for registry adapters to implement
|
|
3
7
|
*/
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
*
|
|
19
|
+
* Simple store interface for registry indexes
|
|
11
20
|
*/
|
|
12
|
-
export
|
|
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
|
-
*
|
|
29
|
+
* Registry index definition
|
|
30
|
+
* @concrete
|
|
16
31
|
*/
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
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(request: string, parent: typeof Module): string;
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
12
|
-
function _load(request: 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 (error: unknown) {
|
|
34
|
-
const name = Module._resolveFilename!(request, parent);
|
|
35
|
-
if (error instanceof Error && Runtime.dynamic && !name.startsWith('test/')) {
|
|
36
|
-
const message = error.message;
|
|
37
|
-
console.debug(`Unable to load ${name}: stubbing out with error proxy.`, message);
|
|
38
|
-
const fail = (): never => { throw new Error(message); };
|
|
39
|
-
mod = new Proxy({}, { getOwnPropertyDescriptor: fail, get: fail, has: fail });
|
|
40
|
-
} else {
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const file = Module._resolveFilename!(request, parent);
|
|
46
|
-
const source = RuntimeIndex.getEntry(file)?.sourceFile;
|
|
47
|
-
// Only proxy workspace modules
|
|
48
|
-
if (source && RuntimeIndex.getModuleFromSource(source)?.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 = (error: Error): void => {
|
|
7
|
-
if (error && (error.message ?? '').includes('Cannot find module')) { // Handle module reloading
|
|
8
|
-
console.error('Cannot find module', { error });
|
|
9
|
-
} else {
|
|
10
|
-
throw error;
|
|
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 event of watchCompiler({ restartOnExit: true })) {
|
|
31
|
-
if (event.file && RuntimeIndex.hasModule(event.module) && VALID_FILE_TYPES.has(ManifestModuleUtil.getFileType(event.file))) {
|
|
32
|
-
if (event.action === 'update' || event.action === 'delete') {
|
|
33
|
-
await loader.unload(event.output);
|
|
34
|
-
}
|
|
35
|
-
if (event.action === 'create' || event.action === 'delete') {
|
|
36
|
-
RuntimeIndex.reinitForModule(Runtime.main.name);
|
|
37
|
-
}
|
|
38
|
-
if (event.action === 'create' || event.action === 'update') {
|
|
39
|
-
await loader.load(event.output);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
yield event;
|
|
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(value: unknown): value is Function {
|
|
11
|
-
const proto = value && Object.getPrototypeOf(value);
|
|
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, value: unknown): boolean {
|
|
47
|
-
return Object.setPrototypeOf(this.target, castTo(value));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
getPrototypeOf(target: T): object | null {
|
|
51
|
-
return Object.getPrototypeOf(this.target);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
get(target: T, property: PropertyKey, receiver: unknown): Any {
|
|
55
|
-
if (property === ProxyTargetSymbol) {
|
|
56
|
-
return this.target;
|
|
57
|
-
}
|
|
58
|
-
let result = this.target[castKey<T>(property)];
|
|
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, property: PropertyKey): boolean {
|
|
67
|
-
return castTo<object>(this.target).hasOwnProperty(property);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
set(target: T, property: PropertyKey, value: unknown): boolean {
|
|
71
|
-
this.target[castKey<T>(property)] = 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, property: PropertyKey): boolean {
|
|
83
|
-
return delete this.target[castKey<T>(property)];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
defineProperty(target: T, property: PropertyKey, attributes: PropertyDescriptor): boolean {
|
|
87
|
-
Object.defineProperty(this.target, property, 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>(value: U): U {
|
|
103
|
-
return castTo<{ [ProxyTargetSymbol]: U }>(value)?.[ProxyTargetSymbol] ?? value;
|
|
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
|
-
}
|
package/src/service/registry.ts
DELETED
|
@@ -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
|
-
#uniqueId = 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(event => 'current' in event).map(event => event.current));
|
|
57
|
-
|
|
58
|
-
for (const indexCls of this.#indexOrder) { // Visit every index, in order
|
|
59
|
-
const inst = this.instance(indexCls);
|
|
60
|
-
const matched = events.filter(event => inst.store.has('current' in event ? event.current : event.previous!));
|
|
61
|
-
if (matched.length) {
|
|
62
|
-
inst.process(matched);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
Util.queueMacroTask().then(() => {
|
|
67
|
-
this.#removeItems(events.filter(event => 'previous' in event).map(event => event.previous!));
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
for (const event of events) {
|
|
71
|
-
this.#emitter.emit('event', event);
|
|
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', { uniqueId: this.#uniqueId });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const added = await this.#classSource.init();
|
|
87
|
-
this.process(added.map(cls => ({ type: 'added', current: cls })));
|
|
88
|
-
this.#classSource.on(event => this.process([event]));
|
|
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', { uniqueId: this.#uniqueId, 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('current' in event ? event.current : event.previous!)) {
|
|
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 emitter = this.#methodSource ??= new MethodSource(this.#classSource);
|
|
153
|
-
if (!matches) {
|
|
154
|
-
emitter.on(handler);
|
|
155
|
-
} else {
|
|
156
|
-
const inst = this.instance(matches);
|
|
157
|
-
emitter.on((event) => {
|
|
158
|
-
if (inst.store.has('current' in event ? event.current[0] : event.previous[0])) {
|
|
159
|
-
handler(event);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export const Registry = new $Registry();
|
package/src/service/types.ts
DELETED
|
@@ -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.value?.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 source = Runtime.getSourceFile(cls);
|
|
39
|
-
if (!this.#classes.has(source)) {
|
|
40
|
-
this.#classes.set(source, new Map());
|
|
41
|
-
}
|
|
42
|
-
this.#classes.get(source)!.set(cls.Ⲑid, cls);
|
|
43
|
-
this.emit({ type: 'added', current: 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', previous: 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 previous = new Map<string, Class>();
|
|
66
|
-
if (this.#classes.has(sourceFile)) {
|
|
67
|
-
previous = new Map(this.#classes.get(sourceFile)!.entries());
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const keys = new Set([...Array.from(previous.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 key of keys) {
|
|
80
|
-
if (!next.has(key)) {
|
|
81
|
-
changes += 1;
|
|
82
|
-
this.emit({ type: 'removing', previous: previous.get(key)! });
|
|
83
|
-
this.#classes.get(sourceFile)!.delete(key);
|
|
84
|
-
} else {
|
|
85
|
-
this.#classes.get(sourceFile)!.set(key, next.get(key)!);
|
|
86
|
-
if (!previous.has(key)) {
|
|
87
|
-
changes += 1;
|
|
88
|
-
this.emit({ type: 'added', current: next.get(key)! });
|
|
89
|
-
} else {
|
|
90
|
-
const prevHash = describeFunction(previous.get(key)!)?.hash;
|
|
91
|
-
const nextHash = describeFunction(next.get(key)!)?.hash;
|
|
92
|
-
if (prevHash !== nextHash) {
|
|
93
|
-
changes += 1;
|
|
94
|
-
this.emit({ type: 'changed', current: next.get(key)!, previous: previous.get(key)! });
|
|
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 cls of classes) {
|
|
108
|
-
const importPath = Runtime.getImport(cls);
|
|
109
|
-
if (!classesByFile.has(importPath)) {
|
|
110
|
-
classesByFile.set(importPath, []);
|
|
111
|
-
}
|
|
112
|
-
classesByFile.get(importPath)!.push(cls);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for (const [importPath, items] of classesByFile.entries()) {
|
|
116
|
-
if (!this.#handleFileChanges(importPath, items)) {
|
|
117
|
-
this.#emitter.emit('unchanged-import', importPath);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Emit a change event
|
|
124
|
-
*/
|
|
125
|
-
emit(event: ChangeEvent<Class>): void {
|
|
126
|
-
if (this.trace) {
|
|
127
|
-
console.debug('Emitting change', {
|
|
128
|
-
type: event.type,
|
|
129
|
-
current: (event.type !== 'removing' ? event.current?.Ⲑid : undefined),
|
|
130
|
-
previous: (event.type !== 'added' ? event.previous?.Ⲑid : undefined)
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
this.#emitter.emit('change', event);
|
|
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 event of await DynamicFileLoader.listen()) {
|
|
143
|
-
if (event.action === 'delete') {
|
|
144
|
-
this.#removeFile(event.file); // File no longer exists
|
|
145
|
-
} else {
|
|
146
|
-
this.#handleChanges(flushPendingFunctions().filter(isClass));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (event.action === 'create') {
|
|
150
|
-
this.#flush();
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
})();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Ensure everything is loaded
|
|
157
|
-
for (const entry of RuntimeIndex.find({
|
|
158
|
-
module: (mod) => {
|
|
159
|
-
const role = Env.TRV_ROLE.value;
|
|
160
|
-
return role !== 'test' && // Skip all modules when in test
|
|
161
|
-
mod.roles.includes('std') &&
|
|
162
|
-
(
|
|
163
|
-
!Runtime.production || mod.prod ||
|
|
164
|
-
(role === 'doc' && mod.roles.includes(role))
|
|
165
|
-
);
|
|
166
|
-
},
|
|
167
|
-
folder: folder => folder === 'src' || folder === '$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,64 +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(event => this.onClassEvent(event));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async init(): Promise<void> { }
|
|
23
|
-
|
|
24
|
-
emit(event: ChangeEvent<[Class, Function]>): void {
|
|
25
|
-
this.#emitter.emit('change', event);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* On a class being emitted, check methods
|
|
30
|
-
*/
|
|
31
|
-
onClassEvent(event: ChangeEvent<Class>): void {
|
|
32
|
-
const next = (event.type !== 'removing' ? describeFunction(event.current!)?.methods : null) ?? {};
|
|
33
|
-
const previous = (event.type !== 'added' ? describeFunction(event.previous!)?.methods : null) ?? {};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Go through each method, comparing hashes. To see added/removed and changed
|
|
37
|
-
*/
|
|
38
|
-
for (const key of Object.keys(next)) {
|
|
39
|
-
if ((!previous[key] || !('previous' in event)) && event.type !== 'removing') {
|
|
40
|
-
this.emit({ type: 'added', current: [event.current!, event.current!.prototype[key]] });
|
|
41
|
-
} else if (next[key].hash !== previous[key].hash && event.type === 'changed') {
|
|
42
|
-
// FIXME: Why is event.previous undefined sometimes?
|
|
43
|
-
this.emit({
|
|
44
|
-
type: 'changed',
|
|
45
|
-
current: [event.current, event.current.prototype[key]],
|
|
46
|
-
previous: [event.previous, event.previous.prototype[key]]
|
|
47
|
-
});
|
|
48
|
-
} else {
|
|
49
|
-
// Method Unchanged
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for (const key of Object.keys(previous)) {
|
|
54
|
-
if (!next[key] && event.type !== 'added') {
|
|
55
|
-
this.emit({ type: 'removing', previous: [event.previous, event.previous.prototype[key]] });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
on(callback: ChangeHandler<[Class, Function]>): this {
|
|
61
|
-
this.#emitter.on('change', callback);
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
}
|