mani-game-engine 1.0.0-pre.3 → 1.0.0-pre.30

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.
@@ -0,0 +1,363 @@
1
+ import 'reflect-metadata';
2
+
3
+ export type Class<T = any> = { name: string, new(...args: any[]): T; }
4
+
5
+ type ProviderFunction = () => unknown;
6
+ export type ResolverFunction = (context?: any) => unknown;
7
+
8
+ export type ID = string | symbol;
9
+
10
+ export type Dependency = { kind: string; index: number };
11
+ type TypeDependency = Dependency & { kind: 'type'; index: number; id: ID; };
12
+ type InjectDependency = Dependency & { kind: 'inject'; index: number; id: ID; type: Class; };
13
+
14
+ const enum TypeMappingKind { CLASS, SINGLETON, VALUE, PROVIDER}
15
+
16
+ type ClassTypeMapping = { kind: TypeMappingKind.CLASS; type: Class };
17
+ type SingletonTypeMapping = { kind: TypeMappingKind.SINGLETON; type: Class; injector: Injector };
18
+ type ValueTypeMapping = { kind: TypeMappingKind.VALUE; value: unknown; };
19
+ type ProviderTypeMapping = { kind: TypeMappingKind.PROVIDER; provider: ProviderFunction; };
20
+ // undefined kind is needed because there is no default mapping for types
21
+ type TypeMapping = ClassTypeMapping | SingletonTypeMapping | ValueTypeMapping | ProviderTypeMapping | { kind: undefined };
22
+
23
+ const enum ClassMappingKind { INSTANCE, VALUE, SINGLETON, PROVIDER}
24
+
25
+ type InstanceClassMapping = { kind: ClassMappingKind.INSTANCE; };
26
+ type ValueClassMapping = { kind: ClassMappingKind.VALUE; value: unknown; };
27
+ type SingletonClassMapping = { kind: ClassMappingKind.SINGLETON; injector: Injector };
28
+ type ProviderClassMapping = { kind: ClassMappingKind.PROVIDER; provider: ProviderFunction; };
29
+ type ClassMapping = InstanceClassMapping | ValueClassMapping | SingletonClassMapping | ProviderClassMapping;
30
+
31
+ // most of the class mappings are done with the default id, store them in the def field so we eliminate a second map lookup
32
+ type MapContainer = { map: Map<ID, ClassMapping>; def?: ClassMapping; };
33
+ type SingletonContainer = { map: Map<ID, Object>; def?: Object; };
34
+ type ClassResolverContainer = { map: Map<ID, ResolverFunction>; def?: ResolverFunction; };
35
+
36
+ export type ResolverContext = {
37
+ [propName: string]: any;
38
+ type: Class
39
+ kind: string;
40
+ }
41
+
42
+ const isInjectDependency = (dep: Dependency): dep is InjectDependency => dep.kind === 'inject';
43
+ const isTypeDependency = (dep: Dependency): dep is TypeDependency => dep.kind === 'type';
44
+
45
+ interface ClassMapper<T extends Class> {
46
+ toValue(value: InstanceType<T>): void;
47
+ toSingleton(): void;
48
+ toProvider(provider: () => InstanceType<T>): void;
49
+ }
50
+
51
+ interface TypeMapper<T> {
52
+ toClass<C extends Class<T>>(classValue: C): void;
53
+ toSingleton<C extends Class<T>>(classValue: C): void;
54
+ toValue(value: T): void;
55
+ toProvider(provider: () => T): void;
56
+ }
57
+
58
+ // why is this not in javascript?! ...
59
+ export const putIfAbsent = <T extends Map<K, V>, K, V>(map: T, key: K, value: () => V) => {
60
+ let v = map.get(key);
61
+ if (!v) {
62
+ v = value();
63
+ map.set(key, v);
64
+ }
65
+ return v;
66
+ };
67
+
68
+ // helper method to create annotation functions
69
+ export const createDependencyAnnotation = (cb: (type: any, index: number, dependantType: Class) => Dependency) => (dependantType: Class, _propertyKey: string | symbol, index: number): any => {
70
+ const metadata = Reflect.getMetadata('design:paramtypes', dependantType);
71
+ const type = metadata[index];
72
+ if (type === dependantType) {
73
+ throw new Error('Could not inject class in itself.');
74
+ }
75
+ const depList = putIfAbsent(Injector.dependencyMap, dependantType, (): Dependency[] => []);
76
+ depList.push(cb(type, index, dependantType));
77
+ };
78
+
79
+ // Default DependencyAnnotations
80
+ export const Inject = createDependencyAnnotation((type, index) => ({kind: 'inject', index, type, id: ''}));
81
+ export const InjectId = (id: ID) => createDependencyAnnotation((type, index) => ({kind: 'inject', index, type, id}));
82
+ export const InjectType = (id: ID) => createDependencyAnnotation((_type, index) => ({kind: 'type', index, id}));
83
+
84
+ type DependencyExtensionResolver = (context: ResolverContext, dependency: Dependency) => ResolverFunction;
85
+
86
+ export class Injector {
87
+ static readonly dependencyMap = new Map<Class, Dependency[]>();
88
+
89
+ protected readonly typeMappings = new Map<ID, TypeMapping>();
90
+ protected readonly classMappings = new Map<Class, MapContainer>();
91
+ protected readonly parameterResolverArrays = new Map<Class, ResolverFunction[]>();
92
+
93
+ protected readonly classResolvers = new Map<Class, ClassResolverContainer>();
94
+ protected readonly typeResolvers = new Map<ID, ResolverFunction>();
95
+
96
+ protected readonly singletons = new Map<Class, SingletonContainer>();
97
+ protected readonly typeSingletons = new Map<ID, Object>();
98
+
99
+ protected readonly dependencyResolverExtensions: Map<string, DependencyExtensionResolver>;
100
+
101
+ constructor(readonly parent?: Injector) {
102
+ this.dependencyResolverExtensions = parent ? parent.dependencyResolverExtensions : new Map<string, DependencyExtensionResolver>();
103
+ this.map(Injector).toValue(this);
104
+ }
105
+
106
+ createChild(): this {
107
+ return new (<typeof Injector>this.constructor)(this) as this;
108
+ }
109
+
110
+ addExtensionResolver(kind: string, resolver: DependencyExtensionResolver) {
111
+ this.dependencyResolverExtensions.set(kind, resolver);
112
+ }
113
+
114
+ map<T extends Class>(type: T, id: ID = ''): ClassMapper<T> {
115
+ const mapper = new InternalClassMapper<T>(this);
116
+ const idMappings = putIfAbsent(this.classMappings, type as Class, (): MapContainer => ({map: new Map<ID, ClassMapping>()}));
117
+ if (id === '') {
118
+ idMappings.def = mapper.mapping;
119
+ } else {
120
+ idMappings.map.set(id, mapper.mapping);
121
+ }
122
+ return mapper;
123
+ }
124
+
125
+ mapType<T>(id: ID): TypeMapper<T> {
126
+ const typeMapper = new InternalTypeMapper<T>(this);
127
+ this.typeMappings.set(id, typeMapper.mapping);
128
+ return typeMapper;
129
+ }
130
+
131
+ getType<T>(id: ID): T {
132
+ const typeResolver = this.getTypeResolver(id);
133
+ if (!typeResolver) {
134
+ throw new Error(`No Mapping for Type with id: '${String(id)}'`);
135
+ }
136
+ return typeResolver() as T;
137
+ }
138
+
139
+ get<T extends Class>(type: T, id: ID = ''): InstanceType<T> {
140
+ const resolver = this.getClassIdResolver(type, id);
141
+ if (!resolver) {
142
+ throw new Error(`No Mapping for Type ${type.name}` + String(id === '' ? '' : ` with id: '${String(id)}'`));
143
+ }
144
+ return resolver() as InstanceType<T>;
145
+ }
146
+
147
+ protected createInstance<T extends Class>(type: T) {
148
+ return this.getCreateInstanceResolver(type)();
149
+ }
150
+
151
+ protected createResolverArray(resolverContext: ResolverContext) {
152
+ const {type} = resolverContext;
153
+ const result: ResolverFunction[] = [];
154
+ const dependencies = Injector.dependencyMap.get(resolverContext.type);
155
+ if (!dependencies) {
156
+ return [];
157
+ }
158
+ for (const dependency of dependencies) {
159
+ let resolver: ResolverFunction;
160
+ if (isInjectDependency(dependency)) {
161
+ if (!dependency.type) throw new Error(`Undefined dependency type for ${type.name}. Check for circular dependency.`);
162
+ const classIdResolver = this.getClassIdResolver(dependency.type, dependency.id);
163
+ if (!classIdResolver) {
164
+ throw new Error(`Could not inject ${dependency.type.name} into ${resolverContext.type.name}`);
165
+ }
166
+ resolver = classIdResolver;
167
+ } else if (isTypeDependency(dependency)) {
168
+ const typeResolver = this.getTypeResolver(dependency.id);
169
+ if (!typeResolver) {
170
+ throw new Error(`Could not inject type with id '${String(dependency.id)}' into ${resolverContext.type.name}`);
171
+ }
172
+ resolver = typeResolver;
173
+ } else {
174
+ const extensionResolver = this.dependencyResolverExtensions.get(dependency.kind);
175
+ if (!extensionResolver) {
176
+ throw new Error(`no dependency resolver for '${dependency.kind}'`);
177
+ }
178
+ resolver = extensionResolver(resolverContext, dependency);
179
+ }
180
+ result[dependency.index] = resolver;
181
+ }
182
+ return result;
183
+ }
184
+
185
+ protected getClassMapping(type: Class, id: ID): ClassMapping | undefined {
186
+ const idMapping = this.classMappings.get(type);
187
+ if (!idMapping) {
188
+ if (!this.parent) return undefined;
189
+ return this.parent.getClassMapping(type, id);
190
+ }
191
+ const mapping = id === '' ? idMapping.def : idMapping.map.get(id);
192
+ if (!mapping) {
193
+ if (!this.parent) return undefined;
194
+ return this.parent.getClassMapping(type, id);
195
+ }
196
+ return mapping;
197
+ }
198
+
199
+ protected getClassIdResolver(dependencyType: Class, id: ID) {
200
+ const getResolver = (): ResolverFunction | undefined => {
201
+ const mapping = this.getClassMapping(dependencyType, id);
202
+ if (!mapping) return undefined;
203
+ switch (mapping.kind) {
204
+ case ClassMappingKind.INSTANCE:
205
+ return this.getCreateInstanceResolver(dependencyType);
206
+ case ClassMappingKind.VALUE:
207
+ // we can cache the value for values
208
+ const instance = mapping.value;
209
+ return () => instance;
210
+ case ClassMappingKind.SINGLETON:
211
+ let singleton: unknown;
212
+ // use the injector defined in the mapping to get the right injector
213
+ const singletonContainer = putIfAbsent(mapping.injector.singletons, dependencyType, (): SingletonContainer => ({map: new Map<ID, Object>()}));
214
+ if (id === '') {
215
+ if (singletonContainer.def) {
216
+ singleton = singletonContainer.def;
217
+ } else {
218
+ singletonContainer.def = mapping.injector.createInstance(dependencyType);
219
+ singleton = singletonContainer.def;
220
+ }
221
+ } else {
222
+ singleton = putIfAbsent(singletonContainer.map, id, () => mapping.injector.createInstance(dependencyType));
223
+ }
224
+ return () => singleton;
225
+ case ClassMappingKind.PROVIDER:
226
+ // we can directly set the provider function as resolver
227
+ return mapping.provider;
228
+ }
229
+ };
230
+
231
+ const container = putIfAbsent(this.classResolvers, dependencyType, (): ClassResolverContainer => ({
232
+ def: undefined,
233
+ map: new Map<ID, ResolverFunction>(),
234
+ }));
235
+
236
+ if (id === '') {
237
+ if (container.def) {
238
+ return container.def;
239
+ }
240
+ const resolver = getResolver();
241
+ container.def = resolver;
242
+ return resolver;
243
+ } else {
244
+ return putIfAbsent(container.map, id, () => getResolver());
245
+ }
246
+ }
247
+
248
+ protected getTypeResolver(id: ID): ResolverFunction | undefined {
249
+ return putIfAbsent(this.typeResolvers, id, () => {
250
+ const mapping = this.getTypeMapping(id);
251
+ if (!mapping) return undefined;
252
+ if (mapping.kind === undefined) {
253
+ // mapping.kind is undefined if there is a type mapping without a target (toClass, toSingleton, toValue)
254
+ throw new Error(`No TypeMapping for id ${String(id)}.`);
255
+ }
256
+ if (mapping.kind === TypeMappingKind.VALUE) {
257
+ const instance = mapping.value;
258
+ return () => instance;
259
+ } else if (mapping.kind === TypeMappingKind.CLASS) {
260
+ return this.getCreateInstanceResolver(mapping.type);
261
+ } else if (mapping.kind === TypeMappingKind.SINGLETON) {
262
+ // use the injector defined in the mapping to get the right injector
263
+ const instance = putIfAbsent(mapping.injector.typeSingletons, id, () => mapping.injector.createInstance(mapping.type));
264
+ return () => instance;
265
+ } else {
266
+ // mapping kind has to be provider
267
+ return mapping.provider;
268
+ }
269
+ });
270
+ }
271
+
272
+ protected getCreateInstanceResolver(type: Class) {
273
+ const resolvers = putIfAbsent(this.parameterResolverArrays, type as Class, () => this.createResolverArray({
274
+ kind: 'class',
275
+ type: type,
276
+ }));
277
+ if (resolvers.length === 0) {
278
+ return () => new type();
279
+ }
280
+ const args = new Array(resolvers.length);
281
+ return () => {
282
+ for (let i = 0; i < args.length; i++) {
283
+ args[i] = resolvers[i]();
284
+ }
285
+ return new type(...args);
286
+ };
287
+ }
288
+
289
+ protected getTypeMapping(id: ID): TypeMapping | undefined {
290
+ const mapping = this.typeMappings.get(id);
291
+ if (!mapping) {
292
+ if (!this.parent) return undefined;
293
+ return this.parent.getTypeMapping(id);
294
+ }
295
+ return mapping;
296
+ }
297
+ }
298
+
299
+ class InternalTypeMapper<T> implements TypeMapper<T> {
300
+ mapping: TypeMapping = {kind: undefined};
301
+
302
+ constructor(private readonly injector: Injector) {
303
+ }
304
+
305
+ toClass<C extends Class<T>>(classValue: C): void {
306
+ Object.assign<TypeMapping, ClassTypeMapping>(this.mapping, {
307
+ kind: TypeMappingKind.CLASS,
308
+ type: classValue,
309
+ });
310
+ }
311
+
312
+ toSingleton<C extends Class<T>>(classValue: C): void {
313
+ Object.assign<TypeMapping, SingletonTypeMapping>(this.mapping, {
314
+ kind: TypeMappingKind.SINGLETON,
315
+ type: classValue,
316
+ injector: this.injector,
317
+ });
318
+ }
319
+
320
+ toValue(value: T): void {
321
+ Object.assign<TypeMapping, ValueTypeMapping>(this.mapping, {
322
+ kind: TypeMappingKind.VALUE,
323
+ value: value,
324
+ });
325
+ }
326
+
327
+ toProvider(provider: () => T): void {
328
+ Object.assign<TypeMapping, ProviderTypeMapping>(this.mapping, {
329
+ kind: TypeMappingKind.PROVIDER,
330
+ provider: provider,
331
+ });
332
+ }
333
+ }
334
+
335
+ class InternalClassMapper<T extends Class> implements ClassMapper<T> {
336
+ // instance is the default class mapping
337
+ mapping: ClassMapping = {kind: ClassMappingKind.INSTANCE};
338
+
339
+ constructor(private readonly injector: Injector) {}
340
+
341
+ toValue(value: InstanceType<T>) {
342
+ Object.assign<ClassMapping, ValueClassMapping>(this.mapping, {
343
+ kind: ClassMappingKind.VALUE,
344
+ value: value,
345
+ });
346
+ return value;
347
+ }
348
+
349
+ toSingleton(): void {
350
+ Object.assign<ClassMapping, SingletonClassMapping>(this.mapping, {
351
+ kind: ClassMappingKind.SINGLETON,
352
+ injector: this.injector,
353
+ });
354
+ this.mapping.kind = ClassMappingKind.SINGLETON;
355
+ }
356
+
357
+ toProvider(provider: () => InstanceType<T>): void {
358
+ Object.assign<ClassMapping, ProviderClassMapping>(this.mapping, {
359
+ kind: ClassMappingKind.PROVIDER,
360
+ provider: provider,
361
+ });
362
+ }
363
+ }
@@ -0,0 +1,320 @@
1
+ import {Class, EcsInjector, Signal, SignalBinding, SignalCallback} from '../index';
2
+ import {signalHandlers} from '../ecsInjector';
3
+ import {ID, putIfAbsent} from '../injector';
4
+
5
+ export type ScopeSignalOptions = { keepAlive?: boolean | Scope[], group?: string }
6
+
7
+ export const scopeSignalHandlers = new Map<Object, [string, ID, ScopeSignalOptions?][]>();
8
+
9
+ export const OnScopeSignal = (id: ID, options?: ScopeSignalOptions) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
10
+ if (target instanceof Function) {
11
+ throw new Error('only allowed on non static methods');
12
+ } else {
13
+ const mappingList = putIfAbsent(scopeSignalHandlers, target.constructor, (): [string, ID, ScopeSignalOptions?][] => []);
14
+ mappingList.push([propertyKey, id, options]);
15
+ }
16
+ };
17
+
18
+ export const SCOPE_CONTEXT = {
19
+ log: true,
20
+ };
21
+
22
+ export interface Scope {
23
+ [propName: string]: any; // disable weak type detection
24
+ onEnter?(): void;
25
+ onExit?(): void;
26
+ onSubReturn?(): void;
27
+ onSubExit?(): void;
28
+ onActivate?(): void;
29
+ onDeactivate?(): void;
30
+ }
31
+
32
+ class ScopeSignalBinding {
33
+ readonly keepAlive?: boolean | Scope[];
34
+ private group?: string;
35
+
36
+ constructor(private signalBinding: SignalBinding, options?: ScopeSignalOptions) {
37
+ this.keepAlive = options?.keepAlive;
38
+ this.group = options?.group;
39
+ }
40
+
41
+ activate() {
42
+ this.signalBinding.setActive(true);
43
+ }
44
+
45
+ deactivate() {
46
+ this.signalBinding.setActive(false);
47
+ }
48
+
49
+ detach() {
50
+ this.signalBinding.detach();
51
+ }
52
+ }
53
+
54
+ class ScopeStack {
55
+ readonly stack: ScopeContext[] = [];
56
+ queuedScopeChanges: Function[] = [];
57
+ ongoingChange = false;
58
+ target?: Scope;
59
+
60
+ constructor(readonly rootScope: ScopeContext) {
61
+ this.stack.push(rootScope);
62
+ }
63
+
64
+ get rootContext() {
65
+ return this.stack[0];
66
+ }
67
+
68
+ get activeContext() {
69
+ return this.stack[this.stack.length - 1];
70
+ }
71
+
72
+ }
73
+
74
+ export type ScopeMapping = (params: {
75
+ injector: EcsInjector,
76
+ registerScopeService: (serviceClass: Class) => void,
77
+ }) => void
78
+
79
+ interface AddScopeSignalOptions extends ScopeSignalOptions {
80
+ context?: unknown;
81
+ }
82
+
83
+ // TODO: maybe move Signal resolver to Injector and dont use EcsInjector here?
84
+ export class ScopeContext {
85
+ readonly scope: Scope;
86
+ readonly injector: EcsInjector;
87
+ private readonly stack: ScopeStack; // TODO: naming
88
+ private readonly signalBindings: ScopeSignalBinding[] = [];
89
+
90
+ // Scope services will be active (signals bounds) in this scope and all sub scopes
91
+ // when this scope exits, the signals will be unbound
92
+ private serviceBindings: SignalBinding[] = [];
93
+ private closed = false;
94
+ private muteKeepAliveSignals = false;
95
+
96
+ constructor(injector: EcsInjector, scopeClass: Class, mapping?: ScopeMapping, private parent?: ScopeContext) {
97
+ if (!parent) {
98
+ this.stack = new ScopeStack(this);
99
+
100
+ } else {
101
+ this.stack = parent.stack;
102
+ this.stack.stack.push(this);
103
+ }
104
+
105
+ this.injector = injector.createChild();
106
+ //TODO: i think it would really be better if we implement an injector.injectIntoUnmapped() instead of mapping here
107
+ this.injector.map(scopeClass).toSingleton();
108
+ this.injector.map(ScopeContext).toValue(this);
109
+
110
+ mapping?.({
111
+ injector: this.injector,
112
+ registerScopeService: serviceClass => {
113
+ this.injector.map(serviceClass).toSingleton();
114
+ this.mapServiceSignals(this.injector.get(serviceClass));
115
+ },
116
+ });
117
+
118
+ this.scope = this.injector.get(scopeClass) as Scope;
119
+ this.mapScopeSignals();
120
+ this.updateSignalBindings();
121
+ // TODO: maybe the scope needs to prepare resources or something before it can react to signals, so we should move onEnter bevore the signal mapping and make it async (not working in constructor though)
122
+ this.scope.onEnter?.();
123
+ this.scope.onActivate?.();
124
+ }
125
+
126
+ get isRoot() {
127
+ return this === this.stack.rootScope;
128
+ }
129
+
130
+ // TODO: test this
131
+ get target() {
132
+ return this.stack.target;
133
+ }
134
+
135
+ get isActive() {
136
+ return this === this.stack.activeContext;
137
+ }
138
+
139
+ get activeContext() { return this.stack.activeContext;}
140
+
141
+ /**
142
+ * returns the new scope only if there is currently no ongoing scope change happening
143
+ * @param scopeClass
144
+ * @param mapping
145
+ */
146
+ enterScope(scopeClass: Class, mapping?: ScopeMapping): ScopeContext | undefined {
147
+ let newContext;
148
+ const doChange = () => {
149
+
150
+ this.activeContext.scope.onSubExit?.();
151
+ this.activeContext.scope.onDeactivate?.();
152
+
153
+ newContext = new ScopeContext(this.activeContext.injector, scopeClass, mapping, this.activeContext);
154
+
155
+ this.logStack();
156
+ const nextChange = this.stack.queuedScopeChanges.shift();
157
+ if (nextChange) {
158
+ nextChange();
159
+ } else {
160
+ this.stack.ongoingChange = false;
161
+ }
162
+ };
163
+ // TODO: this queued changes arent somewhat experimental
164
+ if (!this.stack.ongoingChange) {
165
+ this.stack.ongoingChange = true;
166
+ doChange();
167
+ } else {
168
+ this.stack.queuedScopeChanges.push(doChange);
169
+ }
170
+ return newContext;
171
+ }
172
+
173
+ /**
174
+ * Exits this scope (and all open subscopes=), if a scope class is given. all scopes from the stack are closed until after the scope with the given class
175
+ */
176
+ exitScope(target?: Scope) {
177
+ const doChange = () => {
178
+ if (this.closed) throw new Error(`Scope already closed`);
179
+ // TODO: check if target is in stack?
180
+ this.stack.target = target || this.scope.constructor;
181
+ while (true) {
182
+ const ctx = this.stack.stack.pop();
183
+ if (!ctx) {
184
+ throw new Error('no scope in stack');
185
+ }
186
+ ctx!.exitThis();
187
+ if (ctx!.scope.constructor === this.stack.target) {
188
+ break;
189
+ }
190
+ }
191
+ this.stack.target = undefined;
192
+ const nextChange = this.stack.queuedScopeChanges.shift();
193
+ if (nextChange) {
194
+ nextChange();
195
+ } else {
196
+ this.stack.ongoingChange = false;
197
+ }
198
+
199
+ };
200
+ if (!this.stack.ongoingChange) {
201
+ this.stack.ongoingChange = true;
202
+ doChange();
203
+ } else {
204
+ this.stack.queuedScopeChanges.push(doChange);
205
+ }
206
+ }
207
+
208
+ closeSubScopes() {
209
+ this.stack.target = this.scope.constructor;
210
+ while (!this.isActive) {
211
+ const ctx = this.stack.stack.pop();
212
+ if (!ctx) {
213
+ throw new Error('no scope in stack');
214
+ }
215
+ ctx.exitThis();
216
+ }
217
+ this.stack.target = undefined;
218
+ }
219
+
220
+ private logStack() {
221
+ if (SCOPE_CONTEXT.log) {
222
+ console.debug('%c' + this.stack.stack.map(c => c.scope.constructor.name).join(' -> '), 'color:yellow');
223
+ }
224
+ }
225
+
226
+ getStackClasses(): Scope[] {
227
+ return this.stack.stack.map(context => context.scope.constructor);
228
+ }
229
+
230
+ addScopeSignal<T>(signal: Signal<T>, callback: SignalCallback<T>, params?: AddScopeSignalOptions) {
231
+ const signalBinding = new ScopeSignalBinding(signal.add(callback, params?.context), params);
232
+ this.signalBindings.push(signalBinding);
233
+ }
234
+
235
+ disableKeepAliveSignals() {
236
+ this.muteKeepAliveSignals = true;
237
+ this.updateSignalBindings();
238
+ }
239
+
240
+ enableKeepAliveSignals() {
241
+ this.muteKeepAliveSignals = false;
242
+ this.updateSignalBindings();
243
+ }
244
+
245
+ private updateSignalBindings(muteKeepAlive = false) {
246
+ for (const binding of this.signalBindings) {
247
+ if (this.isActive) {
248
+ binding.activate();
249
+ } else if (muteKeepAlive) {
250
+ binding.deactivate();
251
+ } else if (binding.keepAlive === true) {
252
+ binding.activate();
253
+ } else if (!!(binding.keepAlive)) {
254
+ // is array of scopes
255
+ // OPT: cache active context?
256
+ if (binding.keepAlive.indexOf(this.activeContext.scope.constructor) != -1) {
257
+ binding.activate();
258
+ } else {
259
+ binding.deactivate();
260
+ }
261
+ } else {
262
+ binding.deactivate();
263
+ }
264
+ }
265
+ muteKeepAlive = muteKeepAlive || this.muteKeepAliveSignals;
266
+ this.parent?.updateSignalBindings(muteKeepAlive);
267
+ }
268
+
269
+ private detachSignalBindings() {
270
+ for (const signalBinding of this.signalBindings) {
271
+ signalBinding.detach();
272
+ }
273
+ }
274
+
275
+ private mapScopeSignals() {
276
+ const handlers = scopeSignalHandlers.get(this.scope.constructor);
277
+ if (!handlers) return;
278
+
279
+ for (const [field, id, options] of handlers) {
280
+ const signal = this.injector.getSignal(id);
281
+ const callback = (<any>this.scope) [field];
282
+ this.addScopeSignal(signal, callback, {...options, context: this.scope});
283
+ }
284
+ }
285
+
286
+ private mapServiceSignals(scopeService: Object) {
287
+ const handlers = signalHandlers.get(scopeService.constructor);
288
+ if (!handlers) return;
289
+
290
+ for (const [field, id] of handlers) {
291
+ const signal = this.injector.getSignal(id);
292
+ const signalBinding = signal.add((<any>scopeService) [field], scopeService);
293
+ this.serviceBindings.push(signalBinding);
294
+ }
295
+ }
296
+
297
+ private detachServiceBindings() {
298
+ for (const binding of this.serviceBindings) {
299
+ binding.detach();
300
+ }
301
+ this.serviceBindings = [];
302
+ }
303
+
304
+ private exitThis() {
305
+
306
+ // TODO: BUG: when there is a scope change within another scope change there is some confusion about the call order
307
+ if (!this.parent) {
308
+ throw new Error('can\'t exit root scope?!');
309
+ }
310
+ this.logStack();
311
+ this.detachSignalBindings();
312
+ this.detachServiceBindings();
313
+ this.scope.onDeactivate?.();
314
+ this.scope.onExit?.();
315
+ this.parent.updateSignalBindings();
316
+ this.parent.scope.onSubReturn?.();
317
+ this.parent.scope.onActivate?.();
318
+ this.closed = true;
319
+ }
320
+ }