@travetto/registry 6.0.0 → 7.0.0-rc.0

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,20 +20,21 @@ 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 [RootRegistry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/root.ts#L10) and will automatically register/load all relevant files
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
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 [RootRegistry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/root.ts#L10) 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/service/registry.ts#L9) 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
 
29
29
  **Code: Sample Registry**
30
30
  ```typescript
31
31
  import { Class } from '@travetto/runtime';
32
- import { MetadataRegistry } from '@travetto/registry';
32
+ import { ChangeEvent, RegistryAdapter, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
33
33
 
34
34
  interface Group {
35
35
  class: Class;
36
36
  name: string;
37
+ children: Child[];
37
38
  }
38
39
 
39
40
  interface Child {
@@ -41,41 +42,68 @@ interface Child {
41
42
  method: Function;
42
43
  }
43
44
 
44
- function isComplete(o: Partial<Group>): o is Group {
45
- return !!o;
46
- }
45
+ /**
46
+ * The adapter to handle mapping/modeling a specific class
47
+ */
48
+ class SampleRegistryAdapter implements RegistryAdapter<Group> {
49
+
50
+ #class: Class;
51
+ #config: Group;
52
+
53
+ constructor(cls: Class) {
54
+ this.#class = cls;
55
+ }
47
56
 
48
- export class SampleRegistry extends MetadataRegistry<Group, Child> {
49
- /**
50
- * Finalize class after all metadata is collected
51
- */
52
- onInstallFinalize<T>(cls: Class<T>): Group {
53
- const pending: Partial<Group> = this.getOrCreatePending(cls);
54
- if (isComplete(pending)) {
55
- return pending;
56
- } else {
57
- throw new Error('Invalid Group');
57
+ register(...data: Partial<Partial<Group>>[]): Group {
58
+ for (const d of data) {
59
+ Object.assign(this.#config, {
60
+ ...d,
61
+ children: [
62
+ ...(this.#config?.children ?? []),
63
+ ...(d.children ?? [])
64
+ ]
65
+ });
58
66
  }
67
+ return this.#config;
68
+ }
69
+
70
+ registerChild(method: Function, name: string): void {
71
+ this.register({ children: [{ method, name }] });
72
+ }
73
+
74
+ finalize?(parent?: Partial<Group> | undefined): void {
75
+ // Nothing to do
59
76
  }
60
77
 
61
- /**
62
- * Create scaffolding on first encounter of a class
63
- */
64
- createPending(cls: Class): Partial<Group> {
65
- return {
66
- class: cls,
67
- name: cls.name
68
- };
78
+ get(): Group {
79
+ return this.#config;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Basic Index that handles cross-class activity
85
+ */
86
+ export class SampleRegistryIndex implements RegistryIndex {
87
+ static #instance = Registry.registerIndex(SampleRegistryIndex);
88
+
89
+ static getForRegister(cls: Class, allowFinalized = false): SampleRegistryAdapter {
90
+ return this.#instance.store.getForRegister(cls, allowFinalized);
91
+ }
92
+
93
+ store = new RegistryIndexStore(SampleRegistryAdapter);
94
+
95
+ process(events: ChangeEvent<Class>[]): void {
96
+ // Nothing to do
69
97
  }
70
98
  }
71
99
  ```
72
100
 
73
- The registry is a [MetadataRegistry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/metadata.ts#L14) 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/service/types.ts#L34) 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.
74
102
 
75
103
  ### Live Flow
76
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.
77
105
 
78
- 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 [RootRegistry](https://github.com/travetto/travetto/tree/main/module/registry/src/service/root.ts#L10) will pick it up, and process it accordingly.
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.
79
107
 
80
108
  ## Supporting Metadata
81
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.
@@ -116,7 +144,7 @@ As mentioned in [Manifest](https://github.com/travetto/travetto/tree/main/module
116
144
  const nextHash = describeFunction(next.get(k)!)?.hash;
117
145
  if (prevHash !== nextHash) {
118
146
  changes += 1;
119
- this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k) });
147
+ this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k)! });
120
148
  }
121
149
  }
122
150
  }
package/__index__.ts CHANGED
@@ -1,8 +1,7 @@
1
- export * from './src/registry.ts';
2
- export * from './src/service/metadata.ts';
3
- export * from './src/service/root.ts';
1
+ export * from './src/service/registry.ts';
2
+ export * from './src/service/types.ts';
3
+ export * from './src/service/store.ts';
4
4
  export * from './src/proxy.ts';
5
- export * from './src/registry.ts';
6
5
  export * from './src/source/class-source.ts';
7
6
  export * from './src/source/method-source.ts';
8
7
  export * from './src/types.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/registry",
3
- "version": "6.0.0",
3
+ "version": "7.0.0-rc.0",
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": "^6.0.0"
30
+ "@travetto/runtime": "^7.0.0-rc.0"
31
31
  },
32
32
  "peerDependencies": {
33
- "@travetto/cli": "^6.0.0",
34
- "@travetto/transformer": "^6.0.0"
33
+ "@travetto/cli": "^7.0.0-rc.0",
34
+ "@travetto/transformer": "^7.0.0-rc.0"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/transformer": {
@@ -0,0 +1,164 @@
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 } 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
+ }
31
+ }
32
+
33
+ #finalizeItems(classes: Class[]): void {
34
+ for (const idx of this.#indexOrder) {
35
+ const inst = this.instance(idx);
36
+ for (const cls of classes) {
37
+ if (inst.store.has(cls) && !inst.store.finalized(cls)) {
38
+ if (inst.finalize) {
39
+ inst.finalize(cls);
40
+ } else {
41
+ inst.store.finalize(cls);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ finalizeForIndex(indexCls: RegistryIndexClass): void {
49
+ const inst = this.instance(indexCls);
50
+ this.#finalizeItems(inst.store.getClasses());
51
+ }
52
+
53
+ process(events: ChangeEvent<Class>[]): void {
54
+ this.#finalizeItems(events.filter(ev => 'curr' in ev).map(ev => ev.curr));
55
+
56
+ for (const indexCls of this.#indexOrder) { // Visit every index, in order
57
+ const inst = this.instance(indexCls);
58
+ const matched = events.filter(e => inst.store.has('curr' in e ? e.curr : e.prev!));
59
+ if (matched.length) {
60
+ inst.process(matched);
61
+ }
62
+ }
63
+
64
+ Util.queueMacroTask().then(() => {
65
+ this.#removeItems(events.filter(ev => 'prev' in ev).map(ev => ev.prev!));
66
+ });
67
+
68
+ for (const e of events) {
69
+ this.#emitter.emit('event', e);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Run initialization
75
+ */
76
+ async #init(): Promise<void> {
77
+ try {
78
+ this.#resolved = false;
79
+
80
+ if (this.trace) {
81
+ console.debug('Initializing', { uid: this.#uid });
82
+ }
83
+
84
+ const added = await this.#classSource.init();
85
+ this.process(added.map(cls => ({ type: 'added', curr: cls })));
86
+ this.#classSource.on(e => this.process([e]));
87
+ } finally {
88
+ this.#resolved = true;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Verify initialized state
94
+ */
95
+ verifyInitialized(): void {
96
+ if (!this.#resolved) {
97
+ throw new AppError('Registry not initialized, call init() first');
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Register a new index
103
+ */
104
+ registerIndex<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
105
+ if (!this.#indexes.has(indexCls)) {
106
+ this.#indexes.set(indexCls, new indexCls());
107
+ this.#indexOrder.push(indexCls);
108
+ }
109
+ return castTo(this.#indexes.get(indexCls));
110
+ }
111
+
112
+ /**
113
+ * Initialize, with a built-in latch to prevent concurrent initializations
114
+ */
115
+ async init(): Promise<unknown> {
116
+ if (this.trace && this.#initialized) {
117
+ console.trace('Trying to re-initialize', { uid: this.#uid, initialized: !!this.#initialized });
118
+ }
119
+ return this.#initialized ??= this.#init();
120
+ }
121
+
122
+ instance<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
123
+ return castTo(this.#indexes.get(indexCls));
124
+ }
125
+
126
+ /**
127
+ * Listen for changes
128
+ */
129
+ onClassChange(handler: (event: ChangeEvent<Class>) => void, matches?: RegistryIndexClass): void {
130
+ if (!matches) {
131
+ this.#emitter.on('event', handler);
132
+ } else {
133
+ const inst = this.instance(matches);
134
+ this.#emitter.on('event', (event) => {
135
+ if (inst.store.has('curr' in event ? event.curr : event.prev!)) {
136
+ handler(event);
137
+ }
138
+ });
139
+ }
140
+ }
141
+
142
+ onNonClassChanges(handler: (file: string) => void): void {
143
+ this.#classSource.onNonClassChanges(handler);
144
+ }
145
+
146
+ onMethodChange(
147
+ handler: (event: ChangeEvent<[Class, Function]>) => void,
148
+ matches?: RegistryIndexClass,
149
+ ): void {
150
+ const src = this.#methodSource ??= new MethodSource(this.#classSource);
151
+ if (!matches) {
152
+ src.on(handler);
153
+ } else {
154
+ const inst = this.instance(matches);
155
+ src.on((event) => {
156
+ if (inst.store.has('curr' in event ? event.curr[0] : event.prev[0])) {
157
+ handler(event);
158
+ }
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ export const Registry = new $Registry();
@@ -0,0 +1,80 @@
1
+ import { AppError, castTo, Class, getParentClass } from '@travetto/runtime';
2
+
3
+ import { RegistrationMethods, RegistryAdapter } from './types';
4
+
5
+ /**
6
+ * Base registry index implementation
7
+ */
8
+ export class RegistryIndexStore<A extends RegistryAdapter<{}> = RegistryAdapter<{}>> {
9
+
10
+ // Core data
11
+ #adapters = new Map<Class, A>();
12
+ #idToCls = new Map<string, Class>();
13
+ #adapterCls: new (cls: Class) => A;
14
+ #finalized = new Map<Class, boolean>();
15
+
16
+ constructor(adapterCls: new (cls: Class) => A) {
17
+ this.#adapterCls = adapterCls;
18
+ }
19
+
20
+ getClasses(): Class[] {
21
+ return Array.from(this.#adapters.keys());
22
+ }
23
+
24
+ has(cls: Class): boolean {
25
+ return this.#adapters.has(cls);
26
+ }
27
+
28
+ getClassById(id: string): Class {
29
+ return this.#idToCls.get(id)!;
30
+ }
31
+
32
+ finalize(cls: Class, parentConfig?: ReturnType<A['get']>): void {
33
+ if (!parentConfig) {
34
+ const parentClass = getParentClass(cls);
35
+ parentConfig = castTo(parentClass && this.has(parentClass) ? this.get(parentClass).get() : undefined);
36
+ }
37
+ this.adapter(cls).finalize?.(parentConfig);
38
+ this.#finalized.set(cls, true);
39
+ }
40
+
41
+ adapter(cls: Class): A {
42
+ if (!this.#adapters.has(cls)!) {
43
+ const adapter = new this.#adapterCls(cls);
44
+ this.#adapters.set(cls, adapter);
45
+ this.#idToCls.set(cls.Ⲑid, cls);
46
+ }
47
+
48
+ return castTo(this.#adapters.get(cls));
49
+ }
50
+
51
+ remove(cls: Class): void {
52
+ this.#adapters.delete(cls);
53
+ this.#finalized.delete(cls);
54
+ }
55
+
56
+ getForRegister(cls: Class, allowFinalized = false): A {
57
+ if (this.#finalized.get(cls) && !allowFinalized) {
58
+ throw new AppError(`Class ${cls.Ⲑid} is already finalized`);
59
+ }
60
+ return this.adapter(cls);
61
+ }
62
+
63
+ get(cls: Class): Omit<A, RegistrationMethods> {
64
+ if (!this.has(cls)) {
65
+ throw new AppError(`Class ${cls.Ⲑid} is not registered for ${this.#adapterCls.Ⲑid}`);
66
+ }
67
+ return this.adapter(cls);
68
+ }
69
+
70
+ getOptional(cls: Class): Omit<A, RegistrationMethods> | undefined {
71
+ if (!this.has(cls)) {
72
+ return undefined;
73
+ }
74
+ return this.adapter(cls);
75
+ }
76
+
77
+ finalized(cls: Class): boolean {
78
+ return this.#finalized.has(cls);
79
+ }
80
+ }
@@ -0,0 +1,38 @@
1
+ import { Class } from '@travetto/runtime';
2
+ import { ChangeEvent } from '../types';
3
+
4
+ export type RegistrationMethods = `register${string}` | `finalize${string}`;
5
+
6
+ /**
7
+ * Interface for registry adapters to implement
8
+ */
9
+ export interface RegistryAdapter<C extends {} = {}> {
10
+ register(...data: Partial<C>[]): C;
11
+ finalize?(parent?: C): void;
12
+ get(): C;
13
+ }
14
+
15
+ export type RegistryIndexClass = {
16
+ new(): RegistryIndex;
17
+ };
18
+
19
+ /**
20
+ * Simple store interface for registry indexes
21
+ */
22
+ export interface RegistrySimpleStore {
23
+ has(cls: Class): boolean;
24
+ finalize(cls: Class): void;
25
+ finalized(cls: Class): boolean;
26
+ remove(cls: Class): void;
27
+ getClasses(): Class[];
28
+ };
29
+
30
+ /**
31
+ * Registry index definition
32
+ * @concrete
33
+ */
34
+ export interface RegistryIndex {
35
+ store: RegistrySimpleStore;
36
+ process(events: ChangeEvent<Class>[]): void;
37
+ finalize?(cls: Class): void;
38
+ }
@@ -17,7 +17,11 @@ function isClass(cls: Function): cls is Class {
17
17
  export class ClassSource implements ChangeSource<Class> {
18
18
 
19
19
  #classes = new Map<string, Map<string, Class>>();
20
- #emitter = new EventEmitter();
20
+ #emitter = new EventEmitter<{
21
+ change: [ChangeEvent<Class>];
22
+ // eslint-disable-next-line @typescript-eslint/naming-convention
23
+ 'unchanged-import': [string];
24
+ }>();
21
25
  #listening: Promise<void> | undefined;
22
26
 
23
27
  /**
@@ -28,8 +32,9 @@ export class ClassSource implements ChangeSource<Class> {
28
32
  /**
29
33
  * Flush classes
30
34
  */
31
- #flush(): void {
32
- for (const cls of flushPendingFunctions().filter(isClass)) {
35
+ #flush(): Class[] {
36
+ const flushed = flushPendingFunctions().filter(isClass);
37
+ for (const cls of flushed) {
33
38
  const src = Runtime.getSourceFile(cls);
34
39
  if (!this.#classes.has(src)) {
35
40
  this.#classes.set(src, new Map());
@@ -37,6 +42,7 @@ export class ClassSource implements ChangeSource<Class> {
37
42
  this.#classes.get(src)!.set(cls.Ⲑid, cls);
38
43
  this.emit({ type: 'added', curr: cls });
39
44
  }
45
+ return flushed;
40
46
  }
41
47
 
42
48
  #removeFile(file: string): void {
@@ -85,7 +91,7 @@ export class ClassSource implements ChangeSource<Class> {
85
91
  const nextHash = describeFunction(next.get(k)!)?.hash;
86
92
  if (prevHash !== nextHash) {
87
93
  changes += 1;
88
- this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k) });
94
+ this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k)! });
89
95
  }
90
96
  }
91
97
  }
@@ -118,7 +124,11 @@ export class ClassSource implements ChangeSource<Class> {
118
124
  */
119
125
  emit(e: ChangeEvent<Class>): void {
120
126
  if (this.trace) {
121
- console.debug('Emitting change', { type: e.type, curr: e.curr?.Ⲑid, prev: e.prev?.Ⲑid });
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
+ });
122
132
  }
123
133
  this.#emitter.emit('change', e);
124
134
  }
@@ -126,7 +136,7 @@ export class ClassSource implements ChangeSource<Class> {
126
136
  /**
127
137
  * Initialize
128
138
  */
129
- async init(): Promise<void> {
139
+ async init(): Promise<Class[]> {
130
140
  if (Runtime.dynamic && !this.#listening) {
131
141
  this.#listening = (async (): Promise<void> => {
132
142
  for await (const ev of await DynamicFileLoader.listen()) {
@@ -147,10 +157,12 @@ export class ClassSource implements ChangeSource<Class> {
147
157
  for (const entry of RuntimeIndex.find({
148
158
  module: (m) => {
149
159
  const role = Env.TRV_ROLE.val;
150
- return m.roles.includes('std') && (
151
- !Runtime.production || m.prod ||
152
- ((role === 'doc' || role === 'test') && m.roles.includes(role))
153
- );
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
+ );
154
166
  },
155
167
  folder: f => f === 'src' || f === '$index'
156
168
  })) {
@@ -158,7 +170,7 @@ export class ClassSource implements ChangeSource<Class> {
158
170
  }
159
171
 
160
172
  // Flush all load events
161
- this.#flush();
173
+ return this.#flush();
162
174
  }
163
175
 
164
176
  /**
@@ -168,6 +180,13 @@ export class ClassSource implements ChangeSource<Class> {
168
180
  this.#emitter.on('change', callback);
169
181
  }
170
182
 
183
+ /**
184
+ * Add callback for change events
185
+ */
186
+ off(callback: ChangeHandler<Class>): void {
187
+ this.#emitter.off('change', callback);
188
+ }
189
+
171
190
  /**
172
191
  * Add callback for when a import is changed, but emits no class changes
173
192
  */
@@ -29,16 +29,16 @@ export class MethodSource implements ChangeSource<[Class, Function]> {
29
29
  * On a class being emitted, check methods
30
30
  */
31
31
  onClassEvent(e: ChangeEvent<Class>): void {
32
- const next = describeFunction(e.curr!)?.methods ?? {};
33
- const prev = describeFunction(e.prev!)?.methods ?? {};
32
+ const next = (e.type !== 'removing' ? describeFunction(e.curr!)?.methods : null) ?? {};
33
+ const prev = (e.type !== 'added' ? describeFunction(e.prev!)?.methods : null) ?? {};
34
34
 
35
35
  /**
36
36
  * Go through each method, comparing hashes. To see added/removed and changed
37
37
  */
38
38
  for (const k of Object.keys(next)) {
39
- if (!prev[k] || !e.prev) {
39
+ if ((!prev[k] || !('prev' in e)) && e.type !== 'removing') {
40
40
  this.emit({ type: 'added', curr: [e.curr!, e.curr!.prototype[k]] });
41
- } else if (next[k].hash !== prev[k].hash && e.curr) {
41
+ } else if (next[k].hash !== prev[k].hash && e.type === 'changed') {
42
42
  // FIXME: Why is e.prev undefined sometimes?
43
43
  this.emit({ type: 'changed', curr: [e.curr, e.curr.prototype[k]], prev: [e.prev, e.prev.prototype[k]] });
44
44
  } else {
@@ -47,7 +47,7 @@ export class MethodSource implements ChangeSource<[Class, Function]> {
47
47
  }
48
48
 
49
49
  for (const k of Object.keys(prev)) {
50
- if (!next[k] && e.prev) {
50
+ if (!next[k] && e.type !== 'added') {
51
51
  this.emit({ type: 'removing', prev: [e.prev, e.prev.prototype[k]] });
52
52
  }
53
53
  }
package/src/types.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * A change event
3
3
  */
4
- export interface ChangeEvent<T> {
5
- type: 'changed' | 'added' | 'removing';
6
- prev?: T;
7
- curr?: T;
8
- }
4
+ export type ChangeEvent<T> =
5
+ { type: 'changed', prev: T, curr: T } |
6
+ { type: 'added', curr: T } |
7
+ { type: 'removing', prev: T };
9
8
 
10
9
  /**
11
10
  * Change handler
package/src/registry.ts DELETED
@@ -1,209 +0,0 @@
1
- import { EventEmitter } from 'node:events';
2
- import { Class, Env } from '@travetto/runtime';
3
- import { ChangeSource, ChangeEvent, ChangeHandler } from './types.ts';
4
-
5
- /**
6
- * Base registry class, designed to listen to changes over time
7
- */
8
- export abstract class Registry implements ChangeSource<Class> {
9
-
10
- /**
11
- * Has the registry been resolved
12
- */
13
- #resolved: boolean;
14
- /**
15
- * Initializing promises
16
- */
17
- #initialized?: Promise<unknown>;
18
- /**
19
- * Event emitter, to broadcast event changes
20
- */
21
- #emitter = new EventEmitter();
22
- /**
23
- * Dependent registries
24
- */
25
- #dependents: Registry[] = [];
26
- /**
27
- * Parent registries
28
- */
29
- #parents: ChangeSource<Class>[] = [];
30
- /**
31
- * Unique identifier
32
- */
33
- #uid: string;
34
-
35
- /**
36
- * Are we in a mode that should have enhanced debug info
37
- */
38
- trace = Env.DEBUG.val?.includes('@travetto/registry');
39
-
40
- /**
41
- * Creates a new registry, with it's parents specified
42
- */
43
- constructor(...parents: ChangeSource<Class>[]) {
44
- this.#uid = `${this.constructor.name}_${Date.now()}`;
45
- this.#parents = parents;
46
-
47
- if (this.#parents.length) {
48
- // Have the child listen to the parents
49
- for (const parent of this.#parents) {
50
- this.listen(parent);
51
- if (parent instanceof Registry) {
52
- parent.#dependents.push(this);
53
- }
54
- }
55
- }
56
- }
57
-
58
- /**
59
- * Run initialization
60
- */
61
- async #runInit(): Promise<void> {
62
- try {
63
- this.#resolved = false;
64
- if (this.trace) {
65
- console.debug('Initializing', { id: this.constructor.Ⲑid, uid: this.#uid });
66
- }
67
-
68
- // Handle top level when dealing with non-registry
69
- const waitFor = this.#parents.filter(x => !(x instanceof Registry));
70
- await Promise.all(waitFor.map(x => x.init()));
71
-
72
- const classes = await this.initialInstall();
73
-
74
- if (classes) {
75
- for (const cls of classes) {
76
- this.install(cls, { type: 'added', curr: cls });
77
- }
78
- }
79
-
80
- await Promise.all(this.#dependents.map(x => x.init()));
81
- } finally {
82
- this.#resolved = true;
83
- }
84
- }
85
-
86
- get resolved(): boolean {
87
- return this.#resolved;
88
- }
89
-
90
- /**
91
- * Return list of classes for the initial installation
92
- */
93
- initialInstall(): Class[] {
94
- return [];
95
- }
96
-
97
- /**
98
- * Verify initialized state
99
- */
100
- verifyInitialized(): void {
101
- if (!this.#resolved) {
102
- throw new Error(`${this.constructor.name} has not been initialized, you probably need to call RootRegistry.init()`);
103
- }
104
- }
105
-
106
- /**
107
- * Initialize, with a built-in latch to prevent concurrent initializations
108
- */
109
- async init(): Promise<unknown> {
110
- if (this.trace) {
111
- console.debug('Trying to initialize', { id: this.constructor.Ⲑid, uid: this.#uid, initialized: !!this.#initialized });
112
- }
113
-
114
- if (!this.#initialized) {
115
- this.#initialized = this.#runInit();
116
- }
117
- return this.#initialized;
118
- }
119
-
120
- parent<T extends ChangeSource<Class>>(type: Class<T>): T | undefined {
121
- return this.#parents.find((dep: unknown): dep is T => dep instanceof type);
122
- }
123
-
124
- /**
125
- * When an installation event occurs
126
- */
127
- onInstall?(cls: Class, e: ChangeEvent<Class>): void;
128
-
129
- /**
130
- * When an un-installation event occurs
131
- */
132
- onUninstall?(cls: Class, e: ChangeEvent<Class>): void;
133
-
134
- /**
135
- * Uninstall a class or list of classes
136
- */
137
- uninstall(classes: Class | Class[], e: ChangeEvent<Class>): void {
138
- if (!Array.isArray(classes)) {
139
- classes = [classes];
140
- }
141
- for (const cls of classes) {
142
- this.onUninstall?.(cls, e);
143
- }
144
- }
145
-
146
- /**
147
- * Install a class or a list of classes
148
- */
149
- install(classes: Class | Class[], e: ChangeEvent<Class>): void {
150
- if (!Array.isArray(classes)) {
151
- classes = [classes];
152
- }
153
- for (const cls of classes) {
154
- this.onInstall?.(cls, e);
155
- }
156
- }
157
-
158
- /**
159
- * Listen for events from the parent
160
- */
161
- onEvent(event: ChangeEvent<Class>): void {
162
- if (this.trace) {
163
- console.debug('Received', { id: this.constructor.Ⲑid, type: event.type, targetId: (event.curr ?? event.prev)!.Ⲑid });
164
- }
165
-
166
- switch (event.type) {
167
- case 'removing':
168
- this.uninstall(event.prev!, event);
169
- break;
170
- case 'added':
171
- this.install(event.curr!, event);
172
- break;
173
- case 'changed':
174
- this.uninstall(event.prev!, event);
175
- this.install(event.curr!, event);
176
- break;
177
- default:
178
- return;
179
- }
180
- }
181
-
182
- /**
183
- * Emit a new event
184
- */
185
- emit(e: ChangeEvent<Class>): void {
186
- this.#emitter.emit('change', e);
187
- }
188
-
189
- /**
190
- * Register additional listeners
191
- */
192
- on<T>(callback: ChangeHandler<Class<T>>): void {
193
- this.#emitter.on('change', callback);
194
- }
195
-
196
- /**
197
- * Remove listeners
198
- */
199
- off<T>(callback: ChangeHandler<Class<T>>): void {
200
- this.#emitter.off('change', callback);
201
- }
202
-
203
- /**
204
- * Connect changes sources
205
- */
206
- listen(source: ChangeSource<Class>): void {
207
- source.on(e => this.onEvent(e));
208
- }
209
- }
@@ -1,192 +0,0 @@
1
- /* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "none"} ] */
2
- import { Class } from '@travetto/runtime';
3
-
4
- import { Registry } from '../registry.ts';
5
- import { ChangeEvent } from '../types.ts';
6
-
7
- function id(cls: string | Class): string {
8
- return cls && typeof cls !== 'string' ? cls.Ⲑid : cls;
9
- }
10
-
11
- /**
12
- * Metadata registry
13
- */
14
- export abstract class MetadataRegistry<C extends { class: Class }, M = unknown, F = Function> extends Registry {
15
-
16
- static id = id;
17
- /**
18
- * Classes pending removal
19
- */
20
- expired = new Map<string, C>();
21
- /**
22
- * Classes pending creation
23
- */
24
- pending = new Map<string, Partial<C>>();
25
- /**
26
- * Fields pending creation
27
- */
28
- pendingFields = new Map<string, Map<F, Partial<M>>>();
29
- /**
30
- * Active items
31
- */
32
- entries = new Map<string, C>();
33
-
34
- /**
35
- * Code to call when the installation is finalized
36
- */
37
- abstract onInstallFinalize<T>(cls: Class<T>): C;
38
-
39
- /**
40
- * Code to call when uninstall is finalized
41
- */
42
- onUninstallFinalize<T>(cls: Class<T>): void {
43
-
44
- }
45
-
46
- /**
47
- * Create a pending class. Items are pending until the registry is activated
48
- */
49
- abstract createPending(cls: Class): Partial<C>;
50
-
51
- /**
52
- * Is class found by id or by Class
53
- */
54
- has(cls: string | Class): boolean {
55
- return this.entries.has(id(cls));
56
- }
57
-
58
- /**
59
- * Get class by id or by Class
60
- */
61
- get(cls: string | Class): C {
62
- return this.entries.get(id(cls))!;
63
- }
64
-
65
- /**
66
- * Retrieve the class that is being removed
67
- */
68
- getExpired(cls: string | Class): C {
69
- return this.expired.get(id(cls))!;
70
- }
71
-
72
- /**
73
- * Is there a class that is expiring
74
- */
75
- hasExpired(cls: string | Class): boolean {
76
- return this.expired.has(id(cls));
77
- }
78
-
79
- /**
80
- * Is there a pending state for the class
81
- */
82
- hasPending(cls: string | Class): boolean {
83
- return this.pending.has(id(cls));
84
- }
85
-
86
- /**
87
- * Get list of all classes that have been registered
88
- */
89
- getClasses(): Class[] {
90
- return Array.from(this.entries.values()).map(x => x.class);
91
- }
92
-
93
- /**
94
- * Trigger initial install, moves pending to finalized (active)
95
- */
96
- override initialInstall(): Class[] {
97
- return Array.from(this.pending.values()).map(x => x.class).filter(x => !!x);
98
- }
99
-
100
- /**
101
- * Create a pending field
102
- */
103
- createPendingField(cls: Class, field: F): Partial<M> {
104
- return {};
105
- }
106
-
107
- /**
108
- * Find parent class for a given class object
109
- */
110
- getParentClass(cls: Class): Class | null {
111
- const parent: Class = Object.getPrototypeOf(cls);
112
- return parent.name && parent !== Object ? parent : null;
113
- }
114
-
115
- /**
116
- * Get or create a pending class
117
- */
118
- getOrCreatePending(cls: Class): Partial<C> {
119
- const cid = id(cls);
120
- if (!this.pending.has(cid)) {
121
- this.pending.set(cid, this.createPending(cls));
122
- this.pendingFields.set(cid, new Map());
123
- }
124
- return this.pending.get(cid)!;
125
- }
126
-
127
- /**
128
- * Get or create a pending field
129
- */
130
- getOrCreatePendingField(cls: Class, field: F): Partial<M> {
131
- this.getOrCreatePending(cls);
132
- const classId = cls.Ⲑid;
133
-
134
- if (!this.pendingFields.get(classId)!.has(field)) {
135
- this.pendingFields.get(classId)!.set(field, this.createPendingField(cls, field));
136
- }
137
- return this.pendingFields.get(classId)!.get(field)!;
138
- }
139
-
140
- /**
141
- * Register a pending class, with partial config to overlay
142
- */
143
- register(cls: Class, pConfig: Partial<C> = {}): void {
144
- const conf = this.getOrCreatePending(cls);
145
- Object.assign(conf, pConfig);
146
- }
147
-
148
- /**
149
- * Register a pending field, with partial config to overlay
150
- */
151
- registerField(cls: Class, field: F, pConfig: Partial<M>): void {
152
- const conf = this.getOrCreatePendingField(cls, field);
153
- Object.assign(conf, pConfig);
154
- }
155
-
156
- /**
157
- * On an install event, finalize
158
- */
159
- onInstall(cls: Class, e: ChangeEvent<Class>): void {
160
- const classId = cls.Ⲑid;
161
- if (this.pending.has(classId) || this.pendingFields.has(classId)) {
162
- if (this.trace) {
163
- console.debug('Installing', { service: this.constructor.name, id: classId });
164
- }
165
- const result = this.onInstallFinalize(cls);
166
- this.pendingFields.delete(classId);
167
- this.pending.delete(classId);
168
-
169
- this.entries.set(classId, result);
170
- this.emit(e);
171
- }
172
- }
173
-
174
- /**
175
- * On an uninstall event, remove
176
- */
177
- onUninstall(cls: Class, e: ChangeEvent<Class>): void {
178
- const classId = cls.Ⲑid;
179
- if (this.entries.has(classId)) {
180
- if (this.trace) {
181
- console.debug('Uninstalling', { service: this.constructor.name, id: classId });
182
- }
183
- this.expired.set(classId, this.entries.get(classId)!);
184
- this.entries.delete(classId);
185
- this.onUninstallFinalize(cls);
186
- if (e.type === 'removing') {
187
- this.emit(e);
188
- }
189
- process.nextTick(() => this.expired.delete(classId));
190
- }
191
- }
192
- }
@@ -1,32 +0,0 @@
1
- import { Class } from '@travetto/runtime';
2
-
3
- import { Registry } from '../registry.ts';
4
- import { ClassSource } from '../source/class-source.ts';
5
- import { ChangeEvent } from '../types.ts';
6
-
7
- /**
8
- * The root registry that controls all registries
9
- */
10
- class $RootRegistry extends Registry {
11
- constructor() {
12
- super(new ClassSource());
13
- }
14
-
15
- /**
16
- * Send event to all all of the children
17
- */
18
- override async onEvent(e: ChangeEvent<Class>): Promise<void> {
19
- await super.onEvent(e); // Process event, and
20
- this.emit(e); // Send to children
21
- }
22
-
23
- /**
24
- * Registers a listener to be notified when a file changes, but no
25
- * classes are modified
26
- */
27
- onNonClassChanges(handler: (file: string) => void): void {
28
- this.parent(ClassSource)!.onNonClassChanges(handler);
29
- }
30
- }
31
-
32
- export const RootRegistry = new $RootRegistry();