@reventlessdev/reventless-local 3.0.0-alpha.138 → 3.0.0-alpha.140

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,120 @@
1
+ // Focused seam test for the promoted, transport-neutral GraphQL server runtime
2
+ // (`ReventlessGraphqlServer.GraphQL_ServerInstance` + `GraphQL_SubscriptionBridge`).
3
+ //
4
+ // The promotion introduced ONE behavioral seam: the subscription bridge takes
5
+ // its PubSub instance as a *parameter* instead of constructing a module
6
+ // singleton. This test drives the promoted modules directly with an
7
+ // **explicitly injected** in-process PubSub (never the local singleton) to
8
+ // prove the parameterized-PubSub seam is behavior-preserving:
9
+ //
10
+ // 1. registerAll builds the subscription SDL on a fresh GraphQL_ServerInstance,
11
+ // strips @aws_subscribe, and injects `scalar AWSJSON`.
12
+ // 2. A resolver from makeFieldResolver, subscribed on the injected PubSub,
13
+ // receives a payload published via Bridge.publish on that same PubSub.
14
+
15
+ @@warning("-44")
16
+
17
+ open JestGlobals
18
+
19
+ module Bridge = ReventlessGraphqlServer.GraphQL_SubscriptionBridge
20
+ module Server = ReventlessGraphqlServer.GraphQL_ServerInstance
21
+
22
+ // AsyncIterable.next() binding — yoga's PubSub returns {value, done}.
23
+ type asyncIterResult = {value: JSON.t, done: bool}
24
+ @send external iterNext: 'a => promise<asyncIterResult> = "next"
25
+
26
+ @val external setTimeout: (unit => unit, int) => int = "setTimeout"
27
+
28
+ // The subscription resolver object shape produced by makeFieldResolver.
29
+ // `subscribe` returns yoga's Repeater-based AsyncIterable (kept opaque).
30
+ type iterable
31
+ type subResolver = {
32
+ subscribe: (JSON.t, JSON.t, JSON.t) => iterable,
33
+ resolve: JSON.t => JSON.t,
34
+ }
35
+
36
+ let startConsumer = (iter: 'iter, timeoutMs: int): promise<Null.t<JSON.t>> => {
37
+ let timeoutP = Promise.make((resolve, _) => {
38
+ let _: int = setTimeout(() => resolve(Null.null), timeoutMs)
39
+ })
40
+ let consumeP = async () =>
41
+ try {
42
+ let result = await iter->iterNext
43
+ result.done ? Null.null : result.value->Null.make
44
+ } catch {
45
+ | _ => Null.null
46
+ }
47
+ Promise.race([timeoutP, consumeP()])
48
+ }
49
+
50
+ let yieldTick = (): promise<unit> =>
51
+ Promise.make((resolve, _) => {
52
+ let _: int = setTimeout(() => resolve(), 10)
53
+ })
54
+
55
+ describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
56
+ testSync("registerAll builds subscription SDL, strips @aws_subscribe, injects AWSJSON", () => {
57
+ let pubSub = GraphqlYoga.createPubSub()
58
+ let server = Server.make(~label="SeamTest")
59
+
60
+ let sdlField =
61
+ "onCatalogProduct_stateChanged: CatalogProduct\n @aws_subscribe(mutations: [\"addProduct\"])"
62
+
63
+ Bridge.registerAll(
64
+ ~server,
65
+ ~sdlFields=[sdlField],
66
+ ~sourceAEntries=[],
67
+ ~sourceBEntries=[{fieldName: "onCatalogProduct_stateChanged", topic: "onCatalogProduct_stateChanged"}],
68
+ ~pubSub,
69
+ )
70
+
71
+ let sdl = server.buildSdl()
72
+ expect(sdl->String.includes("type Subscription"))->toBe(true)
73
+ expect(sdl->String.includes("onCatalogProduct_stateChanged"))->toBe(true)
74
+ // @aws_subscribe is AppSync-only and must be stripped for yoga.
75
+ expect(sdl->String.includes("@aws_subscribe"))->toBe(false)
76
+ // AWSJSON scalar injected so yoga accepts AppSync-derived event log types.
77
+ expect(sdl->String.includes("scalar AWSJSON"))->toBe(true)
78
+ })
79
+
80
+ testPromise("makeFieldResolver + publish deliver over the injected PubSub", async () => {
81
+ let pubSub = GraphqlYoga.createPubSub()
82
+ let topic = "onCatalogProduct_stateChanged"
83
+
84
+ let resolver: subResolver = Obj.magic(Bridge.makeFieldResolver(~pubSub, topic))
85
+ let iter = resolver.subscribe(JSON.Encode.null, JSON.Encode.null, JSON.Encode.null)
86
+ let consumerPromise = startConsumer(iter, 1000)
87
+ await yieldTick()
88
+
89
+ let payload =
90
+ JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("prod-1"))]))
91
+ Bridge.publish(~pubSub, topic, payload)
92
+
93
+ let received = await consumerPromise
94
+ switch received->Null.toOption {
95
+ | Some(msg) => expect(msg)->toEqual(payload)
96
+ | None => expect("injected-pubsub: timed out")->toBe("received")
97
+ }
98
+ })
99
+
100
+ testPromise("a separate injected PubSub does NOT receive another's publish", async () => {
101
+ // Proves isolation: publishing on PubSub A never reaches a subscriber on B.
102
+ let pubSubA = GraphqlYoga.createPubSub()
103
+ let pubSubB = GraphqlYoga.createPubSub()
104
+ let topic = "onCatalogProduct_stateChanged"
105
+
106
+ let resolverB: subResolver = Obj.magic(Bridge.makeFieldResolver(~pubSub=pubSubB, topic))
107
+ let iterB = resolverB.subscribe(JSON.Encode.null, JSON.Encode.null, JSON.Encode.null)
108
+ let consumerPromise = startConsumer(iterB, 300)
109
+ await yieldTick()
110
+
111
+ Bridge.publish(
112
+ ~pubSub=pubSubA,
113
+ topic,
114
+ JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("prod-1"))])),
115
+ )
116
+
117
+ let received = await consumerPromise
118
+ expect(received->Null.toOption)->toEqual(None)
119
+ })
120
+ })
@@ -0,0 +1,96 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as GraphqlYoga from "graphql-yoga";
4
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
+ import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
6
+ import * as GraphQL_SubscriptionBridge$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_SubscriptionBridge.res.mjs";
7
+
8
+ function startConsumer(iter, timeoutMs) {
9
+ let timeoutP = new Promise((resolve, param) => {
10
+ setTimeout(() => resolve(null), timeoutMs);
11
+ });
12
+ let consumeP = async () => {
13
+ try {
14
+ let result = await iter.next();
15
+ if (result.done) {
16
+ return null;
17
+ } else {
18
+ return result.value;
19
+ }
20
+ } catch (exn) {
21
+ return null;
22
+ }
23
+ };
24
+ return Promise.race([
25
+ timeoutP,
26
+ consumeP()
27
+ ]);
28
+ }
29
+
30
+ function yieldTick() {
31
+ return new Promise((resolve, param) => {
32
+ setTimeout(() => resolve(), 10);
33
+ });
34
+ }
35
+
36
+ globalThis.describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
37
+ globalThis.test("registerAll builds subscription SDL, strips @aws_subscribe, injects AWSJSON", () => {
38
+ let pubSub = GraphqlYoga.createPubSub();
39
+ let server = GraphQL_ServerInstance$ReventlessGraphqlServer.make("SeamTest");
40
+ GraphQL_SubscriptionBridge$ReventlessGraphqlServer.registerAll(server, ["onCatalogProduct_stateChanged: CatalogProduct\n @aws_subscribe(mutations: [\"addProduct\"])"], [], [{
41
+ fieldName: "onCatalogProduct_stateChanged",
42
+ topic: "onCatalogProduct_stateChanged"
43
+ }], pubSub);
44
+ let sdl = server.buildSdl();
45
+ globalThis.expect(sdl.includes("type Subscription")).toBe(true);
46
+ globalThis.expect(sdl.includes("onCatalogProduct_stateChanged")).toBe(true);
47
+ globalThis.expect(sdl.includes("@aws_subscribe")).toBe(false);
48
+ globalThis.expect(sdl.includes("scalar AWSJSON")).toBe(true);
49
+ });
50
+ globalThis.test("makeFieldResolver + publish deliver over the injected PubSub", async () => {
51
+ let pubSub = GraphqlYoga.createPubSub();
52
+ let topic = "onCatalogProduct_stateChanged";
53
+ let resolver = GraphQL_SubscriptionBridge$ReventlessGraphqlServer.makeFieldResolver(pubSub, topic);
54
+ let iter = resolver.subscribe(null, null, null);
55
+ let consumerPromise = startConsumer(iter, 1000);
56
+ await yieldTick();
57
+ let payload = Object.fromEntries([[
58
+ "id",
59
+ "prod-1"
60
+ ]]);
61
+ GraphQL_SubscriptionBridge$ReventlessGraphqlServer.publish(pubSub, topic, payload);
62
+ let received = await consumerPromise;
63
+ if (received !== null) {
64
+ globalThis.expect(received).toEqual(payload);
65
+ } else {
66
+ globalThis.expect("injected-pubsub: timed out").toBe("received");
67
+ }
68
+ });
69
+ globalThis.test("a separate injected PubSub does NOT receive another's publish", async () => {
70
+ let pubSubA = GraphqlYoga.createPubSub();
71
+ let pubSubB = GraphqlYoga.createPubSub();
72
+ let topic = "onCatalogProduct_stateChanged";
73
+ let resolverB = GraphQL_SubscriptionBridge$ReventlessGraphqlServer.makeFieldResolver(pubSubB, topic);
74
+ let iterB = resolverB.subscribe(null, null, null);
75
+ let consumerPromise = startConsumer(iterB, 300);
76
+ await yieldTick();
77
+ GraphQL_SubscriptionBridge$ReventlessGraphqlServer.publish(pubSubA, topic, Object.fromEntries([[
78
+ "id",
79
+ "prod-1"
80
+ ]]));
81
+ let received = await consumerPromise;
82
+ globalThis.expect(received === null ? undefined : Primitive_option.some(received)).toEqual(undefined);
83
+ });
84
+ });
85
+
86
+ let Bridge;
87
+
88
+ let Server;
89
+
90
+ export {
91
+ Bridge,
92
+ Server,
93
+ startConsumer,
94
+ yieldTick,
95
+ }
96
+ /* Not a pure module */
@@ -1,310 +0,0 @@
1
- // GraphQL server instance factory.
2
- // Creates independent server instances with isolated registries.
3
- // Used by Platform to create separate core and plugin servers in split mode.
4
-
5
- module YG = GraphqlYoga
6
-
7
- let log = ReventlessCore.Logger.fromEnv()
8
-
9
- type resolverFn = YG.resolverFn
10
-
11
- // graphql-ws WebSocket transport for subscriptions. Yoga's built-in
12
- // subscription transport is SSE-over-POST; clients using the graphql-ws
13
- // protocol (e.g. host-shell, AppSync clients in prod) need an explicit
14
- // WebSocket endpoint. We attach a `WebSocketServer` to the same HTTP server
15
- // and hand it to `graphql-ws/lib/use/ws::useServer` along with the live
16
- // schema, which fans out subscription events to connected sockets.
17
- type wsServer
18
- @new @module("ws")
19
- external newWebSocketServer: {"server": YG.httpServer, "path": string} => wsServer =
20
- "WebSocketServer"
21
- @module("graphql-ws/lib/use/ws")
22
- external wsUseServer: ({"schema": YG.schema}, wsServer) => unit = "useServer"
23
-
24
- @val external processEnv: dict<string> = "process.env"
25
- let debug = processEnv->Dict.get("GRAPHQL_DEBUG")->Option.isSome
26
-
27
- type diagnostics = {
28
- registeredTypeDefinitions: array<string>,
29
- registeredMutationFields: array<string>,
30
- registeredQueryFields: array<string>,
31
- registeredMutationResolvers: array<string>,
32
- registeredQueryResolvers: array<string>,
33
- typeCount: int,
34
- sdlMutationCount: int,
35
- sdlQueryCount: int,
36
- resolverMutationCount: int,
37
- resolverQueryCount: int,
38
- mismatches: array<string>,
39
- fullSdl: option<string>,
40
- serverRunning: bool,
41
- }
42
-
43
- type t = {
44
- registerMutations: (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => unit,
45
- registerQueries: (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => unit,
46
- registerSubscriptions: (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => unit,
47
- registerTypes: (~sdlTypes: array<string>) => unit,
48
- getMutationResolver: string => option<resolverFn>,
49
- getQueryResolver: string => option<resolverFn>,
50
- // `~contextFactory` is the yoga context factory — when supplied, requests
51
- // run through it so resolvers see `ctx.identity`. Omit (or pass None) for
52
- // unauthenticated servers. In-memory `Platform.res` wires
53
- // `Auth_GraphqlContext.buildAuthContext` here so the admin server enforces
54
- // the same bearer-token rules as the data server instead of leaving
55
- // Platform_* queries / mutations wide open.
56
- start: (~port: int=?, ~contextFactory: YG.contextFactory=?, unit) => unit,
57
- stop: unit => unit,
58
- reset: unit => unit,
59
- buildSdl: unit => string,
60
- getFullSdl: unit => option<string>,
61
- getSchema: unit => option<YG.schema>,
62
- diagnostics: unit => diagnostics,
63
- printDiagnostics: unit => unit,
64
- }
65
-
66
- let extractFieldName = (sdlField: string): string => {
67
- let trimmed = sdlField->String.trim
68
- trimmed
69
- ->String.split("(")
70
- ->Array.get(0)
71
- ->Option.getOr("")
72
- ->String.trim
73
- ->String.split(":")
74
- ->Array.get(0)
75
- ->Option.getOr("")
76
- ->String.trim
77
- }
78
-
79
- let make = (~label: string="GraphQL"): t => {
80
- let mutationResolvers: ref<dict<resolverFn>> = ref(Dict.make())
81
- let queryResolvers: ref<dict<resolverFn>> = ref(Dict.make())
82
- let subscriptionResolvers: ref<dict<resolverFn>> = ref(Dict.make())
83
- let mutationFields: ref<array<string>> = ref([])
84
- let queryFields: ref<array<string>> = ref([])
85
- let subscriptionFields: ref<array<string>> = ref([])
86
- let typeDefinitions: ref<array<string>> = ref([])
87
- let activeServer: ref<option<YG.httpServer>> = ref(None)
88
- let activeSchema: ref<option<YG.schema>> = ref(None)
89
- let lastFullSdl: ref<option<string>> = ref(None)
90
-
91
- let registerMutations = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
92
- mutationFields.contents = mutationFields.contents->Array.concat(sdlFields)
93
- resolvers->Dict.toArray->Array.forEach(((k, v)) => mutationResolvers.contents->Dict.set(k, v))
94
- }
95
-
96
- let registerQueries = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
97
- queryFields.contents = queryFields.contents->Array.concat(sdlFields)
98
- resolvers->Dict.toArray->Array.forEach(((k, v)) => queryResolvers.contents->Dict.set(k, v))
99
- }
100
-
101
- let registerSubscriptions = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
102
- subscriptionFields.contents = subscriptionFields.contents->Array.concat(sdlFields)
103
- resolvers->Dict.toArray->Array.forEach(((k, v)) =>
104
- subscriptionResolvers.contents->Dict.set(k, v)
105
- )
106
- }
107
-
108
- let registerTypes = (~sdlTypes: array<string>) => {
109
- typeDefinitions.contents = typeDefinitions.contents->Array.concat(sdlTypes)
110
- }
111
-
112
- let getMutationResolver = (fieldName: string): option<resolverFn> =>
113
- mutationResolvers.contents->Dict.get(fieldName)
114
-
115
- let getQueryResolver = (fieldName: string): option<resolverFn> =>
116
- queryResolvers.contents->Dict.get(fieldName)
117
-
118
- let buildSdl = () => {
119
- let typesSdl =
120
- typeDefinitions.contents->Array.length > 0
121
- ? typeDefinitions.contents->Array.join("\n\n")
122
- : ""
123
- let mutations =
124
- mutationFields.contents->Array.length > 0
125
- ? mutationFields.contents->Array.join("\n")
126
- : " _noop: String"
127
- let queries =
128
- queryFields.contents->Array.length > 0
129
- ? queryFields.contents->Array.join("\n")
130
- : " _noop: String"
131
- let queriesMutationsSdl = `type Query {
132
- ${queries}
133
- }
134
- type Mutation {
135
- ${mutations}
136
- }`
137
- let subscriptionsSdl =
138
- subscriptionFields.contents->Array.length > 0
139
- ? `\n\ntype Subscription {\n${subscriptionFields.contents->Array.join("\n")}\n}`
140
- : ""
141
- let base =
142
- typesSdl->String.length > 0
143
- ? typesSdl ++ "\n\n" ++ queriesMutationsSdl
144
- : queriesMutationsSdl
145
- base ++ subscriptionsSdl
146
- }
147
-
148
- let stop = () =>
149
- switch activeServer.contents {
150
- | Some(server) =>
151
- server->YG.close(() => ())
152
- activeServer.contents = None
153
- activeSchema.contents = None
154
- | None => ()
155
- }
156
-
157
- let start = (~port: int=4000, ~contextFactory: option<YG.contextFactory>=?, ()) => {
158
- let resolvers = Dict.make()
159
- resolvers->Dict.set("Query", queryResolvers.contents)
160
- resolvers->Dict.set("Mutation", mutationResolvers.contents)
161
- if subscriptionResolvers.contents->Dict.keysToArray->Array.length > 0 {
162
- resolvers->Dict.set("Subscription", subscriptionResolvers.contents)
163
- }
164
- let sdl = buildSdl()
165
- lastFullSdl.contents = Some(sdl)
166
- let schema = YG.createSchema({"typeDefs": sdl, "resolvers": resolvers})
167
- activeSchema.contents = Some(schema)
168
- let yoga = switch contextFactory {
169
- | Some(ctx) =>
170
- YG.createYogaWithContext({
171
- "schema": schema,
172
- "graphiql": true,
173
- "logging": debug,
174
- "maskedErrors": !debug,
175
- "context": ctx,
176
- })
177
- | None =>
178
- YG.createYoga({
179
- "schema": schema,
180
- "graphiql": true,
181
- "logging": debug,
182
- "maskedErrors": !debug,
183
- })
184
- }
185
- let server = YG.createServer(yoga)
186
- server->YG.listen(port, () =>
187
- log.info(~comp=label, `listening on http://localhost:${port->Int.toString}/graphql`)
188
- )
189
- if subscriptionResolvers.contents->Dict.keysToArray->Array.length > 0 {
190
- let wss = newWebSocketServer({"server": server, "path": "/graphql"})
191
- wsUseServer({"schema": schema}, wss)
192
- log.info(~comp=label, `graphql-ws subscriptions on ws://localhost:${port->Int.toString}/graphql`)
193
- }
194
- activeServer.contents = Some(server)
195
- }
196
-
197
- let reset = () => {
198
- mutationResolvers.contents = Dict.make()
199
- queryResolvers.contents = Dict.make()
200
- subscriptionResolvers.contents = Dict.make()
201
- mutationFields.contents = []
202
- queryFields.contents = []
203
- subscriptionFields.contents = []
204
- typeDefinitions.contents = []
205
- activeSchema.contents = None
206
- lastFullSdl.contents = None
207
- }
208
-
209
- let getFullSdl = () => lastFullSdl.contents
210
- let getSchema = () => activeSchema.contents
211
-
212
- let diagnostics = (): diagnostics => {
213
- let mutFieldNames =
214
- mutationFields.contents->Array.map(extractFieldName)->Array.filter(n => n != "_noop")
215
- let queryFieldNames =
216
- queryFields.contents->Array.map(extractFieldName)->Array.filter(n => n != "_noop")
217
- let mutResolverNames = mutationResolvers.contents->Dict.keysToArray
218
- let queryResolverNames = queryResolvers.contents->Dict.keysToArray
219
-
220
- let mismatches: array<string> = []
221
-
222
- let mutResolverSet = Set.fromArray(mutResolverNames)
223
- mutFieldNames->Array.forEach(name =>
224
- if !(mutResolverSet->Set.has(name)) {
225
- mismatches->Array.push(`Mutation "${name}": SDL field but no resolver`)
226
- }
227
- )
228
- let queryResolverSet = Set.fromArray(queryResolverNames)
229
- queryFieldNames->Array.forEach(name =>
230
- if !(queryResolverSet->Set.has(name)) {
231
- mismatches->Array.push(`Query "${name}": SDL field but no resolver`)
232
- }
233
- )
234
- let mutFieldSet = Set.fromArray(mutFieldNames)
235
- mutResolverNames->Array.forEach(name =>
236
- if !(mutFieldSet->Set.has(name)) {
237
- mismatches->Array.push(`Mutation "${name}": resolver but no SDL field`)
238
- }
239
- )
240
- let queryFieldSet = Set.fromArray(queryFieldNames)
241
- queryResolverNames->Array.forEach(name =>
242
- if !(queryFieldSet->Set.has(name)) {
243
- mismatches->Array.push(`Query "${name}": resolver but no SDL field`)
244
- }
245
- )
246
- let typeNames =
247
- typeDefinitions.contents->Array.map(ReventlessCore.GraphQL_Stitcher.extractLeadingName)
248
-
249
- {
250
- registeredTypeDefinitions: typeNames,
251
- registeredMutationFields: mutFieldNames,
252
- registeredQueryFields: queryFieldNames,
253
- registeredMutationResolvers: mutResolverNames,
254
- registeredQueryResolvers: queryResolverNames,
255
- typeCount: typeNames->Array.length,
256
- sdlMutationCount: mutFieldNames->Array.length,
257
- sdlQueryCount: queryFieldNames->Array.length,
258
- resolverMutationCount: mutResolverNames->Array.length,
259
- resolverQueryCount: queryResolverNames->Array.length,
260
- mismatches,
261
- fullSdl: lastFullSdl.contents,
262
- serverRunning: activeServer.contents->Option.isSome,
263
- }
264
- }
265
-
266
- let printDiagnostics = () => {
267
- let p = s => log.info(~comp=label, s)
268
- let d = diagnostics()
269
- p("Diagnostics")
270
- p(` Types: ${d.typeCount->Int.toString}`)
271
- p(
272
- ` Mutations: ${d.sdlMutationCount->Int.toString} SDL fields, ${d.resolverMutationCount->Int.toString} resolvers`,
273
- )
274
- p(
275
- ` Queries: ${d.sdlQueryCount->Int.toString} SDL fields, ${d.resolverQueryCount->Int.toString} resolvers`,
276
- )
277
- if d.mismatches->Array.length > 0 {
278
- p(` Mismatches (${d.mismatches->Array.length->Int.toString}):`)
279
- d.mismatches->Array.forEach(m => p(` - ${m}`))
280
- } else {
281
- p(" No mismatches")
282
- }
283
- p(` Server running: ${d.serverRunning ? "yes" : "no"}`)
284
- switch d.fullSdl {
285
- | Some(sdl) =>
286
- p("")
287
- p("--- Full SDL ---")
288
- sdl->String.split("\n")->Array.forEach(line => p(line))
289
- p("--- End ---")
290
- | None => p(" No SDL recorded")
291
- }
292
- }
293
-
294
- {
295
- registerMutations,
296
- registerQueries,
297
- registerSubscriptions,
298
- registerTypes,
299
- getMutationResolver,
300
- getQueryResolver,
301
- start,
302
- stop,
303
- reset,
304
- buildSdl,
305
- getFullSdl,
306
- getSchema,
307
- diagnostics,
308
- printDiagnostics,
309
- }
310
- }