@pooder/core 2.0.0 → 2.2.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/src/index.ts CHANGED
@@ -1,145 +1,338 @@
1
- import { Service, ServiceRegistry } from "./service";
2
- import EventBus from "./event";
3
- import { ExtensionManager } from "./extension";
4
- import Disposable from "./disposable";
5
- import {
6
- Contribution,
7
- ContributionPoint,
8
- ContributionPointIds,
9
- ContributionRegistry,
10
- } from "./contribution";
11
- import { CommandService, ConfigurationService, WorkbenchService } from "./services";
12
- import { ExtensionContext } from "./context";
13
-
14
- export * from "./extension";
15
- export * from "./context";
16
- export * from "./contribution";
17
- export * from "./service";
18
- export * from "./services";
19
- export { default as EventBus } from "./event";
20
-
21
- export class Pooder {
22
- readonly eventBus: EventBus = new EventBus();
23
- private readonly services: ServiceRegistry = new ServiceRegistry();
24
- private readonly contributions: ContributionRegistry =
25
- new ContributionRegistry();
26
- readonly extensionManager: ExtensionManager;
27
-
28
- constructor() {
29
- // Initialize default contribution points
30
- this.initDefaultContributionPoints();
31
-
32
- const commandService = new CommandService();
33
- this.registerService(commandService, "CommandService");
34
-
35
- const configurationService = new ConfigurationService();
36
- this.registerService(configurationService, "ConfigurationService");
37
-
38
- const workbenchService = new WorkbenchService();
39
- workbenchService.setEventBus(this.eventBus);
40
- this.registerService(workbenchService, "WorkbenchService");
41
-
42
- // Create a restricted context for extensions
43
- const context: ExtensionContext = {
44
- eventBus: this.eventBus,
45
- services: {
46
- get: <T extends Service>(serviceName: string) =>
47
- this.services.get<T>(serviceName),
48
- },
49
- contributions: {
50
- get: <T>(pointId: string) => this.getContributions<T>(pointId),
51
- register: <T>(pointId: string, contribution: Contribution<T>) =>
52
- this.registerContribution(pointId, contribution),
53
- },
54
- };
55
-
56
- this.extensionManager = new ExtensionManager(context);
57
- }
58
-
59
- private initDefaultContributionPoints() {
60
- this.registerContributionPoint({
61
- id: ContributionPointIds.CONTRIBUTIONS,
62
- description: "Contribution point for contribution points",
63
- });
64
-
65
- this.registerContributionPoint({
66
- id: ContributionPointIds.COMMANDS,
67
- description: "Contribution point for commands",
68
- });
69
-
70
- this.registerContributionPoint({
71
- id: ContributionPointIds.TOOLS,
72
- description: "Contribution point for tools",
73
- });
74
-
75
- this.registerContributionPoint({
76
- id: ContributionPointIds.VIEWS,
77
- description: "Contribution point for UI views",
78
- });
79
-
80
- this.registerContributionPoint({
81
- id: ContributionPointIds.CONFIGURATIONS,
82
- description: "Contribution point for configurations",
83
- });
84
- }
85
-
86
- // --- Service Management ---
87
-
88
- registerService(service: Service, id?: string) {
89
- const serviceId = id || service.constructor.name;
90
-
91
- try {
92
- service?.init?.();
93
- } catch (e) {
94
- console.error(`Error initializing service ${serviceId}:`, e);
95
- return false;
96
- }
97
-
98
- this.services.register(serviceId, service);
99
- this.eventBus.emit("service:register", service);
100
- return true;
101
- }
102
-
103
- unregisterService(service: Service, id?: string) {
104
- const serviceId = id || service.constructor.name;
105
- if (!this.services.has(serviceId)) {
106
- console.warn(`Service ${serviceId} is not registered.`);
107
- return true;
108
- }
109
-
110
- try {
111
- service?.dispose?.();
112
- } catch (e) {
113
- console.error(`Error disposing service ${serviceId}:`, e);
114
- return false;
115
- }
116
-
117
- this.services.delete(serviceId);
118
- this.eventBus.emit("service:unregister", service);
119
- return true;
120
- }
121
-
122
- getService<T extends Service>(id: string): T | undefined {
123
- return this.services.get<T>(id);
124
- }
125
-
126
- // --- Contribution Management ---
127
-
128
- registerContributionPoint<T>(point: ContributionPoint<T>): void {
129
- this.contributions.registerPoint(point);
130
- this.eventBus.emit("contribution:point:register", point);
131
- }
132
-
133
- registerContribution<T>(
134
- pointId: string,
135
- contribution: Contribution<T>,
136
- ): Disposable {
137
- const disposable = this.contributions.register(pointId, contribution);
138
- this.eventBus.emit("contribution:register", { ...contribution, pointId });
139
- return disposable;
140
- }
141
-
142
- getContributions<T>(pointId: string): Contribution<T>[] {
143
- return this.contributions.get<T>(pointId);
144
- }
145
- }
1
+ import {
2
+ isServiceToken,
3
+ RegisterServiceOptions,
4
+ Service,
5
+ ServiceContext,
6
+ ServiceIdentifier,
7
+ ServiceRegistry,
8
+ } from "./service";
9
+ import EventBus from "./event";
10
+ import { ExtensionManager } from "./extension";
11
+ import Disposable from "./disposable";
12
+ import {
13
+ Contribution,
14
+ ContributionPoint,
15
+ ContributionPointIds,
16
+ ContributionRegistry,
17
+ } from "./contribution";
18
+ import {
19
+ CORE_SERVICE_TOKENS,
20
+ CommandService,
21
+ ConfigurationService,
22
+ ToolRegistryService,
23
+ ToolSessionService,
24
+ WorkbenchService,
25
+ } from "./services";
26
+ import { ExtensionContext } from "./context";
27
+
28
+ export * from "./extension";
29
+ export * from "./context";
30
+ export * from "./contribution";
31
+ export * from "./service";
32
+ export * from "./services";
33
+ export { default as EventBus } from "./event";
34
+
35
+ export class Pooder {
36
+ readonly eventBus: EventBus = new EventBus();
37
+ private readonly services: ServiceRegistry = new ServiceRegistry();
38
+ private readonly serviceContext: ServiceContext = {
39
+ eventBus: this.eventBus,
40
+ get: <T extends Service>(identifier: ServiceIdentifier<T>) =>
41
+ this.services.get(identifier),
42
+ getOrThrow: <T extends Service>(
43
+ identifier: ServiceIdentifier<T>,
44
+ errorMessage?: string,
45
+ ) => this.services.getOrThrow(identifier, errorMessage),
46
+ has: (identifier: ServiceIdentifier<Service>) => this.services.has(identifier),
47
+ };
48
+ private readonly contributions: ContributionRegistry =
49
+ new ContributionRegistry();
50
+ readonly extensionManager: ExtensionManager;
51
+
52
+ constructor() {
53
+ // Initialize default contribution points
54
+ this.initDefaultContributionPoints();
55
+
56
+ const commandService = new CommandService();
57
+ this.registerService(commandService, CORE_SERVICE_TOKENS.COMMAND);
58
+
59
+ const configurationService = new ConfigurationService();
60
+ this.registerService(configurationService, CORE_SERVICE_TOKENS.CONFIGURATION);
61
+
62
+ const toolRegistryService = new ToolRegistryService();
63
+ this.registerService(toolRegistryService, CORE_SERVICE_TOKENS.TOOL_REGISTRY);
64
+
65
+ const toolSessionService = new ToolSessionService({
66
+ commandService,
67
+ toolRegistry: toolRegistryService,
68
+ });
69
+ this.registerService(toolSessionService, CORE_SERVICE_TOKENS.TOOL_SESSION);
70
+
71
+ const workbenchService = new WorkbenchService({
72
+ eventBus: this.eventBus,
73
+ toolRegistry: toolRegistryService,
74
+ sessionService: toolSessionService,
75
+ });
76
+ this.registerService(workbenchService, CORE_SERVICE_TOKENS.WORKBENCH);
77
+
78
+ // Create a restricted context for extensions
79
+ const context: ExtensionContext = {
80
+ eventBus: this.eventBus,
81
+ services: {
82
+ get: <T extends Service>(identifier: ServiceIdentifier<T>) =>
83
+ this.services.get(identifier),
84
+ getOrThrow: <T extends Service>(
85
+ identifier: ServiceIdentifier<T>,
86
+ errorMessage?: string,
87
+ ) => this.services.getOrThrow(identifier, errorMessage),
88
+ has: (identifier: ServiceIdentifier<Service>) =>
89
+ this.services.has(identifier),
90
+ },
91
+ contributions: {
92
+ get: <T>(pointId: string) => this.getContributions<T>(pointId),
93
+ register: <T>(pointId: string, contribution: Contribution<T>) =>
94
+ this.registerContribution(pointId, contribution),
95
+ },
96
+ };
97
+
98
+ this.extensionManager = new ExtensionManager(context);
99
+ }
100
+
101
+ private initDefaultContributionPoints() {
102
+ this.registerContributionPoint({
103
+ id: ContributionPointIds.CONTRIBUTIONS,
104
+ description: "Contribution point for contribution points",
105
+ });
106
+
107
+ this.registerContributionPoint({
108
+ id: ContributionPointIds.COMMANDS,
109
+ description: "Contribution point for commands",
110
+ });
111
+
112
+ this.registerContributionPoint({
113
+ id: ContributionPointIds.TOOLS,
114
+ description: "Contribution point for tools",
115
+ });
116
+
117
+ this.registerContributionPoint({
118
+ id: ContributionPointIds.VIEWS,
119
+ description: "Contribution point for UI views",
120
+ });
121
+
122
+ this.registerContributionPoint({
123
+ id: ContributionPointIds.CONFIGURATIONS,
124
+ description: "Contribution point for configurations",
125
+ });
126
+ }
127
+
128
+ // --- Service Management ---
129
+
130
+ registerService<T extends Service>(
131
+ service: T,
132
+ identifier?: ServiceIdentifier<T>,
133
+ options: RegisterServiceOptions = {},
134
+ ): boolean {
135
+ const serviceIdentifier = this.resolveServiceIdentifier(service, identifier);
136
+ const serviceId = this.getServiceLabel(serviceIdentifier);
137
+
138
+ try {
139
+ const initResult = this.invokeServiceHook(service, "init");
140
+ if (this.isPromiseLike(initResult)) {
141
+ throw new Error(
142
+ `Service "${serviceId}" init() is async. Use registerServiceAsync() instead.`,
143
+ );
144
+ }
145
+
146
+ this.services.register(serviceIdentifier, service, options);
147
+ this.eventBus.emit("service:register", service, { id: serviceId });
148
+ return true;
149
+ } catch (error) {
150
+ console.error(`Error initializing service ${serviceId}:`, error);
151
+ return false;
152
+ }
153
+ }
154
+
155
+ async registerServiceAsync<T extends Service>(
156
+ service: T,
157
+ identifier?: ServiceIdentifier<T>,
158
+ options: RegisterServiceOptions = {},
159
+ ): Promise<boolean> {
160
+ const serviceIdentifier = this.resolveServiceIdentifier(service, identifier);
161
+ const serviceId = this.getServiceLabel(serviceIdentifier);
162
+
163
+ try {
164
+ await this.invokeServiceHookAsync(service, "init");
165
+ this.services.register(serviceIdentifier, service, options);
166
+ this.eventBus.emit("service:register", service, { id: serviceId });
167
+ return true;
168
+ } catch (error) {
169
+ console.error(`Error initializing service ${serviceId}:`, error);
170
+ return false;
171
+ }
172
+ }
173
+
174
+ unregisterService(service: Service, id?: ServiceIdentifier<Service>): boolean;
175
+ unregisterService(identifier: ServiceIdentifier<Service>): boolean;
176
+ unregisterService(
177
+ serviceOrIdentifier: Service | ServiceIdentifier<Service>,
178
+ id?: ServiceIdentifier<Service>,
179
+ ): boolean {
180
+ const resolvedIdentifier = this.resolveUnregisterIdentifier(
181
+ serviceOrIdentifier,
182
+ id,
183
+ );
184
+ const serviceId = this.getServiceLabel(resolvedIdentifier);
185
+ const registeredService = this.services.get(resolvedIdentifier);
186
+
187
+ if (!registeredService) {
188
+ console.warn(`Service ${serviceId} is not registered.`);
189
+ return true;
190
+ }
191
+
192
+ try {
193
+ const disposeResult = this.invokeServiceHook(registeredService, "dispose");
194
+ if (this.isPromiseLike(disposeResult)) {
195
+ throw new Error(
196
+ `Service "${serviceId}" dispose() is async. Use unregisterServiceAsync() instead.`,
197
+ );
198
+ }
199
+ } catch (error) {
200
+ console.error(`Error disposing service ${serviceId}:`, error);
201
+ return false;
202
+ }
203
+
204
+ this.services.delete(resolvedIdentifier);
205
+ this.eventBus.emit("service:unregister", registeredService, { id: serviceId });
206
+ return true;
207
+ }
208
+
209
+ async unregisterServiceAsync(
210
+ serviceOrIdentifier: Service | ServiceIdentifier<Service>,
211
+ id?: ServiceIdentifier<Service>,
212
+ ): Promise<boolean> {
213
+ const resolvedIdentifier = this.resolveUnregisterIdentifier(
214
+ serviceOrIdentifier,
215
+ id,
216
+ );
217
+ const serviceId = this.getServiceLabel(resolvedIdentifier);
218
+ const registeredService = this.services.get(resolvedIdentifier);
219
+
220
+ if (!registeredService) {
221
+ console.warn(`Service ${serviceId} is not registered.`);
222
+ return true;
223
+ }
224
+
225
+ try {
226
+ await this.invokeServiceHookAsync(registeredService, "dispose");
227
+ } catch (error) {
228
+ console.error(`Error disposing service ${serviceId}:`, error);
229
+ return false;
230
+ }
231
+
232
+ this.services.delete(resolvedIdentifier);
233
+ this.eventBus.emit("service:unregister", registeredService, { id: serviceId });
234
+ return true;
235
+ }
236
+
237
+ getService<T extends Service>(identifier: ServiceIdentifier<T>): T | undefined {
238
+ return this.services.get<T>(identifier);
239
+ }
240
+
241
+ getServiceOrThrow<T extends Service>(
242
+ identifier: ServiceIdentifier<T>,
243
+ errorMessage?: string,
244
+ ): T {
245
+ return this.services.getOrThrow(identifier, errorMessage);
246
+ }
247
+
248
+ hasService(identifier: ServiceIdentifier<Service>): boolean {
249
+ return this.services.has(identifier);
250
+ }
251
+
252
+ async dispose(): Promise<void> {
253
+ this.extensionManager.destroy();
254
+
255
+ const registrations = this.services.list().slice().reverse();
256
+ for (const item of registrations) {
257
+ const identifier = item.token ?? item.id;
258
+ await this.unregisterServiceAsync(identifier);
259
+ }
260
+
261
+ this.services.clear();
262
+ }
263
+
264
+ // --- Contribution Management ---
265
+
266
+ registerContributionPoint<T>(point: ContributionPoint<T>): void {
267
+ this.contributions.registerPoint(point);
268
+ this.eventBus.emit("contribution:point:register", point);
269
+ }
270
+
271
+ registerContribution<T>(
272
+ pointId: string,
273
+ contribution: Contribution<T>,
274
+ ): Disposable {
275
+ const disposable = this.contributions.register(pointId, contribution);
276
+ this.eventBus.emit("contribution:register", { ...contribution, pointId });
277
+ return disposable;
278
+ }
279
+
280
+ getContributions<T>(pointId: string): Contribution<T>[] {
281
+ return this.contributions.get<T>(pointId);
282
+ }
283
+
284
+ private resolveServiceIdentifier<T extends Service>(
285
+ service: T,
286
+ identifier?: ServiceIdentifier<T>,
287
+ ): ServiceIdentifier<T> {
288
+ return identifier ?? service.constructor.name;
289
+ }
290
+
291
+ private resolveUnregisterIdentifier(
292
+ serviceOrIdentifier: Service | ServiceIdentifier<Service>,
293
+ id?: ServiceIdentifier<Service>,
294
+ ): ServiceIdentifier<Service> {
295
+ if (
296
+ typeof serviceOrIdentifier === "string" ||
297
+ isServiceToken(serviceOrIdentifier)
298
+ ) {
299
+ return serviceOrIdentifier;
300
+ }
301
+
302
+ return id ?? serviceOrIdentifier.constructor.name;
303
+ }
304
+
305
+ private getServiceLabel(identifier: ServiceIdentifier<Service>): string {
306
+ if (typeof identifier === "string") {
307
+ return identifier;
308
+ }
309
+ return identifier.name;
310
+ }
311
+
312
+ private invokeServiceHook(
313
+ service: Service,
314
+ hook: "init" | "dispose",
315
+ ): void | Promise<void> {
316
+ const handler = service[hook];
317
+ if (!handler) {
318
+ return;
319
+ }
320
+ return handler.call(service, this.serviceContext);
321
+ }
322
+
323
+ private async invokeServiceHookAsync(
324
+ service: Service,
325
+ hook: "init" | "dispose",
326
+ ): Promise<void> {
327
+ await this.invokeServiceHook(service, hook);
328
+ }
329
+
330
+ private isPromiseLike(value: unknown): value is Promise<unknown> {
331
+ return (
332
+ typeof value === "object" &&
333
+ value !== null &&
334
+ "then" in value &&
335
+ typeof (value as { then?: unknown }).then === "function"
336
+ );
337
+ }
338
+ }