diff --git a/guides/javascript_client/graphiql_subscriptions.md b/guides/javascript_client/graphiql_subscriptions.md
new file mode 100644
index 0000000000..bfd097628a
--- /dev/null
+++ b/guides/javascript_client/graphiql_subscriptions.md
@@ -0,0 +1,94 @@
+---
+layout: guide
+doc_stub: false
+search: true
+section: JavaScript Client
+title: GraphiQL Subscriptions
+desc: Testing GraphQL subscriptions in the GraphiQL IDE
+index: 5
+---
+
+After setting up your server, you can integrate subscriptions into [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme), the in-browser GraphQL IDE.
+
+## Adding GraphiQL to your app
+
+To get started, make a page for rendering GraphiQL, for example:
+
+```html
+
+
+```
+
+Then, install GraphiQL (eg, `yarn add graphiql`) and add JavaScript code to import GraphiQL and render it on your page:
+
+```js
+import { GraphiQL } from 'graphiql';
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import 'graphiql/graphiql.css';
+import { createGraphiQLFetcher } from '@graphiql/toolkit';
+
+const fetcher = createGraphiQLFetcher({ url: '/graphql' });
+const root = createRoot(document.getElementById('root'));
+root.render();
+```
+
+After that, you should be able to load the page in your app and see the GraphiQL editor.
+
+## Ably
+
+To integrate {% internal_link "Ably subscriptions", "subscriptions/ably_implementation" %}, use `createAblyFetcher`, for example:
+
+```js
+import Ably from "ably"
+import createAblyFetcher from 'graphql-ruby-client/subscriptions/createAblyFetcher';
+
+// Initialize a client
+// the key must have "subscribe" and "presence" permissions
+const ably = new Ably.Realtime({ key: "your.application.key" })
+
+// Initialize a new fetcher and pass it to GraphiQL below
+var fetcher = createAblyFetcher({ ably: ably, url: "/graphql" })
+const root = createRoot(document.getElementById('root'));
+root.render();
+```
+
+Under the hood, it will use `window.fetch` to send GraphQL operations to the server, then listen for `X-Subscription-ID` headers in responses. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createAblyFetcher({ ... })`.
+
+## Pusher
+
+To integrate {% internal_link "Pusher subscriptions", "subscriptions/pusher_implementation" %}, use `createPusherFetcher`, for example:
+
+```js
+import Pusher from "pusher-js"
+import createPusherFetcher from 'graphql-ruby-client/subscriptions/createPusherFetcher';
+
+// Initialize a client
+const pusher = new Pusher("your-app-key", { cluster: "your-cluster" })
+
+// Initialize a new fetcher and pass it to GraphiQL below
+var fetcher = createPusherFetcher({ pusher: pusher, url: "/graphql" })
+const root = createRoot(document.getElementById('root'));
+root.render();
+```
+
+Under the hood, it will use `window.fetch` to send GraphQL operations to the server, then listen for `X-Subscription-ID` headers in responses. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createPusherFetcher({ ... })`.
+
+## ActionCable
+
+To integrate {% internal_link "ActionCable subscriptions", "subscriptions/action_cable_implementation" %}, use `createActionCableFetcher`, for example:
+
+```js
+import { createConsumer } from "@rails/actioncable"
+import createActionCableFetcher from 'graphql-ruby-client/subscriptions/createActionCableFetcher';
+
+// Initialize a client
+const actionCable = createConsumer()
+
+// Initialize a new fetcher and pass it to GraphiQL below
+var fetcher = createActionCableFetcher({ consumer: actionCable, url: "/graphql" })
+const root = createRoot(document.getElementById('root'));
+root.render();
+```
+
+Under the hood, it will split traffic: it will send `subscription { ... }` operations via ActionCable and send queries and mutations via HTTP `POST` using `window.fetch`. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createPusherFetcher({ ... })`.
diff --git a/guides/subscriptions/ably_implementation.md b/guides/subscriptions/ably_implementation.md
index f4710cf90d..fad7478a31 100644
--- a/guides/subscriptions/ably_implementation.md
+++ b/guides/subscriptions/ably_implementation.md
@@ -313,4 +313,8 @@ To receive webhooks in development, you can [use ngrok](https://www.ably.io/tuto
## Client configuration
-Install the [Ably JS client](https://github.com/ably/ably-js) then see docs for {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %}.
+Install the [Ably JS client](https://github.com/ably/ably-js) then see docs for:
+
+- {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %}
+- {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}.
+- {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %}
diff --git a/guides/subscriptions/action_cable_implementation.md b/guides/subscriptions/action_cable_implementation.md
index 03e663d523..3464159244 100644
--- a/guides/subscriptions/action_cable_implementation.md
+++ b/guides/subscriptions/action_cable_implementation.md
@@ -12,4 +12,8 @@ index: 4
To get started, see examples in the API docs: {{ "GraphQL::Subscriptions::ActionCableSubscriptions" | api_doc }}.
-See client usage for {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %} or {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}.
+See client usage for:
+
+- {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %}
+- {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}.
+- {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %}
diff --git a/guides/subscriptions/pusher_implementation.md b/guides/subscriptions/pusher_implementation.md
index 16bb02cc00..38ae45659a 100644
--- a/guides/subscriptions/pusher_implementation.md
+++ b/guides/subscriptions/pusher_implementation.md
@@ -296,4 +296,9 @@ To receive Pusher's webhooks in development, Pusher [suggests using ngrok](https
## Client configuration
-Install the [Pusher JS client](https://github.com/pusher/pusher-js) then see docs for {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %} or {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}.
+Install the [Pusher JS client](https://github.com/pusher/pusher-js) then see docs for:
+
+- {% internal_link "Apollo Client", "/javascript_client/apollo_subscriptions" %}
+- {% internal_link "Relay Modern", "/javascript_client/relay_subscriptions" %}
+- {% internal_link "GraphiQL", "/javascript_client/graphiql_subscriptions" %}
+- {% internal_link "urql", "/javascript_client/urql_subscriptions" %}
diff --git a/javascript_client/package.json b/javascript_client/package.json
index 5e99cc0e87..a992e7dac1 100644
--- a/javascript_client/package.json
+++ b/javascript_client/package.json
@@ -27,6 +27,7 @@
"pako": "^2.0.3",
"prettier": "^1.19.1",
"pusher-js": "^7.0.3",
+ "@rails/actioncable": "^7.0.0",
"relay-runtime": "11.0.2",
"ts-jest": "^29.0.0",
"typescript": "5.1.6",
diff --git a/javascript_client/src/subscriptions/__tests__/createAblyFetcherTest.ts b/javascript_client/src/subscriptions/__tests__/createAblyFetcherTest.ts
new file mode 100644
index 0000000000..f256ec53a9
--- /dev/null
+++ b/javascript_client/src/subscriptions/__tests__/createAblyFetcherTest.ts
@@ -0,0 +1,105 @@
+import createAblyFetcher from "../createAblyFetcher"
+import { Realtime } from "ably"
+
+function createAbly() {
+ const _channels: {[key: string]: any } = {}
+
+ const ably = {
+ _channels: _channels,
+ channels: {
+ get(channelName: string) {
+ return _channels[channelName] ||= {
+ _listeners: [] as [string, Function][],
+ name: channelName,
+ presence: {
+ enterClient(_clientName: string, _status: string) {},
+ leaveClient(_clientName: string) {},
+ },
+ detach(callback: Function) {
+ callback()
+ },
+ subscribe(eventName: string, callback: Function) {
+ this._listeners.push([eventName, callback])
+ },
+ unsubscribe(){}
+ }
+ },
+ release(channelName: string) {
+ delete _channels[channelName]
+ }
+ },
+ __testTrigger(channelName: string, eventName: string, data: any) {
+ const channel = this.channels.get(channelName)
+ const handler = channel._listeners.find((l: any) => l[0] == eventName)
+ if (handler) {
+ handler[1](data)
+ }
+ }
+ }
+
+ return ably
+}
+
+
+describe("createAblyFetcher", () => {
+ it("yields updates for subscriptions", () => {
+ const ably = createAbly()
+
+ const fetchLog: any[] = []
+ const dummyFetch = function(url: string, fetchArgs: any) {
+ fetchLog.push([url, fetchArgs.customOpt])
+ const dummyResponse = {
+ json: () => {
+ return {
+ data: {
+ hi: "First response"
+ }
+ }
+ },
+ headers: {
+ get() {
+ return fetchArgs.body.includes("subscription") ? "abcd" : null
+ }
+ }
+ }
+ return Promise.resolve(dummyResponse)
+ }
+
+ const fetcher = createAblyFetcher({
+ ably: (ably as unknown) as Realtime,
+ url: "/graphql",
+ fetch: ((dummyFetch as unknown) as typeof fetch),
+ fetchOptions: {customOpt: true}
+ })
+
+ const result = fetcher({
+ variables: {},
+ operationName: "hello",
+ body: "subscription hello { hi }"
+ }, {})
+
+ return result.next().then((res) => {
+ expect(res.value.data.hi).toEqual("First response")
+ expect(fetchLog).toEqual([["/graphql", true]])
+ }).then(() => {
+ const promise = result.next().then((res2) => {
+ expect(res2).toEqual({ value: { data: { hi: "Bonjour" } }, done: false })
+ })
+
+ ably.__testTrigger("abcd", "update", { data: { result: { data: { hi: "Bonjour" } } } })
+
+ return promise.then(() => {
+ // Test non-subscriptions too:
+ expect(Object.keys(ably._channels)).toEqual(["abcd"])
+ const queryResult = fetcher({ variables: {}, operationName: null, body: "{ __typename }"}, {})
+ return queryResult.next().then((res) => {
+ expect(res.value.data).toEqual({ hi: "First response"})
+ return queryResult.next().then((res2) => {
+ expect(res2.done).toEqual(true)
+ expect(ably._channels).toEqual({})
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/javascript_client/src/subscriptions/__tests__/createActionCableFetcherTest.ts b/javascript_client/src/subscriptions/__tests__/createActionCableFetcherTest.ts
new file mode 100644
index 0000000000..0a21f680da
--- /dev/null
+++ b/javascript_client/src/subscriptions/__tests__/createActionCableFetcherTest.ts
@@ -0,0 +1,66 @@
+import createActionCableFetcher from "../createActionCableFetcher"
+import type { Consumer } from "@rails/actioncable"
+import { parse } from "graphql"
+
+describe("createActionCableFetcherTest", () => {
+ it("yields updates for subscriptions", () => {
+ var handlers: any
+ var log: [string, any][]= []
+
+ var dummyActionCableConsumer = {
+ subscriptions: {
+ create: (_conn: any, newHandlers: any) => {
+ handlers = newHandlers
+ return {
+ perform: (evt: string, data: any) => {
+ log.push([evt, data])
+ }
+ }
+ }
+ }
+ }
+
+ const fetchLog: any[] = []
+ const dummyFetch = function(url: string, fetchArgs: any) {
+ fetchLog.push([url, fetchArgs.custom])
+ return Promise.resolve({ json: () => { {} } })
+ }
+
+ var options = {
+ consumer: (dummyActionCableConsumer as unknown) as Consumer,
+ url: "/graphql",
+ fetch: dummyFetch as typeof fetch,
+ fetchOptions: {
+ custom: true,
+ }
+ }
+
+ var fetcher = createActionCableFetcher(options)
+
+
+ const queryStr = "subscription listen { update { message } }"
+ const doc = parse(queryStr)
+
+ const res = fetcher({ operationName: "listen", query: queryStr, variables: {}}, { documentAST: doc })
+ const promise = res.next().then((result) => {
+
+ handlers.connected() // trigger the GraphQL send
+
+ expect(result).toEqual({ value: { data: "hello" } , done: false })
+ expect(fetchLog).toEqual([])
+ expect(log).toEqual([
+ ["execute", { operationName: "listen", query: queryStr, variables: {} }],
+ ])
+ })
+
+ handlers.received({ result: { data: "hello" } }) // simulate an update
+
+ return promise.then(() => {
+ let res2 = fetcher({ operationName: null, query: "{ __typename } ", variables: {}}, {})
+ const promise2 = res2.next().then(() => {
+ expect(fetchLog).toEqual([["/graphql", true]])
+ })
+ return promise2
+ })
+ })
+})
diff --git a/javascript_client/src/subscriptions/__tests__/createPusherFetcherTest.ts b/javascript_client/src/subscriptions/__tests__/createPusherFetcherTest.ts
new file mode 100644
index 0000000000..595baac353
--- /dev/null
+++ b/javascript_client/src/subscriptions/__tests__/createPusherFetcherTest.ts
@@ -0,0 +1,99 @@
+import createPusherFetcher from "../createPusherFetcher"
+import type Pusher from "pusher-js"
+
+type MockChannel = {
+ bind: (action: string, handler: Function) => void,
+ unsubscribe: () => void,
+}
+
+describe("createPusherFetcher", () => {
+ it("yields updates for subscriptions", () => {
+ const pusher = {
+ _channels: {} as {[key: string]: [string, Function][]},
+
+ trigger: function(channel: string, event: string, data: any) {
+ var handlers = this._channels[channel]
+ if (handlers) {
+ handlers.forEach(function(handler: [string, Function]) {
+ if (handler[0] == event) {
+ handler[1](data)
+ }
+ })
+ }
+ },
+ subscribe: function(channel: string): MockChannel {
+ var handlers = this._channels[channel]
+ if (!handlers) {
+ handlers = this._channels[channel] = []
+ }
+
+ return {
+ bind: (action: string, handler: Function): void => {
+ handlers.push([action, handler])
+ },
+ unsubscribe: () => {
+ delete this._channels[channel]
+ }
+ }
+ },
+ unsubscribe: (_channel: string): void => {
+ },
+ }
+
+ const fetchLog: any[] = []
+ const dummyFetch = function(url: string, fetchArgs: any) {
+ fetchLog.push([url, fetchArgs.customOpt])
+ const dummyResponse = {
+ json: () => {
+ return {
+ data: {
+ hi: "First response"
+ }
+ }
+ },
+ headers: {
+ get() {
+ return fetchArgs.body.includes("subscription") ? "abcd" : null
+ }
+ }
+ }
+ return Promise.resolve(dummyResponse)
+ }
+
+ const fetcher = createPusherFetcher({
+ pusher: (pusher as unknown) as Pusher,
+ url: "/graphql",
+ fetch: ((dummyFetch as unknown) as typeof fetch),
+ fetchOptions: {customOpt: true}
+ })
+
+ const result = fetcher({
+ variables: {},
+ operationName: "hello",
+ body: "subscription hello { hi }"
+ }, {})
+
+ return result.next().then((res) => {
+ expect(res.value.data.hi).toEqual("First response")
+ expect(fetchLog).toEqual([["/graphql", true]])
+ }).then(() => {
+ const promise = result.next().then((res2) => {
+ expect(res2).toEqual({ value: { data: { hi: "Bonjour" } }, done: false })
+ })
+ pusher.trigger("abcd", "update", { result: { data: { hi: "Bonjour" } } })
+
+ return promise.then(() => {
+ // Test non-subscriptions too:
+ expect(Object.keys(pusher._channels)).toEqual(["abcd"])
+ const queryResult = fetcher({ variables: {}, operationName: null, body: "{ __typename }"}, {})
+ return queryResult.next().then((res) => {
+ expect(res.value.data).toEqual({ hi: "First response"})
+ return queryResult.next().then((res2) => {
+ expect(res2.done).toEqual(true)
+ expect(pusher._channels).toEqual({})
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/javascript_client/src/subscriptions/createAblyFetcher.ts b/javascript_client/src/subscriptions/createAblyFetcher.ts
new file mode 100644
index 0000000000..dc360f4c08
--- /dev/null
+++ b/javascript_client/src/subscriptions/createAblyFetcher.ts
@@ -0,0 +1,92 @@
+import type Types from "ably"
+
+type AblyFetcherOptions = {
+ ably: Types.Realtime,
+ url: String,
+ fetch?: typeof fetch,
+ fetchOptions?: any,
+}
+
+type SubscriptionIteratorPayload = {
+ value: any,
+ done: Boolean
+}
+
+const clientName = "graphiql-subscriber"
+
+export default function createAblyFetcher(options: AblyFetcherOptions) {
+ var currentChannel: Types.Types.RealtimeChannelCallbacks | null = null
+
+ return async function*(graphqlParams: any, _fetcherParams: any) {
+ var nextPromiseResolve: Function | null = null
+ var shouldBreak = false
+
+ var iterator = {
+ [Symbol.asyncIterator]() {
+ return {
+ next(): Promise {
+ return new Promise((resolve, _reject) => {
+ nextPromiseResolve = resolve
+ })
+ },
+ return(): Promise {
+ if (currentChannel) {
+ currentChannel.presence.leaveClient(clientName)
+ currentChannel.unsubscribe()
+ const channelName = currentChannel.name
+ currentChannel.detach(() => {
+ options.ably.channels.release(channelName)
+ })
+ currentChannel = null
+ nextPromiseResolve = null
+ }
+ return Promise.resolve({ value: null, done: true })
+ }
+ }
+ }
+ }
+
+ const fetchFn = options.fetch || window.fetch
+ fetchFn("/graphql", {
+ method: "POST",
+ body: JSON.stringify(graphqlParams),
+ headers: {
+ 'content-type': 'application/json',
+ },
+ ... options.fetchOptions
+ }).then((r) => {
+ const subId = r.headers.get("X-Subscription-ID")
+ if (subId) {
+ currentChannel && currentChannel.unsubscribe()
+ currentChannel = options.ably.channels.get(subId, { modes: ["SUBSCRIBE", "PRESENCE"] })
+ currentChannel.presence.enterClient(clientName, "subscribed", (err) => {
+ if (err) {
+ console.error(err)
+ }
+ })
+ currentChannel.subscribe("update", (message: Types.Types.Message) => {
+ console.log("update", message)
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: message.data.result, done: false })
+ }
+ })
+
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: r.json(), done: false })
+ }
+ } else {
+ shouldBreak = true
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: r.json(), done: false})
+ }
+ }
+ })
+
+ for await (const payload of iterator) {
+ yield payload
+ if (shouldBreak) {
+ break
+ }
+ }
+ }
+}
diff --git a/javascript_client/src/subscriptions/createActionCableFetcher.ts b/javascript_client/src/subscriptions/createActionCableFetcher.ts
new file mode 100644
index 0000000000..22b5692120
--- /dev/null
+++ b/javascript_client/src/subscriptions/createActionCableFetcher.ts
@@ -0,0 +1,95 @@
+
+import { visit } from "graphql";
+import type { Consumer, Subscription } from "@rails/actioncable"
+
+type ActionCableFetcherOptions = {
+ consumer: Consumer,
+ url: String,
+ fetch?: typeof fetch,
+ fetchOptions?: any,
+}
+
+type SubscriptionIteratorPayload = {
+ value: any,
+ done: Boolean
+}
+
+export default function createActionCableFetcher(options: ActionCableFetcherOptions) {
+ let currentChannel: Subscription | null = null
+ const consumer = options.consumer
+
+ const subscriptionFetcher = async function*(graphqlParams: any, fetcherOpts: any) {
+ let isSubscription = false;
+ let nextPromiseResolve: Function | null = null;
+
+ fetcherOpts.documentAST && visit(fetcherOpts.documentAST, {
+ OperationDefinition(node) {
+ if (graphqlParams.operationName === node.name?.value && node.operation === 'subscription') {
+ isSubscription = true;
+ }
+ },
+ });
+
+ if (isSubscription) {
+ currentChannel?.unsubscribe()
+ currentChannel = consumer.subscriptions.create("GraphqlChannel",
+ {
+ connected: function() {
+ currentChannel?.perform("execute", {
+ query: graphqlParams.query,
+ operationName: graphqlParams.operationName,
+ variables: graphqlParams.variables,
+ })
+ },
+
+ received: function(data: any) {
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: data.result, done: false })
+ }
+ }
+ } as any
+ )
+
+ var iterator = {
+ [Symbol.asyncIterator]() {
+ return {
+ next(): Promise {
+ return new Promise((resolve, _reject) => {
+ nextPromiseResolve = resolve
+ })
+ },
+ return(): Promise {
+ if (currentChannel) {
+ currentChannel.unsubscribe()
+ currentChannel = null
+ }
+ return Promise.resolve({ value: null, done: true })
+ }
+ }
+ }
+ }
+
+ for await (const payload of iterator) {
+ yield payload
+ }
+ } else {
+ const fetchFn = options.fetch || window.fetch
+ // Not a subscription fetcher, post to the given URL
+ yield fetchFn("/graphql", {
+ method: "POST",
+ body: JSON.stringify({
+ query: graphqlParams.query,
+ operationName: graphqlParams.operationName,
+ variables: graphqlParams.variables,
+ }),
+ headers: {
+ 'content-type': 'application/json',
+ },
+ ... options.fetchOptions
+ }).then((r) => r.json())
+ return
+ }
+ }
+
+ return subscriptionFetcher
+}
diff --git a/javascript_client/src/subscriptions/createPusherFetcher.ts b/javascript_client/src/subscriptions/createPusherFetcher.ts
new file mode 100644
index 0000000000..f400ba6ab6
--- /dev/null
+++ b/javascript_client/src/subscriptions/createPusherFetcher.ts
@@ -0,0 +1,79 @@
+import type Pusher from "pusher-js"
+import type { Channel } from "pusher-js"
+
+type PusherFetcherOptions = {
+ pusher: Pusher,
+ url: String,
+ fetch?: typeof fetch,
+ fetchOptions: any,
+}
+
+type SubscriptionIteratorPayload = {
+ value: any,
+ done: Boolean
+}
+
+export default function createPusherFetcher(options: PusherFetcherOptions) {
+ var currentChannel: Channel | null = null
+
+ return async function*(graphqlParams: any, _fetcherParams: any) {
+ var nextPromiseResolve: Function | null = null
+ var shouldBreak = false
+
+ var iterator = {
+ [Symbol.asyncIterator]() {
+ return {
+ next(): Promise {
+ return new Promise((resolve, _reject) => {
+ nextPromiseResolve = resolve
+ })
+ },
+ return(): Promise {
+ if (currentChannel) {
+ currentChannel.unsubscribe()
+ currentChannel = null
+ }
+ return Promise.resolve({ value: null, done: true })
+ }
+ }
+ }
+ }
+
+ const fetchFn = options.fetch || window.fetch
+ fetchFn("/graphql", {
+ method: "POST",
+ body: JSON.stringify(graphqlParams),
+ headers: {
+ 'content-type': 'application/json',
+ },
+ ...options.fetchOptions
+ }).then((r) => {
+ const subId = r.headers.get("X-Subscription-ID")
+ if (subId) {
+ currentChannel && currentChannel.unsubscribe()
+ currentChannel = options.pusher.subscribe(subId)
+ currentChannel.bind("update", (payload: any) => {
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: payload.result, done: false })
+ }
+ })
+
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: r.json(), done: false })
+ }
+ } else {
+ shouldBreak = true
+ if (nextPromiseResolve) {
+ nextPromiseResolve({ value: r.json(), done: false})
+ }
+ }
+ })
+
+ for await (const payload of iterator) {
+ yield payload
+ if (shouldBreak) {
+ break
+ }
+ }
+ }
+}