-
Notifications
You must be signed in to change notification settings - Fork 104
Expand file tree
/
Copy pathEnvironment.ts
More file actions
395 lines (346 loc) · 13.8 KB
/
Environment.ts
File metadata and controls
395 lines (346 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/**
* @license
* Copyright 2022-2026 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/
import { ServiceProvider } from "#environment/ServiceProvider.js";
import { SharedEnvironmentServices } from "#environment/SharedEnvironmentServices.js";
import { SharedServicesManager } from "#environment/SharedServicesManager.js";
import { Diagnostic } from "#log/Diagnostic.js";
import { LogFormat } from "#log/LogFormat.js";
import { InternalError } from "#MatterError.js";
import { Instant } from "#time/TimeUnit.js";
import { Lifetime } from "#util/Lifetime.js";
import { MaybePromise } from "#util/Promises.js";
import { DiagnosticSource } from "../log/DiagnosticSource.js";
import { Logger } from "../log/Logger.js";
import "../polyfills/disposable.js";
import { Time } from "../time/Time.js";
import { Destructable, UnsupportedDependencyError } from "../util/Lifecycle.js";
import { Observable } from "../util/Observable.js";
import { Environmental } from "./Environmental.js";
import { RuntimeService } from "./RuntimeService.js";
import { VariableService } from "./VariableService.js";
const logger = Logger.get("Environment");
/**
* Access to general platform-dependent features.
*
* The following variables are defined by this class:
* * `log.level` - Log level to use {@link Logger.LEVEL}
* * `log.format` - Log format to use {@link Logger.FORMAT}
* * `log.stack.limit` - Stack trace limit, see https://nodejs.org/api/errors.html#errorstacktracelimit
* * `mdns.networkInterface` - Network interface to use for MDNS broadcasts and scanning, default are all available interfaces
* * `mdns.ipv4` - Also announce/scan on IPv4 interfaces
* * `network.interface` - Map of interface names to types, expected to be defined as object with name as key and of `{type: string|number}` objects with types: 1=Wifi, 2=Ethernet, 3=Cellular, 4=Thread (strings or numbers can be used). Can also be provided via env or cli like `MATTER_NETWORK_INTERFACE_ETH0_TYPE=Ethernet`
*
* The environment supports reference-counted service sharing through shared service instances. Create
* a shared instance using {@link asDependent} to access services with automatic lifecycle tracking at
* the root environment. Shared services enable safe sharing across multiple consumers - services are
* protected from premature closure and only cleaned up when all consumers have released them.
*
* Services can be accessed directly via {@link get} for immediate use, or through {@link SharedEnvironmentServices}
* for tracked access at the root level. Direct access means services close immediately when requested.
* Shared access means services are registered at root and only close when all consumers have released them.
*
* When services are deleted or closed without shared tracking, the service is blocked locally and does
* not inherit from parent environments. When accessed through shared instances, the service is managed
* at the root environment and protected as long as any consumer is using it.
*
* TODO - could remove global singletons by moving here
*/
export class Environment implements ServiceProvider, Lifetime.Owner {
#services?: Map<Environmental.ServiceType, Environmental.Service | null>;
#name: string;
#parent?: Environment;
#lifetime: Lifetime;
#added = Observable<[type: Environmental.ServiceType, instance: {}]>();
#deleted = Observable<[type: Environmental.ServiceType, instance: {}]>();
#serviceEvents = new Map<Environmental.ServiceType, Environmental.ServiceEvents<any>>();
constructor(name: string, parent?: Environment) {
this.#name = name;
this.#parent = parent;
this.#lifetime = (parent ?? Lifetime.process).join(Diagnostic.strong(name), "environment");
}
join(...name: unknown[]) {
return this.#lifetime?.join(...name);
}
/**
* Create a shared service instance for reference-counted service access at root.
*
* Shared instances track which services they use and enable safe sharing across multiple
* consumers. Services are automatically protected from premature closure and only
* cleaned up when all consumers have released them, e.g. by calling close().
*
* All shared instances operate at the root environment for centralized lifecycle
* management, regardless of which environment in the hierarchy creates them.
*
* @returns A new shared service instance for accessing services with lifecycle tracking
*/
asDependent(): SharedEnvironmentServices {
const dependent = new SharedEnvironmentServices(this.root);
this.get(SharedServicesManager).add(dependent);
return dependent;
}
/**
* Determine if an environmental service is available.
*/
has(type: Environmental.ServiceType): boolean {
const instance = this.#services?.get(type);
if (instance === null) {
return false;
}
return instance !== undefined || (this.#parent?.has(type) ?? false);
}
/**
* Determine if an environmental services is owned by this environment (not an ancestor).
*/
owns(type: Environmental.ServiceType): boolean {
return !!this.#services?.get(type);
}
/**
* Access an environmental service.
*/
get<T extends object>(type: Environmental.ServiceType<T>): T {
const mine = this.#services?.get(type);
if (mine !== undefined && mine !== null) {
return mine as T;
}
// When null then we do not have it and also do not want to inherit from parent
if (mine === undefined) {
const instance = this.#parent?.maybeGet(type);
if (instance !== undefined && instance !== null) {
// Parent has it, use it
return instance;
}
}
// ... otherwise try to create it. The create method must install it in the environment if needed
if ((type as Environmental.Factory<T>)[Environmental.create]) {
const instance = (type as any)[Environmental.create](this) as T;
if (!(instance instanceof type)) {
throw new InternalError(`Service creation did not produce instance of ${type.name}`);
}
return instance;
}
throw new UnsupportedDependencyError(`Required dependency ${type.name}`, "is not available");
}
/**
* Access an environmental service that may not exist.
*/
maybeGet<T extends object>(type: Environmental.ServiceType<T>): T | undefined {
if (this.has(type)) {
return this.get(type);
}
}
/**
* Remove an environmental service and block further inheritance.
*
* If any consumer is currently using the service, deletion is deferred until all
* consumers have released it. Services accessed via {@link SharedEnvironmentServices}
* are protected from premature removal.
*/
delete(type: Environmental.ServiceType, instance?: any) {
const localInstance = this.#services?.get(type);
if (this.get(SharedServicesManager).has(type)) {
return;
}
// Remove instance and replace by null to prevent inheritance from parent
this.#services?.set(type, null);
if (localInstance === undefined || localInstance === null) {
return;
}
if (instance !== undefined && localInstance !== instance) {
return;
}
this.#deleted.emit(type, localInstance);
const serviceEvents = this.#serviceEvents.get(type);
if (serviceEvents) {
serviceEvents.deleted.emit(localInstance);
}
}
/**
* Remove and close an environmental service.
*
* Calls the service's close method if present, then removes it from the environment.
* If any consumer is currently using the service, closure is deferred until all
* consumers have released it.
*/
close<T extends object>(
type: Environmental.ServiceType<T>,
): T extends { close: () => MaybePromise<void> } ? MaybePromise<void> : void {
const instance = this.maybeGet(type);
this.delete(type, instance);
if (instance !== undefined) {
if (this.get(SharedServicesManager).has(type)) {
// still in use
return;
}
return (instance as Partial<Destructable>).close?.() as T extends { close: () => MaybePromise<void> }
? MaybePromise<void>
: void;
}
}
/**
* Access an environmental service, waiting for any async initialization to complete.
*/
async load<T extends Environmental.Service>(type: Environmental.Factory<T>): Promise<T> {
const instance = this.get(type);
await instance.construction;
return instance;
}
/**
* Install a preinitialized version of an environmental service.
*/
set<T extends {}>(type: Environmental.ServiceType<T>, instance: T) {
if (!this.#services) {
this.#services = new Map();
}
// Services installed via set() don't have a participant mode yet - it will be determined on first access
this.#services.set(type, instance as Environmental.Service);
this.#added.emit(type, instance);
const serviceEvents = this.#serviceEvents.get(type);
if (serviceEvents) {
serviceEvents.added.emit(instance);
}
}
/**
* Name of the environment.
*/
get name() {
return this.#name;
}
/**
* Get the root environment in the hierarchy.
*
* Recursively traverses parent links to find the topmost environment with no parent.
* Used internally to ensure certain services (like DependentsManager) are installed
* at a single, consistent location for the entire environment tree.
*/
get root(): Environment {
return this.#parent?.root ?? this;
}
/**
* Emits on service add.
*
* Currently only emits for services owned directly by this environment.
*/
get added() {
return this.#added;
}
/**
* Emits on service delete.
*
* Currently only emits for services owned directly by this environment.
*/
get deleted() {
return this.#deleted;
}
/**
* Obtain an object with events that trigger when a specific service is added or deleted.
*
* This is a more convenient way to observe a specific service than {@link added} and {@link deleted}.
*/
eventsFor<T extends Environmental.ServiceType>(type: T) {
let events = this.#serviceEvents.get(type);
if (events === undefined) {
events = {
added: Observable(),
deleted: Observable(),
};
this.#serviceEvents.set(type, events);
}
return events as Environmental.ServiceEvents<T>;
}
/**
* Apply functions to a specific service type automatically.
*
* The environment invokes {@link added} immediately if the service is currently present. It then invokes
* {@link added} in the future if the service is added or replaced and/or {@link deleted} if the service is replaced
* or deleted.
*/
applyTo<T extends object>(
type: Environmental.ServiceType<T>,
added?: (env: Environment, service: T) => MaybePromise<void>,
deleted?: (env: Environment, service: T) => MaybePromise<void>,
) {
const events = this.eventsFor(type);
if (added) {
const existing = this.maybeGet(type);
if (existing) {
added(this, existing);
}
events.added.on(service =>
this.runtime.add(async () => {
using _adding = this.join(`adding ${type.name}`);
await added(this, service);
}),
);
}
if (deleted) {
events.deleted.on(service =>
this.runtime.add(async () => {
using _deleting = this.join(`adding ${type.name}`);
await deleted(this, service);
}),
);
}
}
/**
* The default environment.
*
* Currently only emits for services owned directly by this environment.
*/
static get default() {
return global;
}
/**
* Set the default environment.
*/
static set default(env: Environment) {
global[Symbol.dispose]();
global = env;
env.vars.use(() => {
Logger.level = env.vars.get("log.level", Logger.level);
Logger.format = env.vars.get("log.format", Logger.format);
const stackLimit = global.vars.number("log.stack.limit");
if (stackLimit !== undefined) {
(Error as { stackTraceLimit?: number }).stackTraceLimit = stackLimit;
}
});
}
/**
* Shortcut for accessing {@link VariableService.vars}.
*/
get vars() {
return this.get(VariableService);
}
/**
* Shortcut for accessing {@link RuntimeService}.
*/
get runtime() {
return this.get(RuntimeService);
}
/**
* Display tasks that supply diagnostics.
*/
diagnose() {
Time.getTimer("diagnostics", Instant, () => {
try {
logger.notice("Diagnostics follow", DiagnosticSource);
} catch (e) {
logger.error(`Unhandled error gathering diagnostics:`, e);
}
}).start();
}
protected loadVariables(): Record<string, any> {
return {};
}
[Symbol.dispose]() {
// Currently this is just a method for terminating our lifetime
this.#lifetime[Symbol.dispose]();
}
}
let global = new Environment("default");
if (typeof MatterHooks !== "undefined") {
MatterHooks.generateDiagnostics = () =>
(LogFormat.formats[Logger.format] ?? LogFormat.formats.ansi)(DiagnosticSource);
}