@carno.js/live 1.8.0 → 1.8.1
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/dist/LiveEngine.d.ts +14 -0
- package/dist/LiveEngine.js +73 -8
- package/dist/LivePlugin.d.ts +9 -0
- package/dist/LivePlugin.js +12 -0
- package/dist/config.d.ts +12 -1
- package/dist/config.js +1 -0
- package/dist/graph/DependencyGraph.d.ts +25 -0
- package/dist/graph/DependencyGraph.js +65 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/resource/ResourceRegistry.d.ts +3 -0
- package/dist/resource/ResourceRegistry.js +6 -0
- package/dist/scope-warning.d.ts +28 -0
- package/dist/scope-warning.js +48 -0
- package/dist/shared/canonical.d.ts +24 -0
- package/dist/shared/canonical.js +209 -19
- package/package.json +2 -2
- package/src/LiveEngine.ts +806 -730
- package/src/LivePlugin.ts +276 -253
- package/src/config.ts +61 -49
- package/src/graph/DependencyGraph.ts +223 -147
- package/src/index.ts +83 -81
- package/src/resource/ResourceRegistry.ts +185 -178
- package/src/scope-warning.ts +58 -0
- package/src/shared/canonical.ts +301 -63
- package/test/canonical-equivalence.test.ts +290 -0
- package/test/dependency-graph-index.test.ts +206 -0
- package/test/recompute-concurrency.test.ts +183 -0
- package/test/scope-warning.test.ts +157 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/LivePlugin.ts
CHANGED
|
@@ -1,253 +1,276 @@
|
|
|
1
|
-
import { Carno, ObservabilityService, type Container } from '@carno.js/core';
|
|
2
|
-
import { Orm } from '@carno.js/orm';
|
|
3
|
-
import { WebSocketPlugin, type WebSocketPluginConfig } from '@carno.js/websocket';
|
|
4
|
-
import { AllowAllAuthorizer, type LiveAuthorizer } from './auth/authorizer';
|
|
5
|
-
import { InProcessBus } from './bus/InProcessBus';
|
|
6
|
-
import type { InvalidationBus } from './bus/InvalidationBus';
|
|
7
|
-
import { PgNotifyBus } from './bus/PgNotifyBus';
|
|
8
|
-
import { PgNotifyEmitter, type PgNotifyTable } from './emitters/pg-notify-emitter';
|
|
9
|
-
import type { InvalidationEvent } from './graph/types';
|
|
10
|
-
import { resolveLiveConfig, type LiveConfig } from './config';
|
|
11
|
-
import { AppEmitter } from './emitters/AppEmitter';
|
|
12
|
-
import { DependencyGraph } from './graph/DependencyGraph';
|
|
13
|
-
import { SubscriptionRegistry } from './graph/SubscriptionRegistry';
|
|
14
|
-
import { LiveEngine } from './LiveEngine';
|
|
15
|
-
import { LiveMetrics } from './observability';
|
|
16
|
-
import { LiveService } from './LiveService';
|
|
17
|
-
import { ResourceRegistry } from './resource/ResourceRegistry';
|
|
18
|
-
import { createLiveRouteExecutor } from './resource/route-executor';
|
|
19
|
-
import { setLiveRuntime } from './runtime';
|
|
20
|
-
import { LiveETagMiddleware } from './http/etag';
|
|
21
|
-
import { FanTransport } from './transport/FanTransport';
|
|
22
|
-
import { dropLiveConnection, LiveGateway } from './transport/LiveGateway';
|
|
23
|
-
import { ConnectionScopeResolver, type LiveScopeResolver } from './transport/scope-resolver';
|
|
24
|
-
import { SocketTransport } from './transport/SocketTransport';
|
|
25
|
-
import { SseTransport } from './transport/SseTransport';
|
|
26
|
-
import { createSseRoutes } from './transport/sse-routes';
|
|
27
|
-
import { LIVE_CONNECTION_HEADER, LIVE_TOKEN_HEADER } from './shared/protocol';
|
|
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
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
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
|
-
}
|
|
1
|
+
import { Carno, ObservabilityService, type Container } from '@carno.js/core';
|
|
2
|
+
import { Orm } from '@carno.js/orm';
|
|
3
|
+
import { WebSocketPlugin, type WebSocketPluginConfig } from '@carno.js/websocket';
|
|
4
|
+
import { AllowAllAuthorizer, type LiveAuthorizer } from './auth/authorizer';
|
|
5
|
+
import { InProcessBus } from './bus/InProcessBus';
|
|
6
|
+
import type { InvalidationBus } from './bus/InvalidationBus';
|
|
7
|
+
import { PgNotifyBus } from './bus/PgNotifyBus';
|
|
8
|
+
import { PgNotifyEmitter, type PgNotifyTable } from './emitters/pg-notify-emitter';
|
|
9
|
+
import type { InvalidationEvent } from './graph/types';
|
|
10
|
+
import { resolveLiveConfig, type LiveConfig } from './config';
|
|
11
|
+
import { AppEmitter } from './emitters/AppEmitter';
|
|
12
|
+
import { DependencyGraph } from './graph/DependencyGraph';
|
|
13
|
+
import { SubscriptionRegistry } from './graph/SubscriptionRegistry';
|
|
14
|
+
import { LiveEngine } from './LiveEngine';
|
|
15
|
+
import { LiveMetrics } from './observability';
|
|
16
|
+
import { LiveService } from './LiveService';
|
|
17
|
+
import { ResourceRegistry } from './resource/ResourceRegistry';
|
|
18
|
+
import { createLiveRouteExecutor } from './resource/route-executor';
|
|
19
|
+
import { setLiveRuntime } from './runtime';
|
|
20
|
+
import { LiveETagMiddleware } from './http/etag';
|
|
21
|
+
import { FanTransport } from './transport/FanTransport';
|
|
22
|
+
import { dropLiveConnection, LiveGateway } from './transport/LiveGateway';
|
|
23
|
+
import { ConnectionScopeResolver, type LiveScopeResolver } from './transport/scope-resolver';
|
|
24
|
+
import { SocketTransport } from './transport/SocketTransport';
|
|
25
|
+
import { SseTransport } from './transport/SseTransport';
|
|
26
|
+
import { createSseRoutes } from './transport/sse-routes';
|
|
27
|
+
import { LIVE_CONNECTION_HEADER, LIVE_TOKEN_HEADER } from './shared/protocol';
|
|
28
|
+
import { defaultScopeWarning } from './scope-warning';
|
|
29
|
+
|
|
30
|
+
export interface LivePluginOptions {
|
|
31
|
+
/** Controllers holding @Live() handlers. Validated at bootstrap. */
|
|
32
|
+
controllers: (new (...args: any[]) => any)[];
|
|
33
|
+
/**
|
|
34
|
+
* Your own @Gateway classes. They must be listed here rather than passed to
|
|
35
|
+
* a second WebSocketPlugin: Carno.use() keeps only one WebSocket handler
|
|
36
|
+
* builder, so a second plugin silently wins and orphans the first.
|
|
37
|
+
*/
|
|
38
|
+
gateways?: (new (...args: any[]) => any)[];
|
|
39
|
+
/**
|
|
40
|
+
* Turns a handshake into the principal and tenant that key an instance.
|
|
41
|
+
*
|
|
42
|
+
* Left out, the default `ConnectionScopeResolver` makes the connection id
|
|
43
|
+
* the principal, so every `private` resource gets one instance per
|
|
44
|
+
* connection and the boot logs say so. Passing
|
|
45
|
+
* `new ConnectionScopeResolver()` explicitly is how an application states
|
|
46
|
+
* that per-connection instances are what it wants, and silences that.
|
|
47
|
+
*/
|
|
48
|
+
scopeResolver?: LiveScopeResolver;
|
|
49
|
+
/**
|
|
50
|
+
* Decides whether a connection may hold a subscription, and is re-asked
|
|
51
|
+
* whenever `LiveService.invalidate('auth:principal#<id>')` fires.
|
|
52
|
+
*/
|
|
53
|
+
authorizer?: LiveAuthorizer;
|
|
54
|
+
/**
|
|
55
|
+
* Watch these tables with a Postgres trigger, so writes that never went
|
|
56
|
+
* through @carno.js/orm also invalidate. Requires PostgreSQL 11 or newer.
|
|
57
|
+
*/
|
|
58
|
+
pgNotify?: {
|
|
59
|
+
tables: PgNotifyTable[];
|
|
60
|
+
/** Defaults to the ORM's own connection string. */
|
|
61
|
+
url?: string;
|
|
62
|
+
channel?: string;
|
|
63
|
+
};
|
|
64
|
+
/** Carry invalidations from this node to the others. */
|
|
65
|
+
distributed?: {
|
|
66
|
+
transport: 'pg-notify';
|
|
67
|
+
url?: string;
|
|
68
|
+
channel?: string;
|
|
69
|
+
nodeId?: string;
|
|
70
|
+
};
|
|
71
|
+
config?: Partial<LiveConfig>;
|
|
72
|
+
websocket?: WebSocketPluginConfig;
|
|
73
|
+
/**
|
|
74
|
+
* Content-hash ETag on live GET routes, so a client with neither
|
|
75
|
+
* WebSocket nor SSE can poll cheaply. On by default: it only ever adds a
|
|
76
|
+
* header, and it only touches routes that are live.
|
|
77
|
+
*/
|
|
78
|
+
etag?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Serve the protocol over Server-Sent Events as well, for clients whose
|
|
81
|
+
* proxy blocks WebSocket. Off by default: it adds two public routes.
|
|
82
|
+
*/
|
|
83
|
+
sse?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class LivePlugin {
|
|
87
|
+
static create(options: LivePluginOptions): Carno {
|
|
88
|
+
const config = resolveLiveConfig(options.config);
|
|
89
|
+
const resources = new ResourceRegistry();
|
|
90
|
+
const graph = new DependencyGraph();
|
|
91
|
+
const subs = new SubscriptionRegistry();
|
|
92
|
+
const distributedBus = options.distributed
|
|
93
|
+
? new PgNotifyBus({
|
|
94
|
+
url: options.distributed.url ?? '',
|
|
95
|
+
channel: options.distributed.channel,
|
|
96
|
+
nodeId: options.distributed.nodeId
|
|
97
|
+
})
|
|
98
|
+
: null;
|
|
99
|
+
const bus: InvalidationBus = distributedBus ?? new InProcessBus();
|
|
100
|
+
const sockets = new SocketTransport();
|
|
101
|
+
const fan = new FanTransport();
|
|
102
|
+
fan.add(sockets);
|
|
103
|
+
let sink: ObservabilityService | null = null;
|
|
104
|
+
const metrics = new LiveMetrics({
|
|
105
|
+
onMetric: (name, value, tags) => sink?.onMetric(name, value, tags)
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const engine = new LiveEngine(
|
|
109
|
+
resources,
|
|
110
|
+
graph,
|
|
111
|
+
subs,
|
|
112
|
+
bus,
|
|
113
|
+
fan,
|
|
114
|
+
config,
|
|
115
|
+
options.authorizer ?? new AllowAllAuthorizer(),
|
|
116
|
+
metrics
|
|
117
|
+
);
|
|
118
|
+
const emitter = new AppEmitter(bus, config);
|
|
119
|
+
const scopeResolver = options.scopeResolver ?? new ConnectionScopeResolver();
|
|
120
|
+
|
|
121
|
+
const dispose: (() => Promise<void> | void)[] = [() => engine.stop()];
|
|
122
|
+
|
|
123
|
+
setLiveRuntime({
|
|
124
|
+
engine,
|
|
125
|
+
transport: sockets,
|
|
126
|
+
resolver: scopeResolver,
|
|
127
|
+
scopes: new Map(),
|
|
128
|
+
handshakes: new Set(),
|
|
129
|
+
resources,
|
|
130
|
+
dispose
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const plugin = new Carno({ exports: [] });
|
|
134
|
+
plugin.controllers(options.controllers);
|
|
135
|
+
plugin.services([LiveService]);
|
|
136
|
+
|
|
137
|
+
if (options.sse) {
|
|
138
|
+
const sse = new SseTransport({
|
|
139
|
+
heartbeatMs: config.sseHeartbeatMs,
|
|
140
|
+
maxConnections: config.sseMaxConnections,
|
|
141
|
+
onDisconnect: dropLiveConnection
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
fan.add(sse);
|
|
145
|
+
dispose.push(() => sse.stop());
|
|
146
|
+
|
|
147
|
+
const routes = createSseRoutes({
|
|
148
|
+
transport: sse,
|
|
149
|
+
streamPath: config.ssePath,
|
|
150
|
+
controlPath: config.sseControlPath
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
plugin.route('GET', routes.streamPath, routes.stream);
|
|
154
|
+
plugin.route('POST', routes.controlPath, routes.control);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let teachEtag: ((paths: { method: string; path: string; resourceId?: string }[]) => void) | null = null;
|
|
158
|
+
|
|
159
|
+
// Registered now, taught later: `plugin.middlewares()` runs before
|
|
160
|
+
// bootstrap, and the resources are only known inside the builder.
|
|
161
|
+
// The middleware also gates polling, even when ETags are disabled.
|
|
162
|
+
const etag = new LiveETagMiddleware([], { enabled: options.etag !== false });
|
|
163
|
+
etag.setPollingGuard(async ({ resourceId, inputs, request }) => {
|
|
164
|
+
const connectionId = request.headers.get(LIVE_CONNECTION_HEADER);
|
|
165
|
+
|
|
166
|
+
if (!connectionId) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let scope;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
scope = await scopeResolver.resolve({
|
|
174
|
+
connectionId,
|
|
175
|
+
token: request.headers.get(LIVE_TOKEN_HEADER) ?? undefined
|
|
176
|
+
});
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return await engine.authorizePolling(connectionId, resourceId, inputs, scope)
|
|
182
|
+
? scope
|
|
183
|
+
: null;
|
|
184
|
+
});
|
|
185
|
+
plugin.middlewares([etag]);
|
|
186
|
+
teachEtag = paths => etag.setPaths(paths);
|
|
187
|
+
|
|
188
|
+
const websocket = WebSocketPlugin.create(
|
|
189
|
+
[LiveGateway, ...(options.gateways ?? [])],
|
|
190
|
+
options.websocket
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const innerBuilder = websocket._wsHandlerBuilder!;
|
|
194
|
+
const upgradePaths = [...websocket._wsUpgradePaths];
|
|
195
|
+
|
|
196
|
+
plugin.use(websocket);
|
|
197
|
+
|
|
198
|
+
// The builder runs after bootstrap, when the container holds the
|
|
199
|
+
// controller instances and the ORM holds its connection — which is why
|
|
200
|
+
// everything that needs a database URL is started here and not above.
|
|
201
|
+
plugin.wsHandler((container: Container) => {
|
|
202
|
+
// Resolved here and not above: the container does not exist until
|
|
203
|
+
// bootstrap, and an app with no observability plugin never
|
|
204
|
+
// registers one.
|
|
205
|
+
sink = container.has(ObservabilityService) ? container.get(ObservabilityService) : null;
|
|
206
|
+
const routeExecutor = createLiveRouteExecutor(container.get(Carno));
|
|
207
|
+
|
|
208
|
+
for (const ControllerClass of options.controllers) {
|
|
209
|
+
resources.register(ControllerClass, container.get(ControllerClass), routeExecutor);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
teachEtag?.(resources.livePaths());
|
|
213
|
+
|
|
214
|
+
// Both halves of the per-connection trap are known only here: the
|
|
215
|
+
// resolver comes from the options, the resources from the scan the
|
|
216
|
+
// loop above just finished.
|
|
217
|
+
const scopeWarning = defaultScopeWarning({
|
|
218
|
+
privateResourceIds: resources.idsShared('private'),
|
|
219
|
+
usingDefaultResolver: options.scopeResolver === undefined,
|
|
220
|
+
maxInstancesPerNode: config.maxInstancesPerNode
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (scopeWarning) {
|
|
224
|
+
console.warn(scopeWarning);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (options.pgNotify) {
|
|
228
|
+
const driver = Orm.getInstance().driverInstance;
|
|
229
|
+
const deliver = (events: InvalidationEvent[]): void => {
|
|
230
|
+
// A trigger already notified every node. Publishing it on
|
|
231
|
+
// the bus would send it around a second time.
|
|
232
|
+
if (distributedBus) {
|
|
233
|
+
distributedBus.publishLocal(events);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
bus.publish(events);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const pgEmitter = new PgNotifyEmitter(deliver, {
|
|
241
|
+
tables: options.pgNotify.tables,
|
|
242
|
+
url: options.pgNotify.url ?? driver.connectionString,
|
|
243
|
+
channel: options.pgNotify.channel,
|
|
244
|
+
execute: sql => driver.executeSql(sql)
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Two emitters on one table would wake the same instance twice.
|
|
248
|
+
emitter.setCoveredTables(pgEmitter.coveredTables());
|
|
249
|
+
dispose.push(() => pgEmitter.detach());
|
|
250
|
+
|
|
251
|
+
void pgEmitter.attach().catch(error => {
|
|
252
|
+
console.error('[carno:live] the Postgres emitter failed to attach', error);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (distributedBus) {
|
|
257
|
+
if (!options.distributed?.url) {
|
|
258
|
+
distributedBus.setUrl(Orm.getInstance().driverInstance.connectionString);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
void distributedBus.start().catch(error => {
|
|
262
|
+
console.error('[carno:live] the distributed bus failed to start', error);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
dispose.push(() => distributedBus.stop());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
emitter.attach();
|
|
269
|
+
engine.start();
|
|
270
|
+
|
|
271
|
+
return innerBuilder(container);
|
|
272
|
+
}, upgradePaths);
|
|
273
|
+
|
|
274
|
+
return plugin;
|
|
275
|
+
}
|
|
276
|
+
}
|